diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 3fdedd1fb..0fcf48bdb 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -70,13 +70,21 @@ // 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. +// The `return` counterpart: the `ts.If` MLIRGen put at the end of a hand-written `finally` around +// what the scopes around the `try` owe - their `using`s to dispose and their locals to release - +// 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 it in the copy a `return` runs and erases it from the rest. #define RETURN_ONLY_ATTR_NAME "__return_only" +// The same for a `break` or `continue` out of the `try` body or catch clause, one `ts.If` per +// place they go. Its value names the jump: JUMP_ONLY_BREAK_KEY or JUMP_ONLY_CONTINUE_KEY followed +// by the label (empty when there is none), as the jump's BreakOp or ContinueOp carries it. +// TryOpLowering keeps it in the copy that jump runs and erases it from the rest. +#define JUMP_ONLY_ATTR_NAME "__jump_only" +#define JUMP_ONLY_BREAK_KEY "break:" +#define JUMP_ONLY_CONTINUE_KEY "continue:" + // 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 5ff3a8ad8..a482e09e4 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h @@ -244,12 +244,17 @@ struct GenContext 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. + // `return`, `break` or `continue` from inside the body or the catch that gets this far + // disposes and releases the scopes up to here and no further: the finally still runs first, + // and it can read the scopes from here outwards, so the copy of the finally that the jump + // runs disposes and releases them once it is done (mlirGenScopeExitAfterFinally). 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; + // On a finallyGuardScope: the `break`s (true) and `continue`s (false), by label, that stopped + // there - one entry for each place they go, and the finally gets the rest of the scope exit + // for each. Shared by the body's guard and the catch clause's, which is a copy of it. + mlir::SmallVector> *finallyEscapingJumps = 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 98bc8cf29..c8dcad597 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -1629,10 +1629,13 @@ struct TryOpLowering : public TsPattern // 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. + // The scope exit 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. And the one + // each `break` or `continue` left (JUMP_ONLY_ATTR_NAME), kept in the copy for that + // jump alone. Each is a `ts.If` holding the lot, so erasing it erases them all. SmallVector unwindOnlyOps; SmallVector returnOnlyOps; + SmallVector jumpOnlyOps; tryOp.getFinally().walk([&](Operation *op) { if (isa(op)) { @@ -1647,6 +1650,13 @@ struct TryOpLowering : public TsPattern if (op->hasAttr(RETURN_ONLY_ATTR_NAME)) { returnOnlyOps.push_back(op); + return WalkResult::skip(); + } + + if (op->hasAttr(JUMP_ONLY_ATTR_NAME)) + { + jumpOnlyOps.push_back(op); + return WalkResult::skip(); } return WalkResult::advance(); @@ -1659,7 +1669,44 @@ struct TryOpLowering : public TsPattern } }; + // the key MLIRGen tagged a jump's scope exit with: the kind of jump and its label + auto jumpKey = [](Operation *op) { + auto label = op->getAttrOfType("label"); + return std::string(isa(op) ? JUMP_ONLY_BREAK_KEY : JUMP_ONLY_CONTINUE_KEY) + + (label ? label.getValue().str() : std::string()); + }; + + // In the copy it belongs to, the `ts.If` goes and what it holds stays in its place: + // lowered as it is, the constant condition became a conditional branch, and to the + // ownership verifier its other edge was a path that never released. + auto unwrapOps = [&](ArrayRef ops, const mlir::IRMapping &mapping) { + for (auto *op : ops) + { + auto ifOp = cast(mapping.lookup(op)); + auto &thenBlock = ifOp.getThenRegion().front(); + auto *terminator = thenBlock.getTerminator(); + rewriter.inlineBlockBefore(&thenBlock, ifOp); + rewriter.eraseOp(terminator); + rewriter.eraseOp(ifOp); + } + }; + + auto keepJumpOnlyOps = [&](StringRef keepKey, const mlir::IRMapping &mapping) { + for (auto *op : jumpOnlyOps) + { + if (op->getAttrOfType(JUMP_ONLY_ATTR_NAME).getValue() == keepKey) + { + unwrapOps(op, mapping); + } + else + { + rewriter.eraseOp(mapping.lookup(op)); + } + } + }; + eraseOps(returnOnlyOps, finallyMapping); + eraseOps(jumpOnlyOps, finallyMapping); // add clone for 'return' if (returns.size() > 0) @@ -1669,6 +1716,8 @@ struct TryOpLowering : public TsPattern returnFinallyMapping); propagateTsContextEntries(returnFinallyMapping); eraseOps(unwindOnlyOps, returnFinallyMapping); + eraseOps(jumpOnlyOps, returnFinallyMapping); + unwrapOps(returnOnlyOps, returnFinallyMapping); auto returnFinallyBlockLast = continuation->getPrevNode(); rewriter.setInsertionPoint(returnFinallyBlockLast->getTerminator()); auto resultOpOfReturnFinallyBlock = cast(returnFinallyBlockLast->getTerminator()); @@ -1677,7 +1726,7 @@ struct TryOpLowering : public TsPattern // 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). + // releases the scopes it guards (mlirGenScopeExitAfterFinally). mlir::Block *outerCleanup = nullptr; for (auto retOp : returns) { @@ -1705,12 +1754,18 @@ struct TryOpLowering : public TsPattern // real target. Nested try/finally chains compose naturally: this try lowers // before any nested one, so a nested try later re-points the same op at its // own finally copy, which branches here. + // + // A copy ends with the rest of its jump's scope exit (JUMP_ONLY_ATTR_NAME), and that is + // MLIRGen's to say by the jump's kind and label, so a copy is per target and key: an + // unlabelled `break` and a `break outer` for the same loop go to one place, and each + // finds its own scope exit in its copy. if (escapingJumpsBody.size() > 0 || escapingJumpsCatches.size() > 0) { - mlir::DenseMap finallyCopyPerTarget; + std::map, mlir::Block *> finallyCopyPerTarget; auto routeThroughFinally = [&](Operation *op) { auto target = tsContext->jumps.lookup(op); - auto &finallyCopy = finallyCopyPerTarget[target]; + auto key = jumpKey(op); + auto &finallyCopy = finallyCopyPerTarget[{target, key}]; if (!finallyCopy) { mlir::IRMapping jumpFinallyMapping; @@ -1719,6 +1774,7 @@ struct TryOpLowering : public TsPattern propagateTsContextEntries(jumpFinallyMapping); eraseOps(unwindOnlyOps, jumpFinallyMapping); eraseOps(returnOnlyOps, jumpFinallyMapping); + keepJumpOnlyOps(key, jumpFinallyMapping); auto jumpFinallyBlockLast = continuation->getPrevNode(); rewriter.setInsertionPoint(jumpFinallyBlockLast->getTerminator()); auto resultOpOfJumpFinallyBlock = cast(jumpFinallyBlockLast->getTerminator()); @@ -1743,7 +1799,7 @@ struct TryOpLowering : public TsPattern } } - for (auto *op : llvm::concat(unwindOnlyOps, returnOnlyOps)) + for (auto *op : llvm::concat(unwindOnlyOps, returnOnlyOps, jumpOnlyOps)) { rewriter.eraseOp(op); } diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 13f2402e7..1f55d5c62 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -817,6 +817,41 @@ class MLIRGenImpl return !isTarget; } + // Whether a scope exit walking outwards stops here, at a `try` whose `finally` still has to + // run first (GenContext::finallyGuardScope): a `return`, or a `break` or `continue` that + // leaves the `try`. The finally can read this scope and the ones outside it, so the copy of + // the finally that the jump runs disposes and releases them once it is done + // (mlirGenScopeExitAfterFinally). A `break` or `continue` is noted on the guard, for the + // finally to know where it goes; the two walks of one scope exit both come by, hence the + // check for one already there. + static bool scopeExitStopsAtFinallyGuard(DisposeDepth disposeDepth, const std::string &loopLabel, + const GenContext *genContext) + { + if (genContext->finallyGuardScope != genContext) + { + return false; + } + + if (disposeDepth == DisposeDepth::FullStack) + { + return true; + } + + if (disposeDepth != DisposeDepth::LoopScope && disposeDepth != DisposeDepth::BreakScope) + { + return false; + } + + std::pair jump(disposeDepth == DisposeDepth::BreakScope, loopLabel); + auto &escapingJumps = *genContext->finallyEscapingJumps; + if (!llvm::is_contained(escapingJumps, jump)) + { + escapingJumps.push_back(jump); + } + + return true; + } + // Drops the reference each local of this scope took when it was declared. Shaped after // mlirGenDisposable, and walks outwards on the same terms, so that a `return` from a // nested block releases every scope it leaves and a `break` releases up to the loop. @@ -828,9 +863,7 @@ 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) + if (scopeExitStopsAtFinallyGuard(disposeDepth, loopLabel, genContext)) { return mlir::success(); } @@ -931,25 +964,42 @@ class MLIRGenImpl } } - // 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 + // The end of the copy of a hand-written `finally` that a `return`, `break` or `continue` from + // its `try` body or catch clause runs: the rest of that jump's scope exit. The jump disposed + // and 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) + // Walks outwards from the scope around the `try` on the jump's own terms, and so stops where + // it goes, or at the finallyGuardScope of the next `try` out with a `finally`: the jump goes + // on through that finally's own copy (TryOpLowering chains them), which goes on from there. + // + // Inside a `ts.If` on a constant true, tagged `onlyOnAttr` (RETURN_ONLY_ATTR_NAME or + // JUMP_ONLY_ATTR_NAME) with `onlyOnValue`, so that TryOpLowering can keep the lot in the one + // copy of the finally it belongs to and erase it from every other. Nothing at all when the + // scopes owe nothing. + mlir::LogicalResult mlirGenScopeExitAfterFinally(mlir::Location location, DisposeDepth disposeDepth, + std::string loopLabel, const GenContext *genContext, + StringRef onlyOnAttr, mlir::Attribute onlyOnValue) { - for (; genContext != nullptr && genContext->finallyGuardScope != genContext; - genContext = genContext->parentBlockContext) + auto alwaysTrue = builder.create(location, getBooleanType(), builder.getBoolAttr(true)); + auto ifOp = builder.create(location, alwaysTrue, /*withElseRegion=*/false); + ifOp->setAttr(onlyOnAttr, onlyOnValue); + { - if (genContext->ownedVars != nullptr) - { - releaseScopeOwnedVars(location, *genContext->ownedVars, RETURN_ONLY_ATTR_NAME); - } + mlir::OpBuilder::InsertionGuard insertGuard(builder); + builder.setInsertionPointToStart(&ifOp.getThenRegion().front()); + EXIT_IF_FAILED(mlirGenScopeExit(location, disposeDepth, loopLabel, genContext)); } + + auto &thenBlock = ifOp.getThenRegion().front(); + if (&thenBlock.front() == thenBlock.getTerminator()) + { + ifOp->erase(); + alwaysTrue->erase(); + } + + return mlir::success(); } // Whether an exception out of a scope opened in this context lands on a TryOp's landing pad @@ -1345,6 +1395,13 @@ class MLIRGenImpl return mlir::success(); } + // and at a `try` whose `finally` reads this scope before it may be disposed - `{ using r = + // ...; try { return; } finally { r.read(); } }` disposed `r` and then ran the finally + if (scopeExitStopsAtFinallyGuard(disposeDepth, loopLabel, genContext)) + { + return mlir::success(); + } + if (genContext->usingVars != nullptr) { // reverse declaration order, as JS disposes a scope (and as mlirGenReleaseOwned releases diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index cf2cec724..dcbfbf9aa 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -1468,14 +1468,17 @@ namespace mlirgen 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. + // has a `finally`: a `return`, `break` or `continue` from either stops here, and the + // finally's copy for that jump disposes and releases the rest + // (GenContext::finallyGuardScope). A copy of tryGenContext, because the finally itself is + // generated in tryGenContext, and a jump in the finally is not one this finally runs + // after. + SmallVector> finallyEscapingJumps; GenContext finallyGuardGenContext(tryGenContext); if (tryStatementAST->finallyBlock) { finallyGuardGenContext.finallyGuardScope = &finallyGuardGenContext; + finallyGuardGenContext.finallyEscapingJumps = &finallyEscapingJumps; } SmallVector types; @@ -1591,8 +1594,19 @@ namespace mlirgen // 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); + // Marked return-only, so TryOpLowering drops it from the other copies. + EXIT_IF_FAILED(mlirGenScopeExitAfterFinally(location, DisposeDepth::FullStack, {}, &tryGenContext, + RETURN_ONLY_ATTR_NAME, builder.getUnitAttr())); + + // The same for each place a `break` or `continue` from them goes, marked with that + // place, so TryOpLowering keeps it in the copy that jump runs and no other + for (auto &[isBreak, label] : finallyEscapingJumps) + { + auto key = std::string(isBreak ? JUMP_ONLY_BREAK_KEY : JUMP_ONLY_CONTINUE_KEY) + label; + EXIT_IF_FAILED(mlirGenScopeExitAfterFinally( + location, isBreak ? DisposeDepth::BreakScope : DisposeDepth::LoopScope, label, &tryGenContext, + JUMP_ONLY_ATTR_NAME, builder.getStringAttr(key))); + } } // terminator diff --git a/tslang/lib/TypeScript/TypeScriptOps.cpp b/tslang/lib/TypeScript/TypeScriptOps.cpp index 8cbd92b37..f5140f598 100644 --- a/tslang/lib/TypeScript/TypeScriptOps.cpp +++ b/tslang/lib/TypeScript/TypeScriptOps.cpp @@ -1719,6 +1719,13 @@ struct RemoveStaticCondition : public OpRewritePattern return failure(); } + // the scope exit a `return` or a jump left at the end of a `finally`: TryOpLowering still + // has to find it by its tag, to keep it in that jump's copy of the finally alone + if (op->hasAttr(RETURN_ONLY_ATTR_NAME) || op->hasAttr(JUMP_ONLY_ATTR_NAME)) + { + return failure(); + } + if (cast(constant.getValue()).getValue()) { replaceOpWithRegion(rewriter, op, op.getThenRegion()); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bf90629e6..52d7f971f 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -339,6 +339,7 @@ tslang_add_test(NAME test-compile-00-using-dispose-order COMMAND test-runner "${ 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-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-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") @@ -885,6 +886,7 @@ tslang_add_test(NAME test-jit-00-using-dispose-order COMMAND test-runner -jit "$ 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-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-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") @@ -1842,6 +1844,7 @@ set(TSLANG_CORPUS 00using_unwind_outer_locals.ts 00try_unwind_outer_locals.ts 00try_return_through_finally.ts + 00try_scope_exit_through_finally.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00try_scope_exit_through_finally.ts b/tslang/test/tester/tests/00try_scope_exit_through_finally.ts new file mode 100644 index 000000000..f755852e1 --- /dev/null +++ b/tslang/test/tester/tests/00try_scope_exit_through_finally.ts @@ -0,0 +1,354 @@ +// A `return`, `break` or `continue` that leaves a `try` with a `finally` runs the finally first, and +// the finally can read every scope around the `try`. So the jump disposes and releases only the +// scopes inside the `try`; the copy of the finally that the jump runs disposes and releases the +// scopes from the `try` out to where the jump goes once it is done. +// +// A `return` used to dispose the `using`s around the `try` before the finally ran - the finally +// read a disposed resource, and JS disposes it after the finally. A `break` or `continue` used to +// dispose and release the scopes around the `try` up to its loop before the finally ran, so under +// -mm=rc the finally read freed strings. +// +// The values are read again after churn() wherever a release one too many would free them early. + +let trail = ""; + +class Res { + constructor(public name: string) {} + + [Symbol.dispose]() { + churn(); + trail += "d:" + this.name + ";"; + } +} + +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; +} + +// --- return --- + +// a `using` in a block around the `try` is disposed after the finally +function returnUsingInBlock(pre: string) { + { + using r = new Res(pre + "1"); + try { + return r.name + "r"; + } + finally { + churn(); + trail += "f:" + r.name + ";"; + } + } +} + +// the function's own `using`, and a return from the catch clause +function returnUsingFromCatch(pre: string) { + using r = new Res(pre + "2"); + try { + h(); + } + catch { + return r.name + "c"; + } + finally { + churn(); + trail += "f:" + r.name + ";"; + } + + return ""; +} + +// through two finallys: each `using` goes after the finally that reads it +function returnUsingThroughTwoFinallys(pre: string) { + using a = new Res(pre + "3a"); + try { + using b = new Res(pre + "3b"); + try { + return b.name + "r"; + } + finally { + churn(); + trail += "i:" + a.name + b.name + ";"; + } + } + finally { + churn(); + trail += "o:" + a.name + ";"; + } +} + +// --- break / continue --- + +// the `try` sits in a block nested in the loop body, and the finally reads the block's local +function breakFromNestedBlock(pre: string) { + let out = ""; + for (let i = 0; i < 3; i++) { + let t = pre + "4" + i; + { + let s = t + "s"; + try { + if (i == 1) { + break; + } + } + finally { + churn(); + trail += s + ";"; + } + + out += s + ","; + } + } + + churn(); + return out; +} + +function continueFromNestedBlock(pre: string) { + let out = ""; + for (let i = 0; i < 3; i++) { + let t = pre + "5" + i; + { + let s = t + "s"; + try { + if (i == 1) { + continue; + } + } + finally { + churn(); + trail += s + t + ";"; + } + + out += s + ","; + } + } + + churn(); + return out; +} + +// the `try` sits right in the loop body, whose local the finally reads +function breakFromLoopBody(pre: string) { + let out = ""; + for (let i = 0; i < 3; i++) { + let t = pre + "6" + i; + try { + if (i == 1) { + break; + } + } + finally { + churn(); + trail += t + ";"; + } + + out += t + ","; + } + + churn(); + return out; +} + +// a `using` in the block is disposed after the finally, both for a `break` and a `continue` +function jumpUsingInBlock(pre: string) { + for (let i = 0; i < 3; i++) { + { + using r = new Res(pre + "7" + i); + try { + if (i == 0) { + continue; + } + + if (i == 1) { + break; + } + } + finally { + churn(); + trail += "f:" + r.name + ";"; + } + } + } +} + +// a labelled `continue` leaves the inner loop too: the inner body's local and the outer body's go +// after the finally +function labelledContinue(pre: string) { + let out = ""; + outer: for (let i = 0; i < 2; i++) { + let a = pre + "8" + i; + for (let j = 0; j < 2; j++) { + let b = a + j; + { + let s = b + "s"; + try { + if (j == 0) { + continue outer; + } + } + finally { + churn(); + trail += a + "|" + s + ";"; + } + } + } + + out += "never;"; + } + + churn(); + return out; +} + +// through two finallys: each releases what it reads only once it is done +function breakThroughTwoFinallys(pre: string) { + for (let i = 0; i < 3; i++) { + let a = pre + "9" + i; + { + let b = a + "b"; + try { + let c = b + "c"; + { + let d = c + "d"; + try { + if (i == 1) { + break; + } + } + finally { + churn(); + trail += "i:" + c + "|" + d + ";"; + } + } + } + finally { + churn(); + trail += "o:" + a + "|" + b + ";"; + } + } + } +} + +// a `break` out of a case of a switch +function breakFromSwitchCase(pre: string, k: number) { + switch (k) { + case 1: { + let s = pre + "b"; + { + let u = s + "u"; + try { + break; + } + finally { + churn(); + trail += s + u + ";"; + } + } + } + default: + trail += "default;"; + } +} + +// a labelled block +function breakLabelledBlock(pre: string) { + blk: { + let s = pre + "c"; + { + let u = s + "u"; + try { + break blk; + } + finally { + churn(); + trail += s + u + ";"; + } + } + } + + trail += "after;"; +} + +function main() { + let pre = "p"; + + trail = ""; + let r1 = returnUsingInBlock(pre); + churn(); + print("return, using in a block: " + r1 + " " + trail); + assert(r1 == "p1r" && trail == "f:p1;d:p1;", "return, using in a block"); + + trail = ""; + let r2 = returnUsingFromCatch(pre); + churn(); + print("return from a catch, using: " + r2 + " " + trail); + assert(r2 == "p2c" && trail == "f:p2;d:p2;", "return from a catch, using"); + + trail = ""; + let r3 = returnUsingThroughTwoFinallys(pre); + churn(); + print("return through two finallys, using: " + r3 + " " + trail); + assert(r3 == "p3br" && trail == "i:p3ap3b;d:p3b;o:p3a;d:p3a;", "return through two finallys, using"); + + trail = ""; + let r4 = breakFromNestedBlock(pre); + churn(); + print("break from a nested block: " + r4 + " " + trail); + assert(r4 == "p40s," && trail == "p40s;p41s;", "break from a nested block"); + + trail = ""; + let r5 = continueFromNestedBlock(pre); + churn(); + print("continue from a nested block: " + r5 + " " + trail); + assert(r5 == "p50s,p52s," && trail == "p50sp50;p51sp51;p52sp52;", "continue from a nested block"); + + trail = ""; + let r6 = breakFromLoopBody(pre); + churn(); + print("break from a loop body: " + r6 + " " + trail); + assert(r6 == "p60," && trail == "p60;p61;", "break from a loop body"); + + trail = ""; + jumpUsingInBlock(pre); + churn(); + print("break and continue, using: " + trail); + assert(trail == "f:p70;d:p70;f:p71;d:p71;", "break and continue, using"); + + trail = ""; + let r8 = labelledContinue(pre); + churn(); + print("labelled continue: " + r8 + " " + trail); + assert(r8 == "" && trail == "p80|p800s;p81|p810s;", "labelled continue"); + + trail = ""; + breakThroughTwoFinallys(pre); + churn(); + print("break through two finallys: " + trail); + assert(trail == "i:p90bc|p90bcd;o:p90|p90b;i:p91bc|p91bcd;o:p91|p91b;", "break through two finallys"); + + trail = ""; + breakFromSwitchCase(pre, 1); + churn(); + print("break from a switch case: " + trail); + assert(trail == "pbpbu;", "break from a switch case"); + + trail = ""; + breakLabelledBlock(pre); + churn(); + print("break a labelled block: " + trail); + assert(trail == "pcpcu;after;", "break a labelled block"); + + print("done."); +}