diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs index 47314a1c..997117d5 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs @@ -540,6 +540,10 @@ protected override void EmitAwait(Expr.Await a) _il.BeginCatchBlock(typeof(Exception)); _il.Emit(OpCodes.Call, _ctx!.Runtime!.WrapException); _il.Emit(OpCodes.Stloc, _currentTryExceptionLocal); + // Record presence with the flag, not the value, so a rejected null/undefined still engages + // the catch rather than reading as "no exception" (#628). + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, _currentTryExceptionPresentLocal!); _il.Emit(OpCodes.Leave, _currentTryCleanupLabel); // skip the rest of the try body → catch/finally _il.EndExceptionBlock(); _il.Emit(OpCodes.Ldloc, resultTemp); // normal path: push the awaited value diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs index 50e75108..46cd19e6 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Statements.TryCatch.cs @@ -196,14 +196,24 @@ protected override void EmitContinue(Stmt.Continue c) } /// - /// A throw in a catch or finally body must run the enclosing finally(s) before the - /// exception propagates. A throw in a try body is captured by its sync-segment mini try/catch - /// (handled by the catch arm), so it is not routed here. + /// A throw in a catch or finally body propagates to the enclosing flag-based try (the one + /// whose body lexically contains this handler): it runs the finally(s) inside that try, then lands + /// in its catch — rather than a real IL throw that bypasses the flag-based catch (#632). With + /// no enclosing flag-based try it runs the active finally(s) and propagates out of MoveNextAsync. A + /// throw in a try body is captured by its sync-segment mini try/catch, not here. /// protected override void EmitThrow(Stmt.Throw t) { if (_inHandlerBody && _protectedRegionDepth == 0) { + if (_currentTryExceptionLocal != null) + { + EmitThrowIntoEnclosingTry(() => { EmitExpression(t.Value); EnsureBoxed(); }); + return; + } + + // No enclosing flag-based try (this handler belongs to the outermost try), but its own + // finally may still be active and must run before the throw leaves MoveNextAsync. var chain = ActiveFinallyFrames(); if (chain.Count > 0) { @@ -224,6 +234,49 @@ protected override void EmitThrow(Stmt.Throw t) base.EmitThrow(t); } + /// + /// Propagates a guest exception escaping a handler body into the enclosing flag-based try (tracked + /// by et al. while emitting a catch/finally body): stores + /// the value into that try's capture local and sets its present flag, then branches to its cleanup + /// entry so its catch runs. Any finally(s) strictly inside that try run first; because such a + /// finally can yield/await, the value is held in <>pendingException across them and + /// moved into the capture local by the routing terminal (#632, async analog). + /// + private void EmitThrowIntoEnclosingTry(Action loadValue) + { + var enclException = _currentTryExceptionLocal!; + var enclPresent = _currentTryExceptionPresentLocal!; + var enclCleanup = _currentTryCleanupLabel; + var chain = FinallyFramesInside(_currentTryScopeDepth); + if (chain.Count == 0) + { + // No intervening finally: store straight into the enclosing try and branch to its catch. + loadValue(); + _il.Emit(OpCodes.Stloc, enclException); + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, enclPresent); + _il.Emit(OpCodes.Br, enclCleanup); + return; + } + + // Intervening finally(s) may yield/await, so hold the value in a field across them; the routing + // terminal moves it into the enclosing try's capture local and branches to its catch. + _il.Emit(OpCodes.Ldarg_0); + loadValue(); + _il.Emit(OpCodes.Stfld, GetPendingExceptionField()); + int code = _nextExitCode++; + _exitTerminals[code] = () => + { + _il.Emit(OpCodes.Ldarg_0); + _il.Emit(OpCodes.Ldfld, GetPendingExceptionField()); + _il.Emit(OpCodes.Stloc, enclException); + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, enclPresent); + _il.Emit(OpCodes.Br, enclCleanup); + }; + RouteThroughFinallys(chain, code, OpCodes.Br); + } + // ---- Routing helpers ------------------------------------------------------------------------ /// All open finally scopes, innermost first. @@ -249,6 +302,21 @@ private List FinallyFramesAbove(LoopScope target) return result; } + /// + /// The finally scopes strictly inside the flag-based try whose body began at (= _exitScopes.Count at that point), innermost first. Excludes the + /// try's own finally (just below scopeDepth) and everything outside it — the finallys a throw + /// escaping a nested handler must run before reaching that try's catch (#632). + /// + private List FinallyFramesInside(int scopeDepth) + { + var result = new List(); + for (int i = _exitScopes.Count - 1; i >= scopeDepth; i--) + if (_exitScopes[i] is FinallyScope fs) + result.Add(fs); + return result; + } + /// /// Records 's per-frame routing across (each frame /// chains to the next, the outermost is terminal), then sets the pending-exit field and branches to @@ -440,12 +508,20 @@ private void StoreCaughtExceptionToParam(string name) private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) { var caughtExceptionLocal = _il.DeclareLocal(typeof(object)); + // Whether the try body raised an exception, tracked separately from caughtExceptionLocal's + // nullness: a thrown/rejected null/undefined captures as a null CLR reference, which a + // value-nullness gate misreads as "no exception" — skipping the catch and dropping the + // post-finally rethrow (#628, the async analog of #619). This flag records presence regardless + // of the captured value. + var exceptionPresentLocal = _il.DeclareLocal(typeof(bool)); var afterTryBodyLabel = _il.DefineLabel(); var afterTryCatchLabel = _il.DefineLabel(); // No exception captured yet. _il.Emit(OpCodes.Ldnull); _il.Emit(OpCodes.Stloc, caughtExceptionLocal); + _il.Emit(OpCodes.Ldc_I4_0); + _il.Emit(OpCodes.Stloc, exceptionPresentLocal); // Two finally-running mechanisms share `afterTryBodyLabel` as the cleanup entry: // 1. External `generator.return()`: a suspended yield observes `__returnRequested` on resume @@ -469,14 +545,22 @@ private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) _inHandlerBody = false; // Carry this try's capture target down to the suspension points emitted at the top level, so a // rejected `await` inside the try routes its exception into the same flag + catch/finally - // instead of escaping MoveNextAsync (#617). Saved/restored for correct nesting. + // instead of escaping MoveNextAsync (#617), setting the present flag so a null rejection still + // engages the catch (#628). After the body these revert to the enclosing try, which a throw + // escaping this try's catch/finally must route to (#632). Saved/restored for correct nesting. var previousTryExceptionLocal = _currentTryExceptionLocal; + var previousTryExceptionPresentLocal = _currentTryExceptionPresentLocal; var previousTryCleanupLabel = _currentTryCleanupLabel; + var previousTryScopeDepth = _currentTryScopeDepth; _currentTryExceptionLocal = caughtExceptionLocal; + _currentTryExceptionPresentLocal = exceptionPresentLocal; _currentTryCleanupLabel = afterTryBodyLabel; - EmitTryBodyWithSuspensions(t.TryBlock, caughtExceptionLocal, afterTryBodyLabel); + _currentTryScopeDepth = _exitScopes.Count; + EmitTryBodyWithSuspensions(t.TryBlock, caughtExceptionLocal, exceptionPresentLocal, afterTryBodyLabel); _currentTryExceptionLocal = previousTryExceptionLocal; + _currentTryExceptionPresentLocal = previousTryExceptionPresentLocal; _currentTryCleanupLabel = previousTryCleanupLabel; + _currentTryScopeDepth = previousTryScopeDepth; _inHandlerBody = previousInHandler; // Restore the enclosing try's cleanup label (its yields must route to it, not ours). @@ -488,8 +572,10 @@ private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) // a non-local exit (including a throw) from the catch body runs this finally too. if (t.CatchBlock != null) { + // Gate on the present flag, not the value's nullness, so a caught null/undefined enters the + // catch (#628). The value local stays authoritative for the binding below. var skipCatchLabel = _il.DefineLabel(); - _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); + _il.Emit(OpCodes.Ldloc, exceptionPresentLocal); _il.Emit(OpCodes.Brfalse, skipCatchLabel); // Bind the captured value to the catch param, honouring a hoisted field when the param is @@ -502,10 +588,11 @@ private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) StoreCaughtExceptionToParam(t.CatchParam.Lexeme); } - // Catch handles it; clear the flag so the post-finally rethrow below is skipped — and so a - // routed exit re-entering afterTryBody skips the catch rather than re-running it. - _il.Emit(OpCodes.Ldnull); - _il.Emit(OpCodes.Stloc, caughtExceptionLocal); + // Catch handles it; clear the present flag so the post-finally rethrow below is skipped — + // and so a routed exit re-entering afterTryBody skips the catch rather than re-running it. + // The flag (not the value) is the gate now, so clearing it is what matters (#628). + _il.Emit(OpCodes.Ldc_I4_0); + _il.Emit(OpCodes.Stloc, exceptionPresentLocal); _inHandlerBody = true; foreach (var stmt in t.CatchBlock) @@ -545,20 +632,31 @@ private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) // In-body non-local exit (mechanism 2): dispatch any pending exit that routed through here. EmitFinallyDispatch(frame!); - // Rethrow an uncaught exception once the finally has run (try/finally with no catch). + // Propagate an uncaught exception once the finally has run (try/finally with no catch). if (t.CatchBlock == null) { + // Gate on the present flag so a thrown/rejected null/undefined still propagates (#628). var noExceptionLabel = _il.DefineLabel(); - _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); + _il.Emit(OpCodes.Ldloc, exceptionPresentLocal); _il.Emit(OpCodes.Brfalse, noExceptionLabel); - // Mark the generator done before the exception leaves MoveNextAsync. - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldc_I4, -2); - _il.Emit(OpCodes.Stfld, _builder.StateField); - _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); - _il.Emit(OpCodes.Call, _ctx!.Runtime!.CreateException); - _il.Emit(OpCodes.Throw); + if (_currentTryExceptionLocal != null) + { + // The finally has run; the still-uncaught exception now propagates to the enclosing + // flag-based try's catch (not out of MoveNextAsync), so an outer catch still handles + // it — the try/finally analog of the handler-body throw routing (#632). + EmitThrowIntoEnclosingTry(() => _il.Emit(OpCodes.Ldloc, caughtExceptionLocal)); + } + else + { + // No enclosing flag-based try: mark done and propagate out of MoveNextAsync. + _il.Emit(OpCodes.Ldarg_0); + _il.Emit(OpCodes.Ldc_I4, -2); + _il.Emit(OpCodes.Stfld, _builder.StateField); + _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); + _il.Emit(OpCodes.Call, _ctx!.Runtime!.CreateException); + _il.Emit(OpCodes.Throw); + } _il.MarkLabel(noExceptionLabel); } @@ -571,7 +669,7 @@ private void EmitTryCatchWithSuspensions(Stmt.TryCatch t) /// Walks the try body, wrapping runs of plain statements in mini IL try/catch blocks while /// emitting suspension points and non-local exits (return/break/continue) at the top level. /// - private void EmitTryBodyWithSuspensions(List tryBody, LocalBuilder caughtExceptionLocal, Label afterTryLabel) + private void EmitTryBodyWithSuspensions(List tryBody, LocalBuilder caughtExceptionLocal, LocalBuilder exceptionPresentLocal, Label afterTryLabel) { List syncSegment = []; @@ -582,12 +680,13 @@ private void EmitTryBodyWithSuspensions(List tryBody, LocalBuilder caughtE // Flush the accumulated plain statements first. if (syncSegment.Count > 0) { - EmitSyncSegmentInTry(syncSegment, caughtExceptionLocal); + EmitSyncSegmentInTry(syncSegment, caughtExceptionLocal, exceptionPresentLocal); syncSegment.Clear(); } // If an earlier segment threw, skip the suspension/exit and head to catch/finally. - _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); + // Gate on the present flag so a thrown null/undefined still short-circuits here (#628). + _il.Emit(OpCodes.Ldloc, exceptionPresentLocal); _il.Emit(OpCodes.Brtrue, afterTryLabel); // Emitted at the top level: a yield/await's `ret`/resume label and a return's `ret` or a @@ -601,18 +700,19 @@ private void EmitTryBodyWithSuspensions(List tryBody, LocalBuilder caughtE } if (syncSegment.Count > 0) - EmitSyncSegmentInTry(syncSegment, caughtExceptionLocal); + EmitSyncSegmentInTry(syncSegment, caughtExceptionLocal, exceptionPresentLocal); } /// /// Emits a run of plain (non-suspending, non-exiting) statements inside a real IL try/catch /// that records any thrown exception into . /// - private void EmitSyncSegmentInTry(List statements, LocalBuilder caughtExceptionLocal) + private void EmitSyncSegmentInTry(List statements, LocalBuilder caughtExceptionLocal, LocalBuilder exceptionPresentLocal) { - // An earlier segment may already have thrown — don't run this one. + // An earlier segment may already have thrown — don't run this one. Gate on the present flag so + // a prior thrown null/undefined still suppresses this segment (#628). var skipLabel = _il.DefineLabel(); - _il.Emit(OpCodes.Ldloc, caughtExceptionLocal); + _il.Emit(OpCodes.Ldloc, exceptionPresentLocal); _il.Emit(OpCodes.Brtrue, skipLabel); // A real IL protected region is open across the segment body (see _protectedRegionDepth). @@ -624,6 +724,10 @@ private void EmitSyncSegmentInTry(List statements, LocalBuilder caughtExce _il.BeginCatchBlock(typeof(Exception)); _il.Emit(OpCodes.Call, _ctx!.Runtime!.WrapException); _il.Emit(OpCodes.Stloc, caughtExceptionLocal); + // Record presence with the flag, not the value: a caught null/undefined would otherwise read + // as "no exception" at the gates above (#628). + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, exceptionPresentLocal); _il.EndExceptionBlock(); _protectedRegionDepth--; diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.cs b/Compilation/AsyncGeneratorMoveNextEmitter.cs index ab78e57b..07cd15a0 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.cs @@ -44,12 +44,23 @@ public partial class AsyncGeneratorMoveNextEmitter : StatementEmitterBase // While emitting a flag-based try BODY (EmitTryCatchWithSuspensions), these carry that try's // exception-capture target down to suspension points, which are emitted at the top level — // outside the sync segments' mini try/catch. A rejected `await` inside the try captures its - // exception into _currentTryExceptionLocal (exactly as a sync segment does) and `Leave`s to - // _currentTryCleanupLabel so the try's catch/finally run, instead of escaping MoveNextAsync - // unhandled (#617). Null when not inside such a try body (the common case → GetResult is plain). + // exception into _currentTryExceptionLocal (exactly as a sync segment does), sets the present + // flag, and `Leave`s to _currentTryCleanupLabel so the try's catch/finally run, instead of + // escaping MoveNextAsync unhandled (#617). The present flag (not the value's nullness) gates the + // catch so a rejected `Promise.reject(null)`/`throw null` still engages it (#628, the async + // analog of #619). Saved/restored around the body, so during a catch/finally body these instead + // identify the *enclosing* flag-based try — the one whose catch must handle a throw escaping that + // handler (#632). Null when not inside such a try body (the common case → GetResult is plain). private LocalBuilder? _currentTryExceptionLocal; + private LocalBuilder? _currentTryExceptionPresentLocal; private Label _currentTryCleanupLabel; + // `_exitScopes.Count` captured at the start of the current flag-based try's body emission (after + // its own finally scope, if any, was pushed). Finally scopes at indices >= this are strictly + // *inside* that try; a throw escaping a nested handler runs exactly those before reaching this + // try's catch (#632). Meaningful only while _currentTryExceptionLocal is non-null. + private int _currentTryScopeDepth; + // Compilation context for access to functions, classes, etc. private CompilationContext? _ctx; diff --git a/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs b/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs index 46448c0e..c128822a 100644 --- a/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs +++ b/Compilation/GeneratorMoveNextEmitter.Statements.TryCatch.cs @@ -70,14 +70,16 @@ private sealed class FinallyScope : IExitScope private bool _inHandlerBody; // The innermost flag-based try whose *body* is currently being emitted: its catch/finally entry - // (afterTryBodyLabel), the local capturing a try-body exception, and the boolean flag recording - // whether one was captured. An external throw() injected at a yield in this body behaves as if the - // body threw there — it stores the error into the local, sets the flag, and branches to the cleanup - // so the catch/finally run (#526). The flag (not the value's nullness) gates the catch so an - // injected throw(null)/throw(undefined) still engages it (#619). Saved/restored around the try-body - // emission, so it is null (or the enclosing try's) while emitting a catch/finally body, where - // `_inHandlerBody` instead routes an injected throw through the enclosing finally(s). - private (Label AfterTryBody, LocalBuilder CaughtException, LocalBuilder ExceptionPresent)? _tryBodyContext; + // (afterTryBodyLabel), the local capturing a try-body exception, the boolean flag recording whether + // one was captured, and `_exitScopes.Count` at the start of its body (ScopeDepth — finally scopes at + // indices >= it are strictly inside this try). An external throw() injected at a yield in this body + // behaves as if the body threw there — it stores the error into the local, sets the flag, and + // branches to the cleanup so the catch/finally run (#526). The flag (not the value's nullness) gates + // the catch so an injected throw(null)/throw(undefined) still engages it (#619). Saved/restored + // around the try-body emission, so while emitting a catch/finally body it instead identifies the + // *enclosing* flag-based try (or is null) — the one whose catch must handle a throw escaping that + // handler, after the finally(s) inside it have run (#632). + private (Label AfterTryBody, LocalBuilder CaughtException, LocalBuilder ExceptionPresent, int ScopeDepth)? _tryBodyContext; // `<>pendingExit` (int): the code of an in-flight non-local exit, or 0 when none. A finally that // yields suspends MoveNext mid-routing, so this must be a field (a local would reset on re-entry). @@ -221,14 +223,24 @@ protected override void EmitContinue(Stmt.Continue c) } /// - /// A throw in a catch or finally body must run the enclosing finally(s) before the - /// exception propagates. A throw in a try body is captured by its sync-segment mini try/catch - /// (handled by the catch arm), so it is not routed here. + /// A throw in a catch or finally body propagates to the enclosing flag-based try (the one + /// whose body lexically contains this handler): it runs the finally(s) inside that try, then lands + /// in its catch — rather than a real IL throw that bypasses the flag-based catch (#632). With + /// no enclosing flag-based try it runs the active finally(s) and propagates out of MoveNext. A throw + /// in a try body is captured by its sync-segment mini try/catch (handled by the catch arm), not here. /// protected override void EmitThrow(Stmt.Throw t) { if (_inHandlerBody && _protectedRegionDepth == 0) { + if (_tryBodyContext is { } encl) + { + EmitThrowIntoEnclosingTry(encl, () => { EmitExpression(t.Value); EnsureBoxed(); }); + return; + } + + // No enclosing flag-based try (this handler belongs to the outermost try), but its own + // finally may still be active and must run before the throw leaves MoveNext. var chain = ActiveFinallyFrames(); if (chain.Count > 0) { @@ -249,6 +261,47 @@ protected override void EmitThrow(Stmt.Throw t) base.EmitThrow(t); } + /// + /// Propagates a guest exception escaping a handler body into the enclosing flag-based try + /// : stores the value into that try's capture local and sets its present + /// flag, then branches to its cleanup entry so its catch runs (or, catch-less, its finally then its + /// own propagation). Any finally(s) strictly inside that try run first; because such a finally can + /// yield, the value is held in <>pendingException across them and moved into the capture + /// local by the routing terminal. This is the catch-side analog of the finally routing already used + /// for a routed return/throw (#632). pushes the boxed guest value. + /// + private void EmitThrowIntoEnclosingTry((Label AfterTryBody, LocalBuilder CaughtException, LocalBuilder ExceptionPresent, int ScopeDepth) encl, Action loadValue) + { + var chain = FinallyFramesInside(encl.ScopeDepth); + if (chain.Count == 0) + { + // No intervening finally: store straight into the enclosing try and branch to its catch. + loadValue(); + _il.Emit(OpCodes.Stloc, encl.CaughtException); + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, encl.ExceptionPresent); + _il.Emit(OpCodes.Br, encl.AfterTryBody); + return; + } + + // Intervening finally(s) may yield, so hold the value in a field across them; the routing + // terminal moves it into the enclosing try's capture local and branches to its catch. + _il.Emit(OpCodes.Ldarg_0); + loadValue(); + _il.Emit(OpCodes.Stfld, GetPendingExceptionField()); + int code = _nextExitCode++; + _exitTerminals[code] = () => + { + _il.Emit(OpCodes.Ldarg_0); + _il.Emit(OpCodes.Ldfld, GetPendingExceptionField()); + _il.Emit(OpCodes.Stloc, encl.CaughtException); + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, encl.ExceptionPresent); + _il.Emit(OpCodes.Br, encl.AfterTryBody); + }; + RouteThroughFinallys(chain, code, OpCodes.Br); + } + // ---- Routing helpers ------------------------------------------------------------------------ /// All open finally scopes, innermost first. @@ -274,6 +327,21 @@ private List FinallyFramesAbove(LoopScope target) return result; } + /// + /// The finally scopes strictly inside the flag-based try whose body began at (= _exitScopes.Count at that point), innermost first. Excludes the + /// try's own finally (which lives just below scopeDepth) and everything outside it. These are the + /// finallys a throw escaping a nested handler must run before reaching that try's catch (#632). + /// + private List FinallyFramesInside(int scopeDepth) + { + var result = new List(); + for (int i = _exitScopes.Count - 1; i >= scopeDepth; i--) + if (_exitScopes[i] is FinallyScope fs) + result.Add(fs); + return result; + } + /// /// Records 's per-frame routing across (each frame /// chains to the next, the outermost is terminal), then sets the pending-exit field and branches to @@ -454,32 +522,41 @@ private void EmitRoutedReturn(Action loadValue) } /// - /// Emits a throw <value> at a top-level resume point. Inside a try body it behaves - /// as if the body threw there (store into the try's caught-exception local and branch to its - /// cleanup, so the catch/finally run); inside a catch/finally body it routes through the - /// enclosing finally(s); with no enclosing try it propagates out of MoveNext. pushes the boxed guest error value. + /// Emits a throw <value> at a top-level resume point (an external throw() + /// injected at a suspended yield). Inside a try body it behaves as if the body threw there (store + /// into the try's caught-exception local and branch to its cleanup, so the catch/finally run); + /// inside a catch/finally body it propagates to the enclosing flag-based try's catch the same way a + /// lexical handler-body throw does (#632); with no enclosing try it runs the active finally(s) and + /// propagates out of MoveNext. pushes the boxed guest error value. /// private void EmitRoutedThrow(Action loadValue) { - if (!_inHandlerBody && _tryBodyContext is { } tryBody) + if (_tryBodyContext is { } ctx) { - // In a try body: capture exactly like a try-body exception so the catch/finally at - // afterTryBodyLabel handle it. A catch-less yielding finally persists this local to a - // field before suspending, so it survives (#599). Set the present flag (not the value's - // nullness) so an injected throw(null)/throw(undefined) still engages the catch (#619). - loadValue(); - _il.Emit(OpCodes.Stloc, tryBody.CaughtException); - _il.Emit(OpCodes.Ldc_I4_1); - _il.Emit(OpCodes.Stloc, tryBody.ExceptionPresent); - _il.Emit(OpCodes.Br, tryBody.AfterTryBody); + if (!_inHandlerBody) + { + // In a try body: capture exactly like a try-body exception so the catch/finally at + // afterTryBodyLabel handle it. A catch-less yielding finally persists this local to a + // field before suspending, so it survives (#599). Set the present flag (not the value's + // nullness) so an injected throw(null)/throw(undefined) still engages the catch (#619). + loadValue(); + _il.Emit(OpCodes.Stloc, ctx.CaughtException); + _il.Emit(OpCodes.Ldc_I4_1); + _il.Emit(OpCodes.Stloc, ctx.ExceptionPresent); + _il.Emit(OpCodes.Br, ctx.AfterTryBody); + return; + } + + // In a catch/finally body: run the finally(s) inside the enclosing try, then land in its + // catch — the injection-path analog of the lexical handler-body throw fix (#632). + EmitThrowIntoEnclosingTry(ctx, loadValue); return; } var chain = ActiveFinallyFrames(); if (chain.Count > 0) { - // In a catch/finally body: run the enclosing finally(s), then rethrow at the terminal. + // Outermost try's catch/finally body: run its own finally(s), then rethrow at the terminal. _il.Emit(OpCodes.Ldarg_0); loadValue(); _il.Emit(OpCodes.Stfld, GetPendingExceptionField()); @@ -669,7 +746,7 @@ void EmitLoadExceptionPresent() bool previousInHandler = _inHandlerBody; var previousTryBody = _tryBodyContext; _inHandlerBody = false; - _tryBodyContext = (afterTryBodyLabel, caughtExceptionLocal, exceptionPresentLocal); + _tryBodyContext = (afterTryBodyLabel, caughtExceptionLocal, exceptionPresentLocal, _exitScopes.Count); EmitTryBodyWithYields(t.TryBlock, caughtExceptionLocal, exceptionPresentLocal, afterTryBodyLabel); _tryBodyContext = previousTryBody; _inHandlerBody = previousInHandler; @@ -737,20 +814,30 @@ void EmitLoadExceptionPresent() EmitFinallyDispatch(frame!); } - // Rethrow an uncaught exception once the finally has run (try/finally with no catch). + // Propagate an uncaught exception once the finally has run (try/finally with no catch). if (t.CatchBlock == null) { var noExceptionLabel = _il.DefineLabel(); EmitLoadExceptionPresent(); _il.Emit(OpCodes.Brfalse, noExceptionLabel); - // Mark the generator done before the exception leaves MoveNext. - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldc_I4, -2); - _il.Emit(OpCodes.Stfld, _builder.StateField); - EmitLoadCaughtException(); - _il.Emit(OpCodes.Call, _ctx!.Runtime!.CreateException); - _il.Emit(OpCodes.Throw); + if (_tryBodyContext is { } encl) + { + // The finally has run; the still-uncaught exception now propagates to the enclosing + // flag-based try's catch (not out of MoveNext), so an outer catch still handles it — the + // try/finally analog of the handler-body throw routing (#632). + EmitThrowIntoEnclosingTry(encl, EmitLoadCaughtException); + } + else + { + // No enclosing flag-based try: mark the generator done and propagate out of MoveNext. + _il.Emit(OpCodes.Ldarg_0); + _il.Emit(OpCodes.Ldc_I4, -2); + _il.Emit(OpCodes.Stfld, _builder.StateField); + EmitLoadCaughtException(); + _il.Emit(OpCodes.Call, _ctx!.Runtime!.CreateException); + _il.Emit(OpCodes.Throw); + } _il.MarkLabel(noExceptionLabel); } diff --git a/SharpTS.Tests/SharedTests/AsyncGeneratorTryFinallyTests.cs b/SharpTS.Tests/SharedTests/AsyncGeneratorTryFinallyTests.cs index 61a8e460..0d2e5162 100644 --- a/SharpTS.Tests/SharedTests/AsyncGeneratorTryFinallyTests.cs +++ b/SharpTS.Tests/SharedTests/AsyncGeneratorTryFinallyTests.cs @@ -527,6 +527,138 @@ public void CatchParamAfterAwaitInCatchBody_PreservesValue(ExecutionMode mode) Assert.Equal("v1\ncaught boom\nv9\n", TestHarness.Run(source, mode)); } + // ---- #628: throwing/rejecting null/undefined into a flag-based try/catch must engage the catch ---- + // The async analog of #619: the flag-based scheme inferred "was an exception thrown?" from the + // captured value's nullness, so a thrown/rejected null/undefined (a null CLR reference) read as "no + // exception" — skipping the catch. A dedicated present flag now records presence independent of the + // value, set by both the sync-segment capture and the rejected-await routing (#617). CompiledOnly: + // the interpreter's eager-drain ordering (#564) is a separate concern. + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void ThrowNullIntoFlagBasedTryCatch_IsCaught(ExecutionMode mode) + { + // The exact #628 repro: throw null after a yield, caught in the same try's catch. + var source = """ + async function* g() { try { yield 1; throw null; } catch (e) { console.log("caught isNull=" + (e === null)); } } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("v1\ncaught isNull=true\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void ThrowUndefinedIntoFlagBasedTryCatch_IsCaught(ExecutionMode mode) + { + // throw undefined likewise reaches the catch (was skipped). Asserted via `=== undefined`. + var source = """ + async function* g() { try { yield 1; throw undefined; } catch (e) { console.log("isUndef=" + (e === undefined)); } } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("v1\nisUndef=true\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void RejectedNullAwaitInTry_IsCaughtWithNullBinding(ExecutionMode mode) + { + // A rejected await whose reason is null must reach the catch (the await-routing present flag, + // #617 + #628), and the catch param must bind the null reason. + var source = """ + async function* g() { try { await Promise.reject(null); } catch (e) { console.log("isNull=" + (e === null)); } yield 1; } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("isNull=true\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void FalsyNonNullThrowIntoFlagBasedTryCatch_StillCaught(ExecutionMode mode) + { + // Boundary guard: a falsy-but-non-null thrown value (0) boxes to a non-null reference and was + // already caught; it must remain so under the present-flag scheme. + var source = """ + async function* g() { try { yield 1; throw 0; } catch (e) { console.log("caught e=" + e); } } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("v1\ncaught e=0\n", TestHarness.Run(source, mode)); + } + + // ---- #632: a throw escaping a handler body must reach an enclosing flag-based try's catch ---- + // Async analog of the plain-generator #632: a handler-body throw is routed into the enclosing + // flag-based try's capture local (running the finally(s) inside that try first) and branched to its + // cleanup, instead of a real IL throw that bypasses the flag-based catch. + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void RethrowFromCatch_CaughtByEnclosingTryCatch(ExecutionMode mode) + { + // The exact async #632 repro: a rejected await is caught by the inner catch, which rethrows; the + // outer catch must catch the rethrown value. + var source = """ + async function* g() { + try { + try { await Promise.reject("inner"); } catch (e: any) { console.log("inner caught " + e); throw "rethrown"; } + } catch (e: any) { console.log("outer caught " + e); } + yield 1; + } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("inner caught inner\nouter caught rethrown\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void RethrowFromCatch_WithInterveningFinally_RunsFinallyThenOuterCatch(ExecutionMode mode) + { + // The inner try has a finally: a throw escaping the inner catch runs that finally before the + // outer catch sees the value. + var source = """ + async function* g() { + try { + try { yield 0; throw "inner"; } + catch (e: any) { console.log("C1 " + e); throw "rethrown"; } + finally { console.log("F1"); } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("v0\nC1 inner\nF1\nouter rethrown\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void UncaughtThrowFromCatchlessTryFinally_CaughtByEnclosingTry(ExecutionMode mode) + { + // An uncaught exception leaving a catch-less inner try/finally must, after its finally runs, + // propagate to the enclosing flag-based catch rather than escape the state machine. + var source = """ + async function* g() { + try { + try { yield 0; throw "x"; } finally { console.log("F"); } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + async function main() { for await (const v of g()) console.log("v" + v); } + main(); + """; + + Assert.Equal("v0\nF\nouter x\nv1\n", TestHarness.Run(source, mode)); + } + // ---- IL-verification guards (the heart of #559: emitted IL must verify) ---- [Theory] @@ -563,6 +695,13 @@ public void CatchParamAfterAwaitInCatchBody_PreservesValue(ExecutionMode mode) // #569: catch parameter read after a suspension (yield / await) in the catch body — hoisted-field binding. [InlineData("async function* g() { try { yield 1; throw 'boom'; } catch (e) { yield 0; console.log(e); } } async function main(){ for await (const v of g()) {} } main();")] [InlineData("async function* g() { try { yield 1; throw 'boom'; } catch (e) { await Promise.resolve(0); console.log(e); } } async function main(){ for await (const v of g()) {} } main();")] + // #628: thrown/rejected null/undefined into a flag-based try — the present-flag gates must verify. + [InlineData("async function* g() { try { yield 1; throw null; } catch (e) { console.log(e); } } async function main(){ for await (const v of g()) {} } main();")] + [InlineData("async function* g() { try { await Promise.reject(null); } catch (e) { console.log(e); } yield 1; } async function main(){ for await (const v of g()) {} } main();")] + // #632: a throw/rethrow escaping a handler routed into an enclosing flag-based try's catch. + [InlineData("async function* g() { try { try { yield 0; throw 'a'; } catch (e) { throw 'b'; } } catch (e) { console.log(e); } yield 1; } async function main(){ for await (const v of g()) {} } main();")] + [InlineData("async function* g() { try { try { yield 0; throw 'a'; } catch (e) { throw 'b'; } finally { console.log('f'); } } catch (e) { console.log(e); } } async function main(){ for await (const v of g()) {} } main();")] + [InlineData("async function* g() { try { try { yield 0; throw 'x'; } finally { yield 7; } } catch (e) { console.log(e); } } async function main(){ for await (const v of g()) {} } main();")] public void AsyncGeneratorTryFinallyWithSuspension_EmitsVerifiableIL(string source) { var errors = TestHarness.CompileAndVerifyOnly(source); diff --git a/SharpTS.Tests/SharedTests/GeneratorErrorIdentityTests.cs b/SharpTS.Tests/SharedTests/GeneratorErrorIdentityTests.cs new file mode 100644 index 00000000..85c0bf19 --- /dev/null +++ b/SharpTS.Tests/SharedTests/GeneratorErrorIdentityTests.cs @@ -0,0 +1,74 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +/// +/// Regression tests for #543: an error caught by a try/catch inside a compiled +/// generator body must satisfy instanceof, not merely carry the right .name/.message. +/// The catch binding in the generator's MoveNext previously left the caught value as something +/// whose prototype chain failed the instanceof check while the structural fields still read +/// correctly. The compiled path now produces a real error object in both the immediate and the +/// flag-based (post-yield) try shapes. +/// +/// +/// The runtime "not a function" cases are CompiledOnly: the interpreter has a separate, broader gap — +/// a property access / call on undefined throws a raw host "Runtime Error" string rather than a +/// guest TypeError, even at top level outside any generator — tracked in #676. Explicitly thrown +/// errors carry correct identity in both modes and are asserted across both. +/// +/// +public class GeneratorErrorIdentityTests +{ + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void RuntimeTypeError_CaughtInGeneratorBody_InstanceofHolds(ExecutionMode mode) + { + // The exact #543 repro: a "not a function" TypeError caught inside the generator body. + var source = """ + function* g() { + const o: any = undefined; + try { o.foo(); } catch (e: any) { console.log((e instanceof TypeError) + " " + e.name); } + yield 1; + } + g().next(); + """; + + Assert.Equal("true TypeError\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.CompiledOnly), MemberType = typeof(ExecutionModes))] + public void RuntimeTypeError_CaughtInGeneratorBody_AfterYield_InstanceofHolds(ExecutionMode mode) + { + // The flag-based shape: a yield before the throwing call forces the try into the flag-based + // scheme, whose catch binding must also produce a real TypeError. + var source = """ + function* g() { + const o: any = undefined; + try { yield 0; o.foo(); } catch (e: any) { console.log((e instanceof TypeError) + " " + e.name); } + yield 1; + } + const it = g(); it.next(); it.next(); + """; + + Assert.Equal("true TypeError\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ExplicitlyThrownError_CaughtInGeneratorBody_InstanceofHolds(ExecutionMode mode) + { + // Explicit `throw new RangeError(...)` caught in-body keeps its identity in both modes. + var source = """ + function* g() { + try { yield 0; throw new RangeError("rr"); } + catch (e: any) { console.log((e instanceof RangeError) + " " + (e instanceof Error) + " " + e.name + " " + e.message); } + yield 1; + } + const it = g(); it.next(); it.next(); + """; + + Assert.Equal("true true RangeError rr\n", TestHarness.Run(source, mode)); + } +} diff --git a/SharpTS.Tests/SharedTests/GeneratorTests.cs b/SharpTS.Tests/SharedTests/GeneratorTests.cs index 3cbc8851..42566f94 100644 --- a/SharpTS.Tests/SharedTests/GeneratorTests.cs +++ b/SharpTS.Tests/SharedTests/GeneratorTests.cs @@ -1325,9 +1325,9 @@ function make() { // * Compiled tests resolve the receiver through a live object property (`h.it`), because // compiled generators capture closure variables by value (#541) — a self-assigned `it` is // snapshotted as `undefined`, so the `let it; it = g()` form never reaches the guard. - // * `instanceof TypeError` is asserted only when the error is caught OUTSIDE the generator; a - // separate pre-existing bug (#543) makes `instanceof` report false for an error caught - // inside a compiled generator body (its `.name`/`.message` are still correct). + // * `instanceof TypeError` now holds for an error caught inside a compiled generator body too + // (#543, fixed): the compiled re-entrancy test below asserts it, as does + // GeneratorErrorIdentityTests for the runtime "not a function" TypeError. // The yield*-delegation case doesn't rely on the captured self-reference (the inner generator is // created after the outer is assigned), so it runs in both modes from a single source. @@ -1468,12 +1468,13 @@ public void GeneratorExpression_ReentrantNext_ThrowsTypeError(ExecutionMode mode public void Generator_ReentrantNext_Compiled_ThrowsTypeErrorThenResumes(ExecutionMode mode) { // The re-entrant next() throws a catchable TypeError; once caught, the generator is still - // suspended-able and resumes normally (the guard must not corrupt its running state). + // suspended-able and resumes normally (the guard must not corrupt its running state). The + // in-body catch also satisfies `instanceof TypeError` (#543, fixed). var source = """ const h: any = {}; function* g() { try { h.it.next(); } - catch (e: any) { console.log(e.name, e.message); } + catch (e: any) { console.log(e instanceof TypeError, e.name, e.message); } yield 1; } h.it = g(); @@ -1482,7 +1483,7 @@ public void Generator_ReentrantNext_Compiled_ThrowsTypeErrorThenResumes(Executio """; var output = TestHarness.Run(source, mode); - Assert.Equal("TypeError Generator is already running\n1 false\n", output); + Assert.Equal("true TypeError Generator is already running\n1 false\n", output); } [Theory] diff --git a/SharpTS.Tests/SharedTests/GeneratorTryFinallyTests.cs b/SharpTS.Tests/SharedTests/GeneratorTryFinallyTests.cs index cb307379..7a625099 100644 --- a/SharpTS.Tests/SharedTests/GeneratorTryFinallyTests.cs +++ b/SharpTS.Tests/SharedTests/GeneratorTryFinallyTests.cs @@ -863,6 +863,130 @@ public void BareThrowOnSuspendedGenerator_InjectsUndefined(ExecutionMode mode) Assert.Equal("c:undefined\n", TestHarness.Run(source, mode)); } + // ---- #632: a throw escaping a handler body must reach an enclosing flag-based try's catch ---- + // EmitThrow routed a handler-body throw only through enclosing *finally* frames, then did a real IL + // throw — which bypasses an outer flag-based try's caughtException flag, so its catch never ran. The + // throw is now routed into the enclosing try's capture local (running the finally(s) inside that try + // first) and branched to its cleanup, the catch-side analog of the existing finally routing. + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void RethrowFromCatch_CaughtByEnclosingTryCatch(ExecutionMode mode) + { + // The exact #632 repro: an inner catch rethrows; the outer catch must catch the rethrown value. + var source = """ + function* g() { + try { + try { yield 0; throw "inner"; } catch (e: any) { console.log("inner caught " + e); throw "rethrown"; } + } catch (e: any) { console.log("outer caught " + e); } + yield 1; + } + for (const v of g()) console.log("v" + v); + """; + + Assert.Equal("v0\ninner caught inner\nouter caught rethrown\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ThrowFromCatch_WithInterveningFinally_RunsFinallyThenOuterCatch(ExecutionMode mode) + { + // The inner try has a finally: a throw escaping the inner catch must run that finally before the + // outer catch sees the value (the finally is routed via FinallyFramesInside). + var source = """ + function* g() { + try { + try { yield 0; throw "inner"; } + catch (e: any) { console.log("C1 " + e); throw "rethrown"; } + finally { console.log("F1"); } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + for (const v of g()) console.log("v" + v); + """; + + Assert.Equal("v0\nC1 inner\nF1\nouter rethrown\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void UncaughtThrowFromCatchlessTryFinally_CaughtByEnclosingTry(ExecutionMode mode) + { + // An uncaught exception leaving a catch-less inner try/finally must, after its finally runs, + // propagate to the enclosing flag-based catch rather than escape the state machine (the + // try/finally analog: the post-finally rethrow now routes to the enclosing try too). + var source = """ + function* g() { + try { + try { yield 0; throw "x"; } finally { console.log("F"); } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + for (const v of g()) console.log("v" + v); + """; + + Assert.Equal("v0\nF\nouter x\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ThrowFromFinallyBody_CaughtByEnclosingTry(ExecutionMode mode) + { + // A throw from a finally body propagates to the enclosing try's catch the same way. + var source = """ + function* g() { + try { + try { yield 0; } finally { console.log("F1"); throw "fromFinally"; } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + for (const v of g()) console.log("v" + v); + """; + + Assert.Equal("v0\nF1\nouter fromFinally\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void RethrowAcrossTwoNestedFinallys_RunsBothBeforeEnclosingCatch(ExecutionMode mode) + { + // Two finallys lie between the rethrow and the enclosing catch; both run, innermost first. + var source = """ + function* g() { + try { + try { + try { yield 0; throw "deep"; } finally { console.log("Finner"); } + } catch (e: any) { console.log("Cmid " + e); throw "remid"; } finally { console.log("Fmid"); } + } catch (e: any) { console.log("outer " + e); } + yield 1; + } + for (const v of g()) console.log("v" + v); + """; + + Assert.Equal("v0\nFinner\nCmid deep\nFmid\nouter remid\nv1\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ExternalThrowAtCatchBodyYield_CaughtByEnclosingTry(ExecutionMode mode) + { + // An external it.throw() injected at a yield *inside a catch body* must also propagate to the + // enclosing try's catch (the injection-path analog of the lexical handler-body throw fix). + var source = """ + function* g() { + try { + try { throw "a"; } catch (e: any) { yield 1; console.log("unreached"); } + } catch (e: any) { console.log("outer " + e); } + yield 99; + } + const it = g(); + const r1 = it.next(); console.log(r1.value, r1.done); + const r2 = it.throw("b"); console.log(r2.value, r2.done); + """; + + Assert.Equal("1 false\nouter b\n99 false\n", TestHarness.Run(source, mode)); + } + // ---- IL-verification guards (the heart of #477: emitted IL must verify) ---- [Theory] @@ -918,6 +1042,12 @@ public void BareThrowOnSuspendedGenerator_InjectsUndefined(ExecutionMode mode) [InlineData("function* g() { try { yield 1; throw null; } catch (e) { console.log(e); } } for (const v of g()) {}")] [InlineData("function* g() { try { yield 1; throw undefined; } catch (e) { console.log(e); } } for (const v of g()) {}")] [InlineData("function* g() { try { yield 1; throw null; } finally { yield 2; } } try { for (const v of g()) {} } catch (e) {}")] + // #632: a throw/rethrow escaping a handler routed into an enclosing flag-based try's catch — the new + // routing (with intervening finallys, possibly yielding) must verify. + [InlineData("function* g() { try { try { yield 0; throw 'a'; } catch (e) { throw 'b'; } } catch (e) { console.log(e); } yield 1; } for (const v of g()) {}")] + [InlineData("function* g() { try { try { yield 0; throw 'a'; } catch (e) { throw 'b'; } finally { console.log('f'); } } catch (e) { console.log(e); } } for (const v of g()) {}")] + [InlineData("function* g() { try { try { yield 0; throw 'x'; } finally { yield 7; } } catch (e) { console.log(e); } } for (const v of g()) {}")] + [InlineData("function* g() { try { try { yield 0; } finally { throw 'fin'; } } catch (e) { console.log(e); } } for (const v of g()) {}")] public void GeneratorTryFinallyWithYield_EmitsVerifiableIL(string source) { var errors = TestHarness.CompileAndVerifyOnly(source);