From 00f9fa9e7f60e68bd4d5b9057c7fa9db4816cf71 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 20:18:40 +0100 Subject: [PATCH 1/2] A return through a finally releases the scopes the finally reads after it runs A `return` from a `try` body or catch clause released the whole frame and then ran the finally's return copy, which could still read those scopes: under -mm=rc `let s = ...; try { return x; } finally { log(s); }` read a freed string. The return now stops at the scope around the `try` (GenContext::finallyGuardScope), and the finally ends with return-only releases (RETURN_ONLY_ATTR_NAME) for the rest, which TryOpLowering keeps in the return copy alone. A return through two nested finallys also skipped the outer one in every memory model: the inner return copy's ReturnOp now takes over the cleanup the outer TryOp gave the original returns. Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 7 + .../TypeScript/MLIRLogic/MLIRGenContext.h | 8 + tslang/lib/TypeScript/LowerToAffineLoops.cpp | 42 +++- tslang/lib/TypeScript/MLIRGenImpl.h | 38 ++- tslang/lib/TypeScript/MLIRGenStatements.cpp | 21 +- tslang/test/tester/CMakeLists.txt | 3 + .../tests/00try_return_through_finally.ts | 220 ++++++++++++++++++ .../tester/tests/00try_unwind_outer_locals.ts | 8 +- 8 files changed, 331 insertions(+), 16 deletions(-) create mode 100644 tslang/test/tester/tests/00try_return_through_finally.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 8b0ad5cb7..3fdedd1fb 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -70,6 +70,13 @@ // rest. #define UNWIND_ONLY_ATTR_NAME "__unwind_only" +// The `return` counterpart: a release MLIRGen put at the end of a hand-written `finally` for what +// the scopes around the `try` owe once a `return` from its body or catch clause has run the +// finally. The `return` itself stops short of those scopes, since the finally may still read them +// (GenContext::finallyGuardScope). TryOpLowering keeps these in the copy a `return` runs and +// erases them from the rest. +#define RETURN_ONLY_ATTR_NAME "__return_only" + // Marks a `ts.CreateBoundFunction` whose `this` is a capture box built for it a moment earlier, // rather than a receiver that belongs to somebody else. Only such a closure owns its `this`, and // only it gets the type tag at CLOSURE_TYPE_INDEX - a bound method must not take ownership of diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h index 8f15bbbb0..5ff3a8ad8 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h @@ -242,6 +242,14 @@ struct GenContext // scope nested further in releases the scopes between, and stops here // (mlirGenReleaseOwnedOnUnwind). bool isUnwindCleanupScope = false; + // Points at itself on the context that stands, in the walk outwards, for the scope around a + // `try` with a `finally` - the parent of its body's scope, and its catch clause's context. A + // `return` from inside the body or the catch releases the scopes up to here and no further: + // the finally still runs before the function returns, and it can read the scopes from here + // outwards, so the copy of the finally that a `return` runs releases them once it is done + // (mlirGenReleaseOwnedOnReturn). A pointer rather than a flag, so that it needs no clearing: + // every context copied from this one points at this one, not at itself. + const GenContext *finallyGuardScope = nullptr; // out-of-band cancellation signal; mutable so stop() stays callable through the const& threading mutable bool stopProcess = false; mlir::SmallVector> *postponedMessages = nullptr; diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index f80c07d5a..98bc8cf29 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1628,7 +1628,11 @@ struct TryOpLowering : public TsPattern // Erased from each later copy through its mapping, and from the region itself only once // the last copy is made: the rewriter defers an erasure, so an op erased from the // region still goes into every copy cloned from it afterwards. + // + // The releases a `return` left for after the finally (RETURN_ONLY_ATTR_NAME) the other + // way round: kept in the copy a `return` runs, and gone from every other. SmallVector unwindOnlyOps; + SmallVector returnOnlyOps; tryOp.getFinally().walk([&](Operation *op) { if (isa(op)) { @@ -1640,16 +1644,23 @@ struct TryOpLowering : public TsPattern unwindOnlyOps.push_back(op); } + if (op->hasAttr(RETURN_ONLY_ATTR_NAME)) + { + returnOnlyOps.push_back(op); + } + return WalkResult::advance(); }); - auto eraseUnwindOnlyOps = [&](const mlir::IRMapping &mapping) { - for (auto *op : unwindOnlyOps) + auto eraseOps = [&](ArrayRef ops, const mlir::IRMapping &mapping) { + for (auto *op : ops) { rewriter.eraseOp(mapping.lookup(op)); } }; + eraseOps(returnOnlyOps, finallyMapping); + // add clone for 'return' if (returns.size() > 0) { @@ -1657,11 +1668,29 @@ struct TryOpLowering : public TsPattern rewriter.cloneRegionBefore(tryOp.getFinally(), *continuation->getParent(), continuation->getIterator(), returnFinallyMapping); propagateTsContextEntries(returnFinallyMapping); - eraseUnwindOnlyOps(returnFinallyMapping); + eraseOps(unwindOnlyOps, returnFinallyMapping); auto returnFinallyBlockLast = continuation->getPrevNode(); rewriter.setInsertionPoint(returnFinallyBlockLast->getTerminator()); auto resultOpOfReturnFinallyBlock = cast(returnFinallyBlockLast->getTerminator()); - rewriter.replaceOpWithNewOp(resultOpOfReturnFinallyBlock); + auto returnOfFinally = rewriter.replaceOpWithNewOp(resultOpOfReturnFinallyBlock); + + // A `try` around this one lowered first and sent these returns to its own finally + // copy. The return this copy ends with goes there in their stead, so the outer + // finally still runs - pointing the returns at this copy alone skipped it - and + // releases the scopes it guards (mlirGenReleaseOwnedOnReturn). + mlir::Block *outerCleanup = nullptr; + for (auto retOp : returns) + { + auto retOpCleanup = tsContext->cleanup.lookup(retOp); + assert(!outerCleanup || !retOpCleanup || outerCleanup == retOpCleanup); + outerCleanup = outerCleanup ? outerCleanup : retOpCleanup; + } + + if (outerCleanup) + { + tsContext->cleanup[returnOfFinally] = outerCleanup; + } + // if has returns we need to create return cleanup block for (auto retOp : returns) { @@ -1688,7 +1717,8 @@ struct TryOpLowering : public TsPattern rewriter.cloneRegionBefore(tryOp.getFinally(), *continuation->getParent(), continuation->getIterator(), jumpFinallyMapping); propagateTsContextEntries(jumpFinallyMapping); - eraseUnwindOnlyOps(jumpFinallyMapping); + eraseOps(unwindOnlyOps, jumpFinallyMapping); + eraseOps(returnOnlyOps, jumpFinallyMapping); auto jumpFinallyBlockLast = continuation->getPrevNode(); rewriter.setInsertionPoint(jumpFinallyBlockLast->getTerminator()); auto resultOpOfJumpFinallyBlock = cast(jumpFinallyBlockLast->getTerminator()); @@ -1713,7 +1743,7 @@ struct TryOpLowering : public TsPattern } } - for (auto *op : unwindOnlyOps) + for (auto *op : llvm::concat(unwindOnlyOps, returnOnlyOps)) { rewriter.eraseOp(op); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index f8e0cb0ca..13f2402e7 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -828,6 +828,13 @@ class MLIRGenImpl return mlir::success(); } + // a `return` stops at a `try` whose `finally` still has to run: that finally's copy for + // the `return` releases the scopes from here outwards (mlirGenReleaseOwnedOnReturn) + if (disposeDepth == DisposeDepth::FullStack && genContext->finallyGuardScope == genContext) + { + return mlir::success(); + } + if (genContext->ownedVars != nullptr) { releaseScopeOwnedVars(location, *genContext->ownedVars); @@ -859,8 +866,10 @@ class MLIRGenImpl return mlir::success(); } + // `onlyOnAttr` names the copies of a hand-written finally the releases are kept in - + // UNWIND_ONLY_ATTR_NAME or RETURN_ONLY_ATTR_NAME - or is empty for an ordinary release void releaseScopeOwnedVars(mlir::Location location, const SmallVector &ownedVars, - bool unwindOnly = false) + StringRef onlyOnAttr = {}) { // reverse declaration order, the order a scope is unwound in: a later local may // hold the only other reference to what an earlier one points at @@ -880,9 +889,9 @@ class MLIRGenImpl releaseOp = builder.create(location, storage); } - if (releaseOp && unwindOnly) + if (releaseOp && !onlyOnAttr.empty()) { - releaseOp->setAttr(UNWIND_ONLY_ATTR_NAME, builder.getUnitAttr()); + releaseOp->setAttr(onlyOnAttr, builder.getUnitAttr()); } } } @@ -917,7 +926,28 @@ class MLIRGenImpl { if (genContext->ownedVars != nullptr) { - releaseScopeOwnedVars(location, *genContext->ownedVars, unwindOnly); + releaseScopeOwnedVars(location, *genContext->ownedVars, unwindOnly ? UNWIND_ONLY_ATTR_NAME : ""); + } + } + } + + // The end of the copy of a hand-written `finally` that a `return` from its `try` body or catch + // clause runs (RETURN_ONLY_ATTR_NAME): the rest of that `return`'s scope exit. The `return` + // released the scopes inside the `try` and stopped at its finallyGuardScope, because the + // finally can still read every scope from there outwards - `let s = ...; try { return x; } + // finally { log(s); }` read `s` after the `return` had given it back. + // + // Walks outwards from the scope around the `try`, as a `return` does, and stops at the + // finallyGuardScope of the next `try` out with a `finally`: the `return` goes on through that + // finally's own copy (TryOpLowering chains them), which releases from there. + void mlirGenReleaseOwnedOnReturn(mlir::Location location, const GenContext *genContext) + { + for (; genContext != nullptr && genContext->finallyGuardScope != genContext; + genContext = genContext->parentBlockContext) + { + if (genContext->ownedVars != nullptr) + { + releaseScopeOwnedVars(location, *genContext->ownedVars, RETURN_ONLY_ATTR_NAME); } } } diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index f8a6da36d..cf2cec724 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -1467,6 +1467,17 @@ namespace mlirgen tryGenContext.allocateScopeOwnedVarsOutsideOfOperation = true; tryGenContext.currentOperation = tryOp; + // The scope around the `try` as its body and catch clause see it on the way out, when it + // has a `finally`: a `return` from either stops here, and the finally's copy for the + // `return` releases the rest (GenContext::finallyGuardScope). A copy of tryGenContext, + // because the finally itself is generated in tryGenContext, and a `return` in the + // finally is not one this finally runs after. + GenContext finallyGuardGenContext(tryGenContext); + if (tryStatementAST->finallyBlock) + { + finallyGuardGenContext.finallyGuardScope = &finallyGuardGenContext; + } + SmallVector types; /*auto *body =*/builder.createBlock(&tryOp.getBody(), {}, types); @@ -1481,7 +1492,7 @@ namespace mlirgen // prepare custom scope SymbolTableScopeT varScope(symbolTable); GenContext tryBodyGenContext(tryGenContext); - tryBodyGenContext.parentBlockContext = &tryGenContext; + tryBodyGenContext.parentBlockContext = &finallyGuardGenContext; // as in mlirGen(Block): this is the block's own scope, so it is where a break or // continue walking outwards stops @@ -1549,10 +1560,11 @@ namespace mlirgen // With a finally, an exception out of the catch clause unwinds into the finally's // unwind copy, which releases the scopes around the `try` (below) - so a `throw` in // the clause must stop short of them, as it does at a TryOp's body - GenContext catchGenContext(tryGenContext); + GenContext catchGenContext(finallyGuardGenContext); if (tryStatementAST->finallyBlock) { catchGenContext.isUnwindCleanupScope = true; + catchGenContext.finallyGuardScope = &catchGenContext; } auto result = mlirGen(tryStatementAST->catchClause->block, catchGenContext); @@ -1576,6 +1588,11 @@ namespace mlirgen // TryOpLowering drops them from the copies normal completion, `return` and `break` // run, where those scopes are still live. mlirGenReleaseOwnedOnUnwind(location, &tryGenContext, /*unwindOnly=*/true); + + // And the copy a `return` from the body or the catch clause runs: the rest of that + // `return`'s scope exit, which it left for after the finally (finallyGuardScope). + // Marked return-only, so TryOpLowering drops them from the other copies. + mlirGenReleaseOwnedOnReturn(location, &tryGenContext); } // terminator diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 42d422106..bf90629e6 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -338,6 +338,7 @@ tslang_add_test(NAME test-compile-00-switch-case-scope-exit COMMAND test-runner tslang_add_test(NAME test-compile-00-using-dispose-order COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_dispose_order.ts") tslang_add_test(NAME test-compile-00-using-unwind-outer-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_outer_locals.ts") tslang_add_test(NAME test-compile-00-try-unwind-outer-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_unwind_outer_locals.ts") +tslang_add_test(NAME test-compile-00-try-return-through-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_return_through_finally.ts") tslang_add_test(NAME test-compile-00-vars COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00vars.ts") tslang_add_test(NAME test-compile-00-var-bindings COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00var_bindings.ts") tslang_add_test(NAME test-compile-00-globals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals.ts") @@ -883,6 +884,7 @@ tslang_add_test(NAME test-jit-00-switch-case-scope-exit COMMAND test-runner -jit tslang_add_test(NAME test-jit-00-using-dispose-order COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_dispose_order.ts") tslang_add_test(NAME test-jit-00-using-unwind-outer-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00using_unwind_outer_locals.ts") tslang_add_test(NAME test-jit-00-try-unwind-outer-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_unwind_outer_locals.ts") +tslang_add_test(NAME test-jit-00-try-return-through-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_return_through_finally.ts") tslang_add_test(NAME test-jit-00-vars COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00vars.ts") tslang_add_test(NAME test-jit-00-var-bindings COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00var_bindings.ts") tslang_add_test(NAME test-jit-00-globals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00globals.ts") @@ -1839,6 +1841,7 @@ set(TSLANG_CORPUS 00using_dispose_order.ts 00using_unwind_outer_locals.ts 00try_unwind_outer_locals.ts + 00try_return_through_finally.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00try_return_through_finally.ts b/tslang/test/tester/tests/00try_return_through_finally.ts new file mode 100644 index 000000000..522f9239b --- /dev/null +++ b/tslang/test/tester/tests/00try_return_through_finally.ts @@ -0,0 +1,220 @@ +// A `return` from a `try` body or catch clause runs the `finally` before the function returns, and +// the finally can read every scope around the `try`. So the `return` releases only the scopes +// inside the `try`; the copy of the finally that a `return` runs releases the scopes around it once +// it is done. Under -mm=rc the `return` used to release the whole frame and then run the finally, +// which read freed strings. +// +// And a `return` through two nested `finally`s runs both, inner first: the inner one's copy used to +// return straight to the caller and skip the outer one, in every memory model. +// +// The values are read again after churn() wherever a release one too many would free them early. + +let trail = ""; + +function h() { + throw 1; +} + +// allocates over whatever was freed last, so that a string released one time too many no longer +// reads as itself +function churn() { + let junk = ""; + for (let i = 0; i < 64; i++) { + junk = junk + "#"; + } + + return junk.length; +} + +function returnFromBody(pre: string) { + let s = pre + "1"; + try { + return s + "r"; + } + finally { + churn(); + trail += s + ";"; + } +} + +// a bare `return`, and the parameter read in the finally +function returnVoid(pre: string) { + let s = pre + "2"; + try { + trail += "body;"; + return; + } + finally { + churn(); + trail += s + pre + ";"; + } +} + +// the catch clause's own scope goes with the `return`, the function's after the finally +function returnFromCatch(pre: string) { + let s = pre + "3"; + try { + h(); + } + catch { + let k = s + "k"; + return k; + } + finally { + churn(); + trail += s + ";"; + } + + return ""; +} + +// the body's own scope and a block nested in it go with the `return` +function returnFromNestedBlock(pre: string) { + let s = pre + "4"; + try { + let t = s + "t"; + { + let u = t + "u"; + return u; + } + } + finally { + churn(); + trail += s + ";"; + } +} + +// each finally reads the scope around its own `try`; the inner one's copy goes on to the outer one's +function returnThroughTwoFinallys(pre: string) { + let a = pre + "5"; + try { + let b = a + "b"; + try { + let c = b + "c"; + return c; + } + finally { + churn(); + trail += "i:" + b + ";"; + } + } + finally { + churn(); + trail += "o:" + a + ";"; + } +} + +// the scope around the `try` is a loop body's, and the `return` leaves the loop too +function returnInLoop(pre: string) { + let s = pre + "6"; + for (let i = 0; i < 3; i++) { + let t = s + i; + try { + if (i == 1) { + return t; + } + } + finally { + churn(); + trail += t + ";"; + } + } + + return ""; +} + +// the finally returns instead: its own `return` releases the scopes around the `try`, and the +// releases left for after the finally do not run as well +function returnFromFinally(pre: string, abort: boolean) { + let s = pre + "7"; + try { + return s + "r"; + } + finally { + churn(); + trail += s + ";"; + if (abort) { + return s + "f"; + } + } +} + +// normal completion and an exception still release once, after the finally +function completeAndThrow(pre: string, fail: boolean) { + let s = pre + "8"; + try { + if (fail) { + h(); + } + } + finally { + churn(); + trail += s + ";"; + } + + return s; +} + +function main() { + let pre = "p"; + + trail = ""; + let r1 = returnFromBody(pre); + churn(); + print("return from a try body: " + r1 + " " + trail); + assert(r1 == "p1r" && trail == "p1;", "return from a try body"); + + trail = ""; + returnVoid(pre); + churn(); + print("bare return: " + trail); + assert(trail == "body;p2p;", "bare return"); + + trail = ""; + let r3 = returnFromCatch(pre); + churn(); + print("return from a catch: " + r3 + " " + trail); + assert(r3 == "p3k" && trail == "p3;", "return from a catch"); + + trail = ""; + let r4 = returnFromNestedBlock(pre); + churn(); + print("return from a nested block: " + r4 + " " + trail); + assert(r4 == "p4tu" && trail == "p4;", "return from a nested block"); + + trail = ""; + let r5 = returnThroughTwoFinallys(pre); + churn(); + print("return through two finallys: " + r5 + " " + trail); + assert(r5 == "p5bc" && trail == "i:p5b;o:p5;", "return through two finallys"); + + trail = ""; + let r6 = returnInLoop(pre); + churn(); + print("return from a loop: " + r6 + " " + trail); + assert(r6 == "p61" && trail == "p60;p61;", "return from a loop"); + + trail = ""; + let r7 = returnFromFinally(pre, false); + churn(); + let r7f = returnFromFinally(pre, true); + churn(); + print("return from a finally: " + r7 + " " + r7f + " " + trail); + assert(r7 == "p7r" && r7f == "p7f" && trail == "p7;p7;", "return from a finally"); + + trail = ""; + let r8 = completeAndThrow(pre, false); + churn(); + let thrown = false; + try { + completeAndThrow(pre, true); + } + catch { + thrown = true; + } + churn(); + print("complete and throw: " + r8 + " " + trail); + assert(thrown && r8 == "p8" && trail == "p8;p8;", "complete and throw"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00try_unwind_outer_locals.ts b/tslang/test/tester/tests/00try_unwind_outer_locals.ts index cd706da00..138c62582 100644 --- a/tslang/test/tester/tests/00try_unwind_outer_locals.ts +++ b/tslang/test/tester/tests/00try_unwind_outer_locals.ts @@ -84,8 +84,8 @@ function completeThroughFinally(pre: string) { return s; } -// no unwind-only release in the copy a `return` runs. The finally does not read `s`: the return -// releases the frame before its finally runs, a separate bug. +// no unwind-only release in the copy a `return` runs (00try_return_through_finally.ts has more on +// a `return` through a finally) function returnThroughFinally(pre: string) { let s = pre + "4"; try { @@ -93,7 +93,7 @@ function returnThroughFinally(pre: string) { } finally { churn(); - trail += "f;"; + trail += s + ";"; } } @@ -266,7 +266,7 @@ function main() { let r = returnThroughFinally(pre); churn(); print("return through a finally: " + r + " " + trail); - assert(r == "p4r" && trail == "f;", "return through a finally"); + assert(r == "p4r" && trail == "p4;", "return through a finally"); trail = ""; let b = breakThroughFinally(pre); From e2388c1a191a39d0310c9ca9ac35d50a527852f4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 20:21:35 +0100 Subject: [PATCH 2/2] Test a return from a try in a catch and an exception through an empty inner finally Co-Authored-By: Claude Opus 5.5 --- .../tests/00try_return_through_finally.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tslang/test/tester/tests/00try_return_through_finally.ts b/tslang/test/tester/tests/00try_return_through_finally.ts index 522f9239b..1af48d7ce 100644 --- a/tslang/test/tester/tests/00try_return_through_finally.ts +++ b/tslang/test/tester/tests/00try_return_through_finally.ts @@ -104,6 +104,51 @@ function returnThroughTwoFinallys(pre: string) { } } +// the inner try sits in the outer one's catch clause: the `return` ends the catch, then runs both +function returnFromTryInCatch(pre: string) { + let a = pre + "9"; + try { + h(); + } + catch { + let b = a + "b"; + try { + return b; + } + finally { + churn(); + trail += "i:" + a + ";"; + } + } + finally { + churn(); + trail += "o:" + a + ";"; + } + + return ""; +} + +// an empty inner finally gets the releases left for a `return` - none comes through it here - and +// an exception through it still reaches the outer catch with `s` intact +function throwThroughEmptyInnerFinally(pre: string) { + let s = pre + "a"; + try { + let t = s + "t"; + try { + h(); + } + finally { + } + } + catch { + churn(); + trail += s + ";"; + } + + churn(); + return s; +} + // the scope around the `try` is a loop body's, and the `return` leaves the loop too function returnInLoop(pre: string) { let s = pre + "6"; @@ -188,6 +233,18 @@ function main() { print("return through two finallys: " + r5 + " " + trail); assert(r5 == "p5bc" && trail == "i:p5b;o:p5;", "return through two finallys"); + trail = ""; + let r9 = returnFromTryInCatch(pre); + churn(); + print("return from a try in a catch: " + r9 + " " + trail); + assert(r9 == "p9b" && trail == "i:p9;o:p9;", "return from a try in a catch"); + + trail = ""; + let ra = throwThroughEmptyInnerFinally(pre); + churn(); + print("throw through an empty inner finally: " + ra + " " + trail); + assert(ra == "pa" && trail == "pa;", "throw through an empty inner finally"); + trail = ""; let r6 = returnInLoop(pre); churn();