Browse Source

Reverse runtime-async multi-handler try-catch dispatched via an if-chain.

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; <leave outer | br end> }

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.
pull/3731/head
Siegfried Pammer 4 days ago
parent
commit
a9a43f96a9
  1. 54
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs
  2. 70
      ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

54
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs

@ -414,6 +414,60 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -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

70
ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

@ -800,7 +800,10 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -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 @@ -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 @@ -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
// <leave outer | br defaultExitBlock>
// }
// Where the chain may extend beyond two if-blocks.
static bool MatchIfChainDispatch(Block continuation, ILVariable numVariable,
BlockContainer container, out Dictionary<int, Block> caseBlocks, out ILInstruction defaultExit)
{
caseBlocks = new Dictionary<int, Block>();
defaultExit = null;
var visited = new HashSet<Block>();
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: <leave outer | br end> } }
static bool MatchSwitchDispatch(Block continuation, ILVariable numVariable,
out Dictionary<int, Block> caseBlocks, out ILInstruction defaultExit)

Loading…
Cancel
Save