From d9ce405354d82f8eb8a8c123c13d586fe843ca87 Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Fri, 26 Jun 2026 13:24:29 +0200 Subject: [PATCH] Fix #3825: handle hoisted-local cleanup before combined-tokens 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 --- .../TestCases/Pretty/AsyncStreams.cs | 14 ++++++++++++++ .../IL/ControlFlow/AsyncAwaitDecompiler.cs | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncStreams.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncStreams.cs index ecf39897d..f7ea3f010 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncStreams.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncStreams.cs @@ -71,6 +71,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty await Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false); } } + + public static async IAsyncEnumerable AwaitInFinallyWithCancellation([EnumeratorCancellation] CancellationToken cancellationToken) + { + await Task.Yield(); + try + { + yield return 1; + await Task.Delay(10, cancellationToken); + } + finally + { + await Task.Yield(); + } + } } public struct TestStruct diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs index 8e5619d1f..04baaf358 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -911,6 +911,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow finalStateKnown = true; pos++; + // [optional] stfld <>u__N(ldloc this, ldnull) + // The hoisted-local cleanup may appear either before or after the combined-tokens disposal + // (the latter is present for async iterators with an [EnumeratorCancellation] token). + MatchHoistedLocalCleanup(block, ref pos); + if (pos + 2 == block.Instructions.Count && block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) { if (MatchDisposeCombinedTokens(blockContainer, condition, trueInst, falseInst, blocksAnalyzed, out var setResultAndExitBlock)) @@ -1088,6 +1093,12 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow finalState = newState; finalStateKnown = true; } + + // [optional] stfld <>u__N(ldloc this, ldnull) + // The hoisted-local cleanup may appear either before or after the combined-tokens disposal + // (the latter is present for async iterators with an [EnumeratorCancellation] token). + MatchHoistedLocalCleanup(catchBlock, ref pos); + if (pos + 2 == catchBlock.Instructions.Count && catchBlock.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) { if (MatchDisposeCombinedTokens(handlerContainer, condition, trueInst, falseInst, blocksAnalyzed, out var setResultAndExitBlock))