Browse Source

Reverse runtime-async exception lowering for try-catch nested inside try-finally.

The single-handler try-catch matcher was tied to the top-level shape: it
required the try-catch be the last instruction in its parent block, that the
post-catch "no exception" path be a direct Leave that exits the function, and
that the flag-init's ILVariable be identical to the in-handler flag store.
None of those hold for an inner try-catch sitting inside an outer try-finally
where both await — the inner is followed by a `br continuation`, the no-exception
path leaves the outer try-block (not the function), and SplitVariables hands
out a separate ILVariable for the pre-init store.

Drop the "must be last instruction" gate, accept Leave-to-any-ancestor and
cross-container Branch as the no-exception exit (extracted into a new
`IsContainerExit` helper), and match the flag-init by slot/kind/type the same
way the multi-handler matcher already does.

Closes Cluster 3 from #3745.
pull/3731/head
Siegfried Pammer 4 days ago
parent
commit
8059fde539
  1. 19
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs
  2. 71
      ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

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

@ -383,6 +383,25 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -383,6 +383,25 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
return new object();
}
public async Task TryCatchFinallyAllAwait()
{
try
{
await Task.CompletedTask;
Console.WriteLine("try");
}
catch (Exception)
{
await Task.CompletedTask;
Console.WriteLine("catch");
}
finally
{
await Task.CompletedTask;
Console.WriteLine("finally");
}
}
#endif
public static async Task<int> GetIntegerSumAsync(IEnumerable<int> items)

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

@ -75,8 +75,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -75,8 +75,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
continue;
if (parentBlock.Parent is not BlockContainer container)
continue;
if (tryCatch.ChildIndex != parentBlock.Instructions.Count - 1)
continue;
if (tryCatch.Handlers.Count == 1)
{
@ -392,6 +390,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -392,6 +390,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return null;
}
// True when `leave` exits `container` itself or any ancestor container — i.e. control transfers
// out of `container`. Replaces the older `IsLeavingFunction` gate, which only matched the
// top-level case; nested runtime-async patterns also leave to intermediate containers.
static bool LeaveExitsContainer(Leave leave, BlockContainer container)
{
if (leave.TargetContainer == container)
return true;
return container.IsDescendantOf(leave.TargetContainer);
}
// Remove `stloc v(ldc.i4 0)` instructions immediately preceding `tryFinally` whose target
// variable is never read.
static void RemoveDeadFlagStores(Block parentBlock, TryFinally tryFinally)
@ -623,9 +631,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -623,9 +631,15 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
if (prefixCount != 0)
return false;
// Pre-try: somewhere before the TryCatch we expect `stloc num(ldc.i4 0)`.
// Pre-try: somewhere before the TryCatch we expect `stloc num(ldc.i4 0)`. After
// SplitVariables the pre-init's local may be a separate ILVariable, so match the slot
// and type rather than the ILVariable identity (same workaround as the multi-handler
// matcher).
var flagInitStore = FindFlagInitStore(parentBlock, tryCatch,
s => s.Variable == numVariable && s.Value.MatchLdcI4(0));
s => s.Variable.Index == numVariable.Index
&& s.Variable.Kind == numVariable.Kind
&& s.Variable.Type.IsKnownType(KnownTypeCode.Int32)
&& s.Value.MatchLdcI4(0));
if (flagInitStore == null)
return false;
@ -866,9 +880,13 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -866,9 +880,13 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
// Block continuation {
// Variant A: if (comp.i4(num != 1)) leave outer; br catchBody
// Variant B: if (comp.i4(num == 1)) br catchBody; leave outer
// Variant A: if (comp.i4(num != 1)) <exit>; br catchBody
// Variant B: if (comp.i4(num == 1)) br catchBody; <exit>
// }
// <exit> is either a direct Leave that exits `container` (or any ancestor) — which is the
// top-level shape where "no exception" leaves the function — or a Branch to a leave-block
// in `container`. The Branch form arises when the try-catch is nested (e.g. inside an
// outer try-finally), where "no exception" branches to the outer try-block's exit point.
static bool MatchCatchEntryCheck(Block continuation, ILVariable numVariable, BlockContainer container,
out Block catchBodyEntry, out ILInstruction afterCatchExit)
{
@ -880,31 +898,60 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow @@ -880,31 +898,60 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
if (continuation.Instructions[0] is not IfInstruction ifInst)
return false;
// Equals form: if (num == 1) br catchBody ; <fallthrough leave outer>
// Equals form: if (num == 1) br catchBody ; <exit>
if (ifInst.Condition.MatchCompEquals(out var lhs, out var rhs)
&& lhs.MatchLdLoc(numVariable) && rhs.MatchLdcI4(1)
&& ifInst.TrueInst is Branch eqBranch
&& continuation.Instructions[1] is Leave eqLeave && eqLeave.IsLeavingFunction)
&& IsContainerExit(continuation.Instructions[1], container))
{
catchBodyEntry = eqBranch.TargetBlock;
afterCatchExit = eqLeave;
afterCatchExit = continuation.Instructions[1];
return catchBodyEntry?.Parent == container;
}
// Not-equals form: if (num != 1) leave outer ; br catchBody
// Not-equals form: if (num != 1) <exit> ; br catchBody
if (ifInst.Condition.MatchCompNotEquals(out lhs, out rhs)
&& lhs.MatchLdLoc(numVariable) && rhs.MatchLdcI4(1)
&& ifInst.TrueInst is Leave neLeave && neLeave.IsLeavingFunction
&& IsContainerExit(ifInst.TrueInst, container)
&& continuation.Instructions[1] is Branch neBranch)
{
catchBodyEntry = neBranch.TargetBlock;
afterCatchExit = neLeave;
afterCatchExit = ifInst.TrueInst;
return catchBodyEntry?.Parent == container;
}
return false;
}
// True when `inst` transfers control out of `container`. Accepts three shapes that all
// arise from runtime-async lowering:
// - direct `Leave` to `container` or any ancestor;
// - cross-container `Branch` whose target lives in a strict ancestor of `container` (the
// inner-try-catch-inside-outer-try-finally case, where Roslyn emits a single branch that
// spans the inner container);
// - `Branch` to a one-instruction leave-block in `container` (the indirected canonical
// leave-via-helper-block form).
static bool IsContainerExit(ILInstruction inst, BlockContainer container)
{
if (inst is Leave leave)
return LeaveExitsContainer(leave, container);
if (inst is Branch br && br.TargetBlock != null)
{
var targetContainer = br.TargetBlock.Parent as BlockContainer;
if (targetContainer == null)
return false;
if (targetContainer != container && container.IsDescendantOf(targetContainer))
return true;
if (targetContainer == container
&& br.TargetBlock.Instructions.Count == 1
&& br.TargetBlock.Instructions[0] is Leave brLeave)
{
return LeaveExitsContainer(brLeave, container);
}
}
return false;
}
static void ReplaceVariableReadsWithHandlerVariable(ILInstruction root, ILVariable from, ILVariable to)
{
foreach (var ldloc in root.Descendants.OfType<LdLoc>().ToArray())

Loading…
Cancel
Save