@@ -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