From a9a43f96a9bb71f5b919744a8074e34e5236a248 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 20 May 2026 14:43:32 +0200 Subject: [PATCH] Reverse runtime-async multi-handler try-catch dispatched via an if-chain. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-handler matcher only recognized a switch-instruction dispatch — but when a try-catch has just two handlers (or a handful with non-consecutive K values), Roslyn emits an if-chain instead: if (num == K_1) br case_K_1; br nextBlock ; nextBlock { if (num == K_2) br case_K_2; } Add a parallel matcher that walks the if-chain and collects (K, case-block) pairs the same way MatchSwitchDispatch does, plus the terminating leave/branch as the default exit. Call it as a fallback when the switch matcher rejects. Also clone the default-exit before re-adding it to the continuation block — in the if-chain shape it's a child of a *different* block (a later step in the chain), not the now-cleared switch instruction, so the in-place re-add relied on the switch's release cascade and didn't generalize. Closes Cluster 2 from #3745. --- .../TestCases/Pretty/Async.cs | 54 ++++++++++++++ .../RuntimeAsyncExceptionRewriteTransform.cs | 70 +++++++++++++++++-- 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs index 7d5f34126..f5d2e7a71 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs @@ -414,6 +414,60 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty await Task.Yield(); } } + + public async Task HeterogeneousMultiCatch1() + { + try + { + await Task.Yield(); + } + catch (InvalidOperationException ex) + { + await Task.Yield(); + Console.WriteLine(ex.Message); + } + catch (ArgumentException ex2) + { + await Task.Yield(); + Console.WriteLine(ex2.Message); + } + } + + public async Task HeterogeneousMultiCatch2() + { + try + { + await Task.Yield(); + } + catch (InvalidOperationException ex) + { + await Task.Yield(); + Console.WriteLine(ex.Message); + } + catch + { + await Task.Yield(); + Console.WriteLine("other"); + } + } + + public async Task HeterogeneousMultiCatch3() + { + try + { + await Task.Yield(); + } + catch (InvalidOperationException ex) + { + await Task.Yield(); + Console.WriteLine(ex.Message); + } + catch (Exception) + { + await Task.Yield(); + throw; + } + } #if RUNTIMEASYNC // The state-machine async lowering doesn't recognize return-from-try-with-await-in-finally // and decompiles these as `int result; try { ... } finally { ... } return result;`. The diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs index bdd7a8f37..ee03793c1 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs @@ -800,7 +800,10 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow if (flagInitStore == null) return false; // Block continuation { switch (ldloc num) { case K: br case_K ... ; default: leave outer } } - if (!MatchSwitchDispatch(continuation, numVariable, out var caseBlocks, out var defaultExit)) + // — or, for a small number of handlers (typically 2) where Roslyn emits an if-chain + // instead of a switch — a chain of `if (num == K_i) br case_K_i` blocks ending in a leave. + if (!MatchSwitchDispatch(continuation, numVariable, out var caseBlocks, out var defaultExit) + && !MatchIfChainDispatch(continuation, numVariable, container, out caseBlocks, out defaultExit)) return false; // Every K we recorded must have a case in the switch. foreach (var info in handlerInfos) @@ -837,11 +840,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow ReplaceDispatchIdiomWithRethrow(b, handler.Variable, context); } - // Replace continuation with the default exit (leave outer). - // Clear() already detached defaultExit via the SwitchInstruction's release-ref cascade, - // so we can re-add it directly. + // Replace continuation with the default exit (leave outer). Clone the default-exit so + // we don't worry about whose tree it currently belongs to (the switch instruction we're + // tearing down, or a later block in an if-chain dispatch). Clear the clone's ILRange — + // it now sits at a different location than the original, so reusing the source offset + // would produce wrong sequence points. + var defaultExitClone = defaultExit.Clone(); + defaultExitClone.SetILRange(new Interval()); continuation.Instructions.Clear(); - continuation.Instructions.Add(defaultExit); + continuation.Instructions.Add(defaultExitClone); // Remove the pre-try `stloc num(0)`. parentBlock.Instructions.RemoveAt(flagInitStore.ChildIndex); @@ -850,6 +857,59 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow return true; } + // For 2-handler multi-catches, Roslyn emits an if-chain rather than a switch: + // Block continuation { + // if (ldloc num == K_1) br case_K_1 + // br nextBlock + // } + // Block nextBlock { + // if (ldloc num == K_2) br case_K_2 + // + // } + // Where the chain may extend beyond two if-blocks. + static bool MatchIfChainDispatch(Block continuation, ILVariable numVariable, + BlockContainer container, out Dictionary caseBlocks, out ILInstruction defaultExit) + { + caseBlocks = new Dictionary(); + defaultExit = null; + var visited = new HashSet(); + var current = continuation; + while (true) + { + if (!visited.Add(current)) + return false; + if (current.Instructions.Count != 2) + return false; + if (current.Instructions[0] is not IfInstruction ifInst) + return false; + if (!ifInst.Condition.MatchCompEquals(out var lhs, out var rhs) + || !lhs.MatchLdLoc(numVariable) + || !rhs.MatchLdcI4(out int k)) + return false; + if (!ifInst.TrueInst.MatchBranch(out var caseBlock)) + return false; + if (caseBlocks.ContainsKey(k)) + return false; + caseBlocks[k] = caseBlock; + var fallthrough = current.Instructions[1]; + if (fallthrough is Leave directLeave && IsLeaveToContainerOrAncestor(directLeave, container)) + { + defaultExit = directLeave; + return caseBlocks.Count > 0; + } + if (!fallthrough.MatchBranch(out var nextBlock) || nextBlock.Parent != container) + return false; + // One-instruction leave block ends the chain — typical for "switch default" / "no case matched". + if (nextBlock.Instructions.Count == 1 && nextBlock.Instructions[0] is Leave finalLeave + && IsLeaveToContainerOrAncestor(finalLeave, container)) + { + defaultExit = finalLeave; + return caseBlocks.Count > 0; + } + current = nextBlock; + } + } + // Block continuation { switch (ldloc num) { case [K..K+1): br case_K ... ; default: } } static bool MatchSwitchDispatch(Block continuation, ILVariable numVariable, out Dictionary caseBlocks, out ILInstruction defaultExit)