Fix #3825: reconstruct async iterators with [EnumeratorCancellation] and await in finally#3831
Open
sailro wants to merge 1 commit into
Open
Conversation
…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
Contributor
There was a problem hiding this comment.
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 combinedCancellationTokenSourcedisposal in both the set-result/return path and the catch handler path. - Adds a Pretty test case covering
[EnumeratorCancellation]+awaitinfinallyto 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3825.
An
async IAsyncEnumerable<T>iterator that has both an[EnumeratorCancellation]cancellation-token parameter and an
awaitinside afinallywas notreconstructed: 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)) isemitted before the combined
CancellationTokenSourcedisposal in both theset-result block and the catch block.
CheckSetResultReturnBlockandValidateCatchBlockonly consumed that cleanup after the disposal, so thepos + 2 == counttest missed the dispose pattern, the symbolic analysis failed,and
AsyncAwaitDecompilerleft the raw state machine.Fix
Allow the hoisted-local cleanup to appear before the combined-tokens disposal as
well (one extra
MatchHoistedLocalCleanupcall in each of the two methods).Test
Pretty/AsyncStreams.AwaitInFinallyWithCancellationcombines[EnumeratorCancellation]with anawaitin afinally; it now round-trips.Real-world occurrence: Renci.SshNet
SftpClient.ListDirectoryAsync(net462).