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."); +}