From 9927b87128ad3249cc56ffc7a7b12226971ff9fc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 21:51:00 +0100 Subject: [PATCH] A break, continue or return out of a catch clause ends the catch on every path out of it A `break` or `continue` out of a catch clause crashed with 0x80000003 on Windows (JIT and AOT, every memory model, with or without a finally), and so did a `return` out of one whenever the clause could also fall off its end. Two defects: - TryOpLowering only marked a break/continue out of a catch as leaving the catch when the try had a finally, so without one the jump ended no catch at all. A jump out of a catch nested in another catch ended only one of the two. TSContext::catchesLeft now counts the catch clauses a return, break or continue leaves, and their lowering ends that many. The return or jump in the copy of a finally entered through a catch-all pad (#599) counts that pad as one more. - Win32ExceptionPass gave each catch region a single end: its scan follows the layout and closed the region at the first end-of-catch marker. Every other way out of the clause was left as a bare branch out of the catchpad, with no funclet bundle on its calls, which WinEHPrepare turns into unreachable code. findOtherCatchExits now walks each catch clause by its control flow from the pad (a nested catch first, continuing after its ends), collects every end-of-catch marker and the calls on the way to them, and each extra end gets its own catchret. The same goes for the copy of a finally entered through a catch-all pad (#599) that leaves on some paths only (`finally { if (c) return s; ... }`): its run-to-the-end exit, which rethrows, is now always the region's main end and its `return` the other, where the layout could put the `return` first and give the rethrow path a catchret into a `resume` (SimplifyCFG crashed on it). An invoked end-of-catch marker is also recorded among the region's invokes, so that rethrow goes to the enclosing handler rather than to the caller. 00try_jump_from_catch.ts covers break, continue and return out of a catch with and without a finally, a finally reading a local of the block around the try, a typed catch, a catch nested in another, a loop inside a catch, a labelled continue, and a finally that returns or breaks on some paths only, with and without a try around it. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/TypeScriptPassContext.h | 9 +- tslang/lib/TypeScript/LowerToAffineLoops.cpp | 50 +-- .../Win32ExceptionPass.cpp | 272 ++++++++++++ tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00try_jump_from_catch.ts | 414 ++++++++++++++++++ 5 files changed, 719 insertions(+), 29 deletions(-) create mode 100644 tslang/test/tester/tests/00try_jump_from_catch.ts diff --git a/tslang/include/TypeScript/TypeScriptPassContext.h b/tslang/include/TypeScript/TypeScriptPassContext.h index 858e304ec..b7ce52985 100644 --- a/tslang/include/TypeScript/TypeScriptPassContext.h +++ b/tslang/include/TypeScript/TypeScriptPassContext.h @@ -130,6 +130,7 @@ struct TSContext finallyBlockOf.beginRun(); landingBlockOf.beginRun(); leavesCatch.beginRun(); + catchesLeft.beginRun(); catchPadIdOf.beginRun(); finallyPadIdOf.beginRun(); inParentTryFinallyPad.beginRun(); @@ -155,10 +156,12 @@ struct TSContext OpSideTable finallyBlockOf{"finallyBlockOf"}; OpSideTable landingBlockOf{"landingBlockOf"}; // Throws that sit inside a catch clause and therefore have to end the active catch before - // they leave it. `return`, `break` and `continue` carry the same meaning in `unwind`, but - // a throw cannot: `unwind` already means its invoke destination, which is a different - // question with a different answer. + // they leave it. It cannot go in `unwind`: for a throw that already means its invoke + // destination, which is a different question with a different answer. OpSideTable leavesCatch{"leavesCatch"}; + // How many catch clauses a `return`, `break` or `continue` leaves - one end-of-catch each, + // innermost first. More than one for a jump out of a catch nested in another catch clause. + OpSideTable catchesLeft{"catchesLeft"}; // Windows: the id a try's catch landing pad goes by, so that the pads of a try inside that // catch clause can name it as the funclet they are nested in - see tagPad in TryOpLowering // and ts.internal.eh_pad in Win32ExceptionPass. Ids are unique within the module. diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 2724ecd29..fab4a4c41 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -83,6 +83,15 @@ struct EntryOpLowering : public TsPattern } }; +// end the catch clauses a return, break or continue leaves (TSContext::catchesLeft) +static void endCatches(PatternRewriter &rewriter, Location loc, int count) +{ + for (auto i = 0; i < count; i++) + { + rewriter.create(loc); + } +} + struct ExitOpLowering : public TsPattern { using TsPattern::TsPattern; @@ -111,10 +120,7 @@ struct ReturnOpLowering : public TsPattern assert(tsContext->returnBlock); auto retBlock = tsContext->returnBlock; - if (auto unwind = tsContext->unwind.lookup(op)) - { - rewriter.create(loc); - } + endCatches(rewriter, loc, tsContext->catchesLeft.lookup(op)); auto *opBlock = rewriter.getInsertionBlock(); auto opPosition = rewriter.getInsertionPoint(); @@ -152,10 +158,7 @@ struct ReturnValOpLowering : public TsPattern // save value into return rewriter.create(op.getLoc(), op.getOperand(), op.getReference()); - if (auto unwind = tsContext->unwind.lookup(op)) - { - rewriter.create(loc); - } + endCatches(rewriter, loc, tsContext->catchesLeft.lookup(op)); // Split block at `assert` operation. auto *opBlock = rewriter.getInsertionBlock(); @@ -797,10 +800,7 @@ struct BreakOpLowering : public TsPattern auto jump = tsContext->jumps.lookup(breakOp); assert(jump); - if (auto unwind = tsContext->unwind.lookup(breakOp)) - { - rewriter.create(loc); - } + endCatches(rewriter, loc, tsContext->catchesLeft.lookup(breakOp)); rewriter.replaceOpWithNewOp(breakOp, jump); clh.CutBlock(); @@ -823,10 +823,7 @@ struct ContinueOpLowering : public TsPattern auto jump = tsContext->jumps.lookup(continueOp); assert(jump); - if (auto unwind = tsContext->unwind.lookup(continueOp)) - { - rewriter.create(loc); - } + endCatches(rewriter, loc, tsContext->catchesLeft.lookup(continueOp)); rewriter.replaceOpWithNewOp(continueOp, jump); clh.CutBlock(); @@ -1564,13 +1561,9 @@ struct TryOpLowering : public TsPattern // logic to set Invoke attribute CallOp auto visitorReturnOpContinue = [&](Operation *op) { - if (auto returnOp = dyn_cast_or_null(op)) - { - tsContext->unwind[op] = catchesBlock; - } - else if (auto returnValOp = dyn_cast_or_null(op)) + if (isa(op)) { - tsContext->unwind[op] = catchesBlock; + tsContext->catchesLeft[op] = tsContext->catchesLeft.lookup(op) + 1; } else if (auto throwOp = dyn_cast_or_null(op)) { @@ -1611,6 +1604,14 @@ struct TryOpLowering : public TsPattern continue; } } while (false); + + // A break/continue out of the try leaves the catch clause just as a return does and + // owes it the same end-of-catch, finally or not. Only the escaping ones: a jump to a + // loop inside the catch clause stays in it. + for (auto op : escapingJumpsCatches) + { + tsContext->catchesLeft[op] = tsContext->catchesLeft.lookup(op) + 1; + } } else { @@ -1678,7 +1679,7 @@ struct TryOpLowering : public TsPattern // to be ended on the way out, as a catch clause's `return` ends it for (auto op : escapingOpsFinally) { - tsContext->unwind[finallyMapping.lookup(op)] = beforeFinallyBlock->getNextNode(); + tsContext->catchesLeft[finallyMapping.lookup(op)] = tsContext->catchesLeft.lookup(op) + 1; } finallyBlock = beforeFinallyBlock->getNextNode(); @@ -1858,9 +1859,6 @@ struct TryOpLowering : public TsPattern for (auto op : escapingJumpsCatches) { routeThroughFinally(op); - // leaving a catch clause abruptly must end the active catch first, - // mirroring the return-from-catch handling - tsContext->unwind[op] = catchesBlock; } } diff --git a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp index e9bfa1beb..f8f0b205d 100644 --- a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp +++ b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp @@ -32,6 +32,9 @@ struct CatchRegion llvm::Instruction *saveCatch; llvm::Instruction *cxaEndCatch; llvm::Instruction *end; + // the end-of-catch markers of a catch's other ways out besides `end`, each of which needs its + // own catchret: a return, break or continue out of the clause (see findOtherCatchExits) + llvm::SmallVector otherEndMarkers; // every invoke in the region, in order - unwindInfoOp is chosen from these once all the // regions are known (see chooseUnwindInfoOps) llvm::SmallVector invokes; @@ -353,6 +356,8 @@ struct Win32ExceptionPassCode } } + findOtherCatchExits(catchRegionsWorkSet, toRemoveWorkSet); + // a region the scan found no end for cannot be made a funclet; say so rather than crash for (auto &catchRegion : catchRegionsWorkSet) { @@ -591,6 +596,11 @@ struct Win32ExceptionPassCode } } + for (auto *marker : catchRegion.otherEndMarkers) + { + catchReturnAt(catchRegion, marker, toRemoveWorkSet); + } + // convert call to Invoke with the same unwind /* llvm::SmallVector opBundle; @@ -847,6 +857,268 @@ struct Win32ExceptionPassCode } } + static bool isCallTo(llvm::Instruction *I, StringRef name) + { + auto *CB = dyn_cast(I); + return CB && CB->getCalledFunction() != nullptr && CB->getCalledFunction()->hasName() && + CB->getCalledFunction()->getName() == name; + } + + static bool isAncestorRegion(llvm::SmallVector ®ions, int ancestor, int index) + { + for (auto parent = regions[index].parent; parent >= 0; parent = regions[parent].parent) + { + if (parent == ancestor) + { + return true; + } + } + + return false; + } + + // A catch clause can be left along more than one path - a `return`, `break` or `continue` + // out of it, besides the end it falls through to - and each path ends the catch with its own + // `__cxa_end_catch` marker. The scan in runOnFunction follows the layout, so it closes the + // region at whichever marker comes first: every other way out was left a bare branch out of + // the catchpad, and the calls on the way to it had no funclet bundle. WinEHPrepare turns both + // into unreachable code, and the program hits a breakpoint (0x80000003) instead. With a catch + // nested in another, the layout can even hand the inner catch's marker to the outer one. + // + // So walk each clause by its control flow from the pad, along normal edges up to each marker + // it reaches: those are its ends, and every call on the way belongs to it. A catch nested in + // the clause is walked first, and the clause's walk goes on after each of the nested catch's + // ends, where it returns. The scan's `end` stays the region's end when it is one of them, and + // the others become otherEndMarkers. A call that belongs to an unrelated region means the walk + // has left the clause, so it stops there. + static void findOtherCatchExits(llvm::SmallVector ®ions, + llvm::SmallVector &toRemoveWorkSet) + { + DenseMap callOwner; + for (auto index = 0; index < static_cast(regions.size()); index++) + { + for (auto *CB : regions[index].calls) + { + callOwner[CB] = index; + } + } + + auto related = [&](int one, int other) { + return isAncestorRegion(regions, one, other) || isAncestorRegion(regions, other, one); + }; + + auto takeCall = [&](CallBase *CB, int index) { + auto owner = callOwner.find(CB); + if (owner != callOwner.end()) + { + if (owner->second == index) + { + return true; + } + + if (!related(owner->second, index)) + { + return false; + } + + auto &ownerRegion = regions[owner->second]; + ownerRegion.calls.erase(llvm::find(ownerRegion.calls, CB)); + if (auto *II = dyn_cast(CB)) + { + auto it = llvm::find(ownerRegion.invokes, II); + if (it != ownerRegion.invokes.end()) + { + ownerRegion.invokes.erase(it); + } + } + } + + regions[index].calls.push_back(CB); + if (auto *II = dyn_cast(CB)) + { + regions[index].invokes.push_back(II); + } + + callOwner[CB] = index; + return true; + }; + + // every end-of-catch marker of each catch region, in the order its walk reaches them + llvm::SmallVector> exits(regions.size()); + + // a region nested in another comes after it, so walking backwards walks a nested catch first + for (auto index = static_cast(regions.size()) - 1; index >= 0; index--) + { + auto ®ion = regions[index]; + if (!region.isCatch()) + { + continue; + } + + SmallPtrSet visited; + llvm::SmallVector worklist; + auto pushStart = [&](llvm::Instruction *I) { + if (I && visited.insert(I).second) + { + worklist.push_back(I); + } + }; + + // where control goes on in this clause after one of a nested catch's ends + auto pushAfterEnd = [&](llvm::Instruction *marker) { + if (auto *II = dyn_cast(marker)) + { + pushStart(&II->getNormalDest()->front()); + } + else + { + pushStart(marker->getNextNode()); + } + }; + + pushStart(region.landingPad->getNextNode()); + while (!worklist.empty()) + { + for (auto *I = worklist.pop_back_val(); I; I = I->getNextNode()) + { + if (isCallTo(I, "__cxa_end_catch")) + { + // an Itanium marker with no Win64 counterpart, whichever region the scan put it in + if (!llvm::is_contained(toRemoveWorkSet, I)) + { + toRemoveWorkSet.push_back(I); + } + + // an invoked one says where exceptions leaving the region go (chooseUnwindInfoOps), + // which a rethrow at this end needs + auto *II = dyn_cast(I); + if (II && !llvm::is_contained(region.invokes, II)) + { + region.invokes.push_back(II); + } + + exits[index].push_back(I); + break; + } + + // a throw leaves the clause; see the scan for why it carries no bundle + if (isCallTo(I, "_CxxThrowException")) + { + break; + } + + auto *CB = dyn_cast(I); + if (CB && !isCallTo(I, "__cxa_begin_catch") && !isCallTo(I, EH_PAD_FUNC_NAME) && + !isCallTo(I, "ts.internal.save_catch_var") && !takeCall(CB, index)) + { + break; + } + + if (!I->isTerminator()) + { + continue; + } + + // not an invoke's unwind edge: that goes to another region's pad - unless it is a + // catch nested in this clause, which comes back here at its ends + if (auto *II = dyn_cast(I)) + { + pushStart(&II->getNormalDest()->front()); + for (auto nested = index + 1; nested < static_cast(regions.size()); nested++) + { + if (regions[nested].parent == index && regions[nested].padBlock == II->getUnwindDest() && + regions[nested].isCatch()) + { + for (auto *marker : exits[nested]) + { + pushAfterEnd(marker); + } + } + } + } + else + { + for (auto *succ : successors(I->getParent())) + { + pushStart(&succ->front()); + } + } + } + } + + if (exits[index].empty()) + { + continue; + } + + auto isScanEnd = [&](llvm::Instruction *marker) { + return marker == region.cxaEndCatch || marker == region.end || marker->getNextNode() == region.end; + }; + + // An end that hands the exception on (the copy of a finally entered through a catch, run + // to its end: end-of-catch, then `resume`) is one only the end-of-catch loop can finish, + // by rethrowing from inside the funclet - so it is the region's `end` whatever the scan + // took, and a `return` out of that finally is the other end. + auto resumeAfter = [](llvm::Instruction *marker) -> llvm::Instruction * { + auto *next = isa(marker) ? &cast(marker)->getNormalDest()->front() + : marker->getNextNode(); + return isa_and_nonnull(next) ? next : nullptr; + }; + + auto primary = llvm::find_if(exits[index], resumeAfter); + if (primary != exits[index].end()) + { + region.cxaEndCatch = *primary; + region.end = resumeAfter(*primary); + } + else + { + primary = llvm::find_if(exits[index], isScanEnd); + } + + if (primary == exits[index].end()) + { + // the scan gave this region another catch's end: take its own first one instead + primary = exits[index].begin(); + region.cxaEndCatch = *primary; + region.end = isa(*primary) ? *primary : (*primary)->getNextNode(); + } + + for (auto *marker : exits[index]) + { + if (marker != *primary) + { + region.otherEndMarkers.push_back(marker); + } + } + } + } + + // The catchret after one of a catch's otherEndMarkers, as the end-of-catch loop makes one for + // `end`. The marker itself is already on the removal list. Not the instruction after it: that + // can be a call the funclet-bundle loop has since replaced. An invoked marker ends its block + // and is the end itself, as in the scan. + static void catchReturnAt(CatchRegion &catchRegion, llvm::Instruction *marker, + llvm::SmallVector &toRemoveWorkSet) + { + auto *I = isa(marker) ? marker : marker->getNextNode(); + if (isCatchEndingInvoke(I)) + { + return; + } + + auto *BI = dyn_cast(I); + if (!BI || !BI->isUnconditional()) + { + auto *retBlock = I->getParent()->splitBasicBlock(I, "end.of.exception"); + BI = cast(retBlock->getPrevNode()->getTerminator()); + } + + auto *CR = CatchReturnInst::Create(catchRegion.catchPad, BI->getSuccessor(0), BI->getParent()); + BI->replaceAllUsesWith(CR); + toRemoveWorkSet.push_back(BI); + } + // the pad a region's own pads are created within: its parent region's, or none static Value *parentPadOf(llvm::SmallVector ®ions, CatchRegion ®ion) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a56f624a9..d8ae1e3f8 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -340,6 +340,7 @@ tslang_add_test(NAME test-compile-00-using-unwind-outer-locals COMMAND test-runn 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-try-scope-exit-through-finally COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_scope_exit_through_finally.ts") +tslang_add_test(NAME test-compile-00-try-jump-from-catch COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_jump_from_catch.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") @@ -888,6 +889,7 @@ tslang_add_test(NAME test-jit-00-using-unwind-outer-locals COMMAND test-runner - 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-try-scope-exit-through-finally COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_scope_exit_through_finally.ts") +tslang_add_test(NAME test-jit-00-try-jump-from-catch COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00try_jump_from_catch.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") @@ -1847,6 +1849,7 @@ set(TSLANG_CORPUS 00try_unwind_outer_locals.ts 00try_return_through_finally.ts 00try_scope_exit_through_finally.ts + 00try_jump_from_catch.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00try_jump_from_catch.ts b/tslang/test/tester/tests/00try_jump_from_catch.ts new file mode 100644 index 000000000..6b1670da4 --- /dev/null +++ b/tslang/test/tester/tests/00try_jump_from_catch.ts @@ -0,0 +1,414 @@ +// A `break`, `continue` or `return` out of a catch clause leaves the catch: it ends the catch +// first, with or without a `finally`, and the clause's other way out - falling off its end - still +// ends it as well. On Windows every such exit is a catchret out of the catch funclet. +// +// A `break` or `continue` out of a catch with no finally used to leave without ending the catch, +// and on Windows any catch with two ways out - one of them a return, break or continue - kept only +// the first in layout: the other left the funclet with a bare branch, and its calls had no funclet +// bundle. Both crashed with 0x80000003. +// +// 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; +} + +// --- no finally --- + +function breakFromCatch() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch { + if (i == 1) { + break; + } + + trail += "c" + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +function continueFromCatch() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch { + if (i == 1) { + continue; + } + + trail += "c" + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +function returnFromCatch(i: number) { + try { + h(); + } + catch { + if (i == 1) { + return; + } + + trail += "c" + i + ";"; + } + + trail += "a" + i + ";"; +} + +// a typed catch clause +function breakFromTypedCatch() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch (e: number) { + if (i == 1) { + break; + } + + trail += "c" + e + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +// --- with a finally --- + +function breakFromCatchFinally() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch { + if (i == 1) { + break; + } + + trail += "c" + i + ";"; + } + finally { + trail += "f" + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +function continueFromCatchFinally() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch { + if (i == 1) { + continue; + } + + trail += "c" + i + ";"; + } + finally { + trail += "f" + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +// the finally reads a local of the block around the `try` +function breakFinallyReadsBlockLocal(pre: string) { + let out = ""; + for (let i = 0; i < 3; i++) { + let t = pre + "b" + i; + { + let s = t + "s"; + try { + h(); + } + catch { + if (i == 1) { + break; + } + } + finally { + churn(); + trail += s + ";"; + } + + out += s + ","; + } + } + + churn(); + return out; +} + +function continueFinallyReadsBlockLocal(pre: string) { + let out = ""; + for (let i = 0; i < 3; i++) { + let t = pre + "c" + i; + { + let s = t + "s"; + try { + h(); + } + catch { + if (i == 1) { + continue; + } + } + finally { + churn(); + trail += s + t + ";"; + } + + out += s + ","; + } + } + + churn(); + return out; +} + +// --- nesting --- + +// a `break` out of a catch nested in another catch leaves both +function breakFromNestedCatch() { + for (let i = 0; i < 3; i++) { + try { + h(); + } + catch { + try { + h(); + } + catch { + if (i == 1) { + break; + } + + trail += "n" + i + ";"; + } + + trail += "o" + i + ";"; + } + + trail += "a" + i + ";"; + } +} + +// a `break` out of a loop inside the catch clause stays in the catch +function breakLoopInsideCatch() { + try { + h(); + } + catch { + for (let j = 0; j < 3; j++) { + if (j == 1) { + break; + } + + trail += "j" + j + ";"; + } + + trail += "c;"; + } + + trail += "a;"; +} + +// a labelled `continue` out of a catch in the inner loop leaves the inner loop too +function labelledContinueFromCatch() { + outer: for (let i = 0; i < 2; i++) { + for (let j = 0; j < 2; j++) { + try { + h(); + } + catch { + if (j == 1) { + continue outer; + } + + trail += "c" + i + j + ";"; + } + } + + trail += "never;"; + } +} + +// --- a finally that leaves on some paths only --- +// +// On the exceptional path such a finally runs inside a catch-all pad: its `return` or `break` ends +// that catch and swallows the exception, and running to its end hands the exception on. + +function returnFromFinallySometimes(c: boolean) { + try { + h(); + } + finally { + if (c) { + return "r;"; + } + + trail += "f;"; + } + + return "n;"; +} + +// the same with a try around it in the function, which catches what the finally hands on +function returnFromFinallySometimesInTry(c: boolean) { + try { + try { + h(); + } + finally { + if (c) { + return "r;"; + } + + trail += "f;"; + } + } + catch { + trail += "c;"; + } + + return "n;"; +} + +function breakFromFinallySometimes() { + for (let i = 0; i < 3; i++) { + try { + try { + h(); + } + finally { + if (i == 1) { + break; + } + + trail += "f" + i + ";"; + } + } + catch { + trail += "c" + i + ";"; + } + } +} + +function main() { + trail = ""; + breakFromCatch(); + print("break from a catch: " + trail); + assert(trail == "c0;a0;", "break from a catch"); + + trail = ""; + continueFromCatch(); + print("continue from a catch: " + trail); + assert(trail == "c0;a0;c2;a2;", "continue from a catch"); + + trail = ""; + for (let i = 0; i < 3; i++) { + returnFromCatch(i); + } + + print("return from a catch: " + trail); + assert(trail == "c0;a0;c2;a2;", "return from a catch"); + + trail = ""; + breakFromTypedCatch(); + print("break from a typed catch: " + trail); + assert(trail == "c10;a0;", "break from a typed catch"); + + trail = ""; + breakFromCatchFinally(); + print("break from a catch, finally: " + trail); + assert(trail == "c0;f0;a0;f1;", "break from a catch, finally"); + + trail = ""; + continueFromCatchFinally(); + print("continue from a catch, finally: " + trail); + assert(trail == "c0;f0;a0;f1;c2;f2;a2;", "continue from a catch, finally"); + + let pre = "p"; + + trail = ""; + let r1 = breakFinallyReadsBlockLocal(pre); + churn(); + print("break from a catch, finally reads a block local: " + r1 + " " + trail); + assert(r1 == "pb0s," && trail == "pb0s;pb1s;", "break from a catch, finally reads a block local"); + + trail = ""; + let r2 = continueFinallyReadsBlockLocal(pre); + churn(); + print("continue from a catch, finally reads a block local: " + r2 + " " + trail); + assert(r2 == "pc0s,pc2s," && trail == "pc0spc0;pc1spc1;pc2spc2;", "continue from a catch, finally reads a block local"); + + trail = ""; + breakFromNestedCatch(); + print("break from a nested catch: " + trail); + assert(trail == "n0;o0;a0;", "break from a nested catch"); + + trail = ""; + breakLoopInsideCatch(); + print("break a loop inside a catch: " + trail); + assert(trail == "j0;c;a;", "break a loop inside a catch"); + + trail = ""; + labelledContinueFromCatch(); + print("labelled continue from a catch: " + trail); + assert(trail == "c00;c10;", "labelled continue from a catch"); + + trail = ""; + let f1 = returnFromFinallySometimes(true); + trail += f1; + try { + let f2 = returnFromFinallySometimes(false); + trail += f2; + } + catch { + trail += "caught;"; + } + + print("return from a finally on some paths: " + trail); + assert(trail == "r;f;caught;", "return from a finally on some paths"); + + trail = ""; + let t1 = returnFromFinallySometimesInTry(true); + trail += t1; + let t2 = returnFromFinallySometimesInTry(false); + trail += t2; + print("return from a finally on some paths, in a try: " + trail); + assert(trail == "r;f;c;n;", "return from a finally on some paths, in a try"); + + trail = ""; + breakFromFinallySometimes(); + print("break from a finally on some paths: " + trail); + assert(trail == "f0;c0;", "break from a finally on some paths"); + + print("done."); +}