Skip to content

Fix #3825: reconstruct async iterators with [EnumeratorCancellation] and await in finally#3831

Open
sailro wants to merge 1 commit into
icsharpcode:masterfrom
sailro:fix-async-iterator-enumeratorcancellation
Open

Fix #3825: reconstruct async iterators with [EnumeratorCancellation] and await in finally#3831
sailro wants to merge 1 commit into
icsharpcode:masterfrom
sailro:fix-async-iterator-enumeratorcancellation

Conversation

@sailro

@sailro sailro commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #3825.

An async IAsyncEnumerable<T> iterator that has both an [EnumeratorCancellation]
cancellation-token parameter and an await inside a finally was not
reconstructed: the decompiler emitted the raw compiler-generated state machine
(catch (object), goto case, <>1__state, ...), which does not compile.

Root cause

For such an iterator, the hoisted-local cleanup (stfld <>u__N(this, null)) is
emitted before the combined CancellationTokenSource disposal in both the
set-result block and the catch block. CheckSetResultReturnBlock and
ValidateCatchBlock only consumed that cleanup after the disposal, so the
pos + 2 == count test missed the dispose pattern, the symbolic analysis failed,
and AsyncAwaitDecompiler left the raw state machine.

Fix

Allow the hoisted-local cleanup to appear before the combined-tokens disposal as
well (one extra MatchHoistedLocalCleanup call in each of the two methods).

Test

Pretty/AsyncStreams.AwaitInFinallyWithCancellation combines
[EnumeratorCancellation] with an await in a finally; it now round-trips.

Real-world occurrence: Renci.SshNet SftpClient.ListDirectoryAsync (net462).

…kens disposal

For an async iterator with an [EnumeratorCancellation] cancellation token, the
hoisted-local cleanup (stfld <>u__N(this, null)) can be emitted before the
combined CancellationTokenSource disposal in the set-result and catch blocks.
CheckSetResultReturnBlock and ValidateCatchBlock only consumed that cleanup after
the disposal, so the `pos + 2 == count` test missed the dispose pattern and the
analysis failed, leaving the raw state machine (catch (object), goto case, ...).

Allow the cleanup to appear before the combined-tokens disposal as well.

Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a decompilation failure for async IAsyncEnumerable<T> methods that combine an [EnumeratorCancellation] cancellation token parameter with an await inside a finally, where ILSpy previously fell back to emitting the raw compiler-generated state machine (which does not compile).

Changes:

  • Adjusts async iterator reconstruction to accept hoisted-local cleanup (stfld <>u__N(..., null)) appearing before combined CancellationTokenSource disposal in both the set-result/return path and the catch handler path.
  • Adds a Pretty test case covering [EnumeratorCancellation] + await in finally to ensure round-trip decompilation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs Allows MatchHoistedLocalCleanup to be consumed before the combined-tokens disposal pattern in the set-result block and catch block, fixing reconstruction for the reported iterator shape.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncStreams.cs Adds a regression test that exercises an async iterator with [EnumeratorCancellation] and an await in finally.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

async IAsyncEnumerable with [EnumeratorCancellation] and await in finally decompiles to a raw state machine

2 participants