Skip to content

Commit 896fb16

Browse files
Reverse runtime-async flag-based early-return across a try-finally.
When a return crosses an enclosing try-finally with await, runtime-async lowers it as: capture the return value, set an int flag to a unique non-zero value, leave the try block normally so the finally runs, then post-finally check "if (flag == K) return capture;". Detect that pattern after my outer try-finally rewrite (or, in optimized builds, the compiler-emitted TryFinally directly) and replace each capture-flag-and-leave site with a direct "leave outer (capture)" — the leave still passes through the TryFinally, so the user's finally body executes before the function returns, which matches the source-level semantics. Handles both the "if (flag == K)" and "if (flag != K)" check forms (the optimizer emits the latter). Closes the last gap in Issue2436 — RuntimeAsync now passes both Optimize and non-Optimize modes; the full RuntimeAsync* sweep is 12/12 green. Also remap reads of the captured-obj local inside the cleaned filter so optimized builds (where Roslyn inlines the typed-cast directly into the user filter expression instead of stashing it in a local) render against the catch variable rather than against "((T)obj)". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2f98082 commit 896fb16

1 file changed

Lines changed: 195 additions & 2 deletions

File tree

ICSharpCode.Decompiler/IL/ControlFlow/RuntimeAsyncExceptionRewriteTransform.cs

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ public static void Run(ILFunction function, ILTransformContext context)
108108
}
109109
}
110110

111+
// Recognize the "early return from inside a try via a flag local" pattern that runtime-async
112+
// emits whenever a return statement crosses an enclosing try-finally with await.
113+
foreach (var tryFinally in function.Descendants.OfType<TryFinally>().ToArray())
114+
{
115+
if (TryRewriteFlagBasedEarlyReturn(tryFinally, context))
116+
changed = true;
117+
}
118+
111119
if (changed)
112120
{
113121
foreach (var c in function.Body.Descendants.OfType<BlockContainer>())
@@ -204,8 +212,12 @@ static bool NormalizeRuntimeAsyncFilter(TryCatchHandler handler, ILFunction func
204212
if (typedExVar != null)
205213
RemapVariableReads(function, typedExVar, handler.Variable);
206214

207-
// The obj variable is shared between handlers in the multi-handler form; remap it later,
208-
// scoped to the moved catch body, when TryRewriteTryCatch / multi-handler relocates it.
215+
// Optimized builds inline `castclass T(ldloc obj)` directly into the user filter expression
216+
// instead of stashing it in a typedEx local. Remap obj reads within the filter only.
217+
RemapVariableReads(handler.Filter, objVar, handler.Variable);
218+
219+
// The obj variable is shared between handlers in the multi-handler form; the body rewriter
220+
// remaps it scoped per handler.
209221
filterObjByHandler[handler] = objVar;
210222

211223
context.StepEndGroup(keepIfEmpty: true);
@@ -912,6 +924,187 @@ static void ReplaceVariableReadsWithHandlerVariable(ILInstruction root, ILVariab
912924
}
913925
}
914926

927+
// Recognize the runtime-async lowering of an early return that crosses a try-finally.
928+
// Roslyn rewrites `return value;` inside a try-block as:
929+
// stloc capture(value)
930+
// stloc flag(K)
931+
// leave-try (i.e. let the finally run, then exit the try-finally)
932+
// followed by post-try logic of the form:
933+
// if (flag == K) leave outer (capture)
934+
//
935+
// Detect that pattern around a TryFinally we just produced and rewrite each capture-set-flag-and-leave
936+
// site into a direct `leave outer (value)`, then drop the flag/post-flag-check machinery. The leave
937+
// still passes through the TryFinally so the user's finally body runs before the function returns,
938+
// which is the intended source-level semantics of `return` from inside a try-finally.
939+
static bool TryRewriteFlagBasedEarlyReturn(TryFinally tryFinally, ILTransformContext context)
940+
{
941+
if (tryFinally.Parent is not Block parentBlock)
942+
return false;
943+
if (parentBlock.Parent is not BlockContainer container)
944+
return false;
945+
946+
// The TryFinally is followed in parentBlock by either nothing (fall-through into the next block)
947+
// or a single `br checkBlock` we appended ourselves. Locate the post-try check block.
948+
Block checkBlock;
949+
int tryFinallyIdx = tryFinally.ChildIndex;
950+
if (tryFinallyIdx == parentBlock.Instructions.Count - 1)
951+
return false;
952+
if (parentBlock.Instructions[tryFinallyIdx + 1] is Branch br)
953+
checkBlock = br.TargetBlock;
954+
else
955+
return false;
956+
if (checkBlock?.Parent != container)
957+
return false;
958+
959+
// checkBlock: `if (flagVar == K) br earlyBlock; br normalBlock`
960+
// or: `if (flagVar != K) br normalBlock; br earlyBlock`
961+
if (checkBlock.Instructions.Count != 2)
962+
return false;
963+
if (checkBlock.Instructions[0] is not IfInstruction ifInst)
964+
return false;
965+
if (ifInst.TrueInst is not Branch toIfTrue)
966+
return false;
967+
if (!checkBlock.Instructions[1].MatchBranch(out var fallthroughBlock))
968+
return false;
969+
970+
ILVariable flagVar;
971+
int targetK;
972+
Block earlyBlock, normalBlock;
973+
if (ifInst.Condition.MatchCompEquals(out var lhs, out var rhs)
974+
&& lhs.MatchLdLoc(out flagVar) && rhs.MatchLdcI4(out targetK))
975+
{
976+
earlyBlock = toIfTrue.TargetBlock;
977+
normalBlock = fallthroughBlock;
978+
}
979+
else if (ifInst.Condition.MatchCompNotEquals(out lhs, out rhs)
980+
&& lhs.MatchLdLoc(out flagVar) && rhs.MatchLdcI4(out targetK))
981+
{
982+
normalBlock = toIfTrue.TargetBlock;
983+
earlyBlock = fallthroughBlock;
984+
}
985+
else
986+
{
987+
return false;
988+
}
989+
if (!flagVar.Type.IsKnownType(KnownTypeCode.Int32))
990+
return false;
991+
if (earlyBlock?.Parent != container || normalBlock?.Parent != container)
992+
return false;
993+
994+
// earlyBlock chain: optional `stloc returnVar(ldloc capture); br leaveBlock` followed by
995+
// `leave outer (ldloc returnVar)` (or a direct leave with the capture).
996+
if (!ResolveEarlyReturnValue(earlyBlock, container, out var captureVar, out var returnVar, out var leaveBlock))
997+
return false;
998+
999+
// Inside the try-block find the flag-setter block(s): `stloc flagVar(K); leave-tryBlock`.
1000+
// There may be multiple — e.g. several catches in a multi-handler try each with its own
1001+
// early-return — but for the simple case we only need one.
1002+
if (tryFinally.TryBlock is not BlockContainer tryBlockContainer)
1003+
return false;
1004+
var flagSetters = new List<Block>();
1005+
foreach (var b in tryBlockContainer.Blocks)
1006+
{
1007+
if (b.Instructions.Count == 2
1008+
&& b.Instructions[0] is StLoc setStore
1009+
&& setStore.Variable == flagVar
1010+
&& setStore.Value.MatchLdcI4(targetK)
1011+
&& b.Instructions[1] is Leave leaveFromTry
1012+
&& leaveFromTry.TargetContainer == tryBlockContainer)
1013+
{
1014+
flagSetters.Add(b);
1015+
}
1016+
}
1017+
if (flagSetters.Count == 0)
1018+
return false;
1019+
1020+
// Verify flagVar is only set in flag-setters and the pre-try init (`stloc flagVar(0)`).
1021+
foreach (var store in flagVar.StoreInstructions.OfType<StLoc>())
1022+
{
1023+
if (flagSetters.Any(fs => fs.Instructions.Contains(store)))
1024+
continue;
1025+
if (store.Parent == parentBlock && store.Value.MatchLdcI4(0))
1026+
continue;
1027+
return false;
1028+
}
1029+
1030+
// Build the leave instruction shape: leave outer (ldloc captureSource).
1031+
var outerContainer = (BlockContainer)leaveBlock.Parent;
1032+
var outerLeave = (Leave)leaveBlock.Instructions[leaveBlock.Instructions.Count - 1];
1033+
if (outerLeave.TargetContainer != outerContainer)
1034+
return false;
1035+
1036+
context.StepStartGroup("Reduce runtime-async flag-based early return", tryFinally);
1037+
1038+
// For each flag-setter, redirect predecessor branches to a new "leave outer (capture)".
1039+
foreach (var fs in flagSetters)
1040+
{
1041+
foreach (var pred in container.Descendants.OfType<Branch>().ToArray())
1042+
{
1043+
if (pred.TargetBlock != fs)
1044+
continue;
1045+
if (!pred.IsDescendantOf(tryBlockContainer))
1046+
continue;
1047+
// The predecessor block's tail is: ...; stloc capture(value); br fs.
1048+
// Replace `br fs` with `leave outer (ldloc capture)` and let the existing capture
1049+
// store stay (it now feeds the leave value via the variable read).
1050+
pred.ReplaceWith(new Leave(outerContainer, new LdLoc(captureVar)).WithILRange(pred));
1051+
}
1052+
fs.Remove();
1053+
}
1054+
1055+
// The post-flag-check block can now be replaced with `br normalBlock`. The flag write/read,
1056+
// the early-return chain and (eventually) the captureVar/returnVar become unreferenced.
1057+
checkBlock.Instructions.Clear();
1058+
checkBlock.Instructions.Add(new Branch(normalBlock));
1059+
1060+
// Drop the pre-try `stloc flagVar(0)` (and the `stloc returnVar(default)` if present).
1061+
for (int i = 0; i < tryFinallyIdx; i++)
1062+
{
1063+
if (parentBlock.Instructions[i] is StLoc s && s.Variable == flagVar && s.Value.MatchLdcI4(0))
1064+
{
1065+
parentBlock.Instructions.RemoveAt(i);
1066+
tryFinallyIdx--;
1067+
i--;
1068+
}
1069+
}
1070+
1071+
context.StepEndGroup(keepIfEmpty: true);
1072+
return true;
1073+
}
1074+
1075+
// earlyBlock should be either:
1076+
// `stloc returnVar(ldloc capture); br leaveBlock` followed by leaveBlock = `leave outer (ldloc returnVar)`
1077+
// or a direct `leave outer (ldloc capture)`.
1078+
static bool ResolveEarlyReturnValue(Block earlyBlock, BlockContainer container,
1079+
out ILVariable captureVar, out ILVariable returnVar, out Block leaveBlock)
1080+
{
1081+
captureVar = null;
1082+
returnVar = null;
1083+
leaveBlock = null;
1084+
if (earlyBlock.Instructions.Count == 2
1085+
&& earlyBlock.Instructions[0] is StLoc rvStore
1086+
&& rvStore.Value.MatchLdLoc(out captureVar)
1087+
&& earlyBlock.Instructions[1].MatchBranch(out leaveBlock)
1088+
&& leaveBlock?.Parent == container
1089+
&& leaveBlock.Instructions.Count == 1
1090+
&& leaveBlock.Instructions[0] is Leave finalLeave
1091+
&& finalLeave.IsLeavingFunction
1092+
&& finalLeave.Value.MatchLdLoc(rvStore.Variable))
1093+
{
1094+
returnVar = rvStore.Variable;
1095+
return true;
1096+
}
1097+
if (earlyBlock.Instructions.Count == 1
1098+
&& earlyBlock.Instructions[0] is Leave directLeave
1099+
&& directLeave.IsLeavingFunction
1100+
&& directLeave.Value.MatchLdLoc(out captureVar))
1101+
{
1102+
leaveBlock = earlyBlock;
1103+
return true;
1104+
}
1105+
return false;
1106+
}
1107+
9151108
static void ReplaceDispatchIdiomWithRethrow(Block block, ILVariable handlerVariable, ILTransformContext context)
9161109
{
9171110
// Reuse AwaitInCatchTransform.MatchExceptionCaptureBlock through the block-tail shape:

0 commit comments

Comments
 (0)