Browse Source

Reverse runtime-async try-finally where the try body always throws.

`try { throw new ...(); } finally { await ... }` lowers to a try whose only
exit is the throw (handled by the synthetic catch). The existing matcher
required at least one outward Branch to the continuation, which is too strict
— a throw-only try body produces zero outward branches but is still a valid
lowered shape. Two follow-on fixes were also needed:

  - The pre-init's ILVariable diverges from the in-handler store after
    SplitVariables when the try body has no path that reaches the dispatch's
    load without going through the catch; match the flag init by slot/kind/type
    instead of identity (same workaround the multi-handler matcher uses).
  - With a throw-only try body the new TryFinally has unreachable endpoint,
    so appending the no-exception successor after it would put a non-final
    unreachable-endpoint instruction in the parent block. Skip the append in
    that case — the parent block's endpoint is already correctly unreachable.

Closes Cluster 4 from #3745.
pull/3731/head
Siegfried Pammer 4 days ago
parent
commit
8a03ee246f
  1. 12
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs
  2. 38
      ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

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

@ -402,6 +402,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine("finally"); Console.WriteLine("finally");
} }
} }
public async Task ThrowInsideTryFinally()
{
try
{
throw new InvalidOperationException();
}
finally
{
await Task.Yield();
}
}
#endif #endif
public static async Task<int> GetIntegerSumAsync(IEnumerable<int> items) public static async Task<int> GetIntegerSumAsync(IEnumerable<int> items)

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

@ -278,18 +278,23 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return false; return false;
// Pre-try: somewhere among the instructions preceding the TryCatch we expect // Pre-try: somewhere among the instructions preceding the TryCatch we expect
// an `stloc obj(ldnull)`. Other (unrelated) stores may be interleaved. // an `stloc obj(ldnull)`. Other (unrelated) stores may be interleaved. After
// SplitVariables, when the try body always throws the pre-init's local is a separate
// ILVariable from the in-handler store, so match by slot/kind/type rather than identity.
var flagInitStore = FindFlagInitStore(parentBlock, tryCatch, var flagInitStore = FindFlagInitStore(parentBlock, tryCatch,
s => s.Variable == objectVariable && s.Value.MatchLdNull()); s => s.Variable.Index == objectVariable.Index
&& s.Variable.Kind == objectVariable.Kind
&& s.Variable.Type.IsKnownType(KnownTypeCode.Object)
&& s.Value.MatchLdNull());
if (flagInitStore == null) if (flagInitStore == null)
return false; return false;
// Every outward exit of the try body must branch to `continuation`. The runtime-async // Every outward exit of the try body must branch to `continuation`. The runtime-async
// lowering rewrites every return / fallthrough to `br continuation` and routes throws // lowering rewrites every return / fallthrough to `br continuation` and routes throws
// through the synthetic catch, so a Leave or a Branch to anything else means we're not // through the synthetic catch, so a Leave or a Branch to anything else means we're not
// looking at a lowered shape. We also require at least one such exit, to reject try // looking at a lowered shape. A try body with no outward exit at all is also fine —
// bodies with no outward control flow at all. // the user wrote `try { throw ...; } finally { await ... }`, which lowers to a try body
bool seenExit = false; // whose only exit is the throw (handled by the synthetic catch).
foreach (var inst in tryCatch.TryBlock.Descendants.OfType<IBranchOrLeaveInstruction>()) foreach (var inst in tryCatch.TryBlock.Descendants.OfType<IBranchOrLeaveInstruction>())
{ {
// Skip intra-tryBody control flow: inst.TargetContainer being tryBody itself or any // Skip intra-tryBody control flow: inst.TargetContainer being tryBody itself or any
@ -298,12 +303,9 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
if (inst.TargetContainer.IsDescendantOf(tryCatch.TryBlock)) if (inst.TargetContainer.IsDescendantOf(tryCatch.TryBlock))
continue; continue;
if (inst is Branch branch && branch.TargetBlock == continuation) if (inst is Branch branch && branch.TargetBlock == continuation)
seenExit = true; continue;
else
return false;
}
if (!seenExit)
return false; return false;
}
// Find the dispatch idiom at the end of the finally body. // Find the dispatch idiom at the end of the finally body.
// Pattern: a block ending with "if (obj == null) leave outer; br dispatchHead" // Pattern: a block ending with "if (obj == null) leave outer; br dispatchHead"
@ -355,11 +357,17 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
// Append a successor instruction so the parent block remains EndPointUnreachable. // Append a successor instruction so the parent block remains EndPointUnreachable.
// If there was a separate leave-block, branch to it (it stays in the outer container); // If there was a separate leave-block, branch to it (it stays in the outer container);
// otherwise reuse the original Leave-with-value — RewriteFinallyExit already detached // otherwise reuse the original Leave-with-value — RewriteFinallyExit already detached
// it from the if-instruction that previously held it. // it from the if-instruction that previously held it. Skip when the try body always
if (leaveBlock != null) // throws — the resulting TryFinally's endpoint is unreachable and a successor
parentBlock.Instructions.Add(new Branch(leaveBlock).WithILRange(afterFinallyExit)); // instruction after it would put a non-final unreachable-endpoint instruction in the
else // block, violating the block invariant.
parentBlock.Instructions.Add(afterFinallyExit); if (!tryFinally.HasFlag(InstructionFlags.EndPointUnreachable))
{
if (leaveBlock != null)
parentBlock.Instructions.Add(new Branch(leaveBlock).WithILRange(afterFinallyExit));
else
parentBlock.Instructions.Add(afterFinallyExit);
}
// Remove the pre-init `stloc obj(ldnull)`. Also remove any dead // Remove the pre-init `stloc obj(ldnull)`. Also remove any dead
// `stloc <int>(ldc.i4 0)` immediately preceding the TryFinally — the runtime-async // `stloc <int>(ldc.i4 0)` immediately preceding the TryFinally — the runtime-async

Loading…
Cancel
Save