From 13e4656486750a66aeb855d46f90acc8d9aa8c2f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 16:37:40 +0100 Subject: [PATCH] A labelled break or continue disposes and releases up to the statement it names The scope-exit walk handed each enclosing scope an empty label, and every loop's context held an empty one, so a labelled jump stopped at the first loop body it met: `continue outer` in an `if` in an inner loop never disposed the outer body's `using`. A jump to a labelled block matched nothing and ran on to the function's scope, disposing the function's `using` there and then and, under rc, releasing its locals (a string built in the block was freed before it was returned). The walk now carries the label. A loop's body scope records the loop's label in `loopBodyLabel`, apart from `loopLabel`: a loop's context is a copy of the scope around it and stands in for that scope in the walk. A labelled statement that is not a loop gets a scope of its own, `labelledScopeLabel`, where its jumps stop. An unlabelled break in a labelled block left only the block: LabelOp lowering claimed unlabelled jumps, which in JS belong to the loop or switch around it. And a loop body written without braces had no body scope, so a break or continue in it disposed the scopes around the loop; it is now made one as a brace-less if branch is. Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/MLIRLogic/MLIRGenContext.h | 8 + tslang/lib/TypeScript/LowerToAffineLoops.cpp | 17 ++- tslang/lib/TypeScript/MLIRGenImpl.h | 34 +++-- tslang/lib/TypeScript/MLIRGenStatements.cpp | 65 ++++++-- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00labelled_scope_exit.ts | 142 ++++++++++++++++++ 6 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 tslang/test/tester/tests/00labelled_scope_exit.ts diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h index 80a57f69d..9d28aee3f 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h @@ -215,7 +215,15 @@ struct GenContext // Set by a loop on the context it hands its body, and inherited by every context copied // from it - so it answers "somewhere inside a loop", not "is the loop". bool isLoop = false; + // the label of the loop that set `isLoop`, handed to its body (see loopBodyLabel) std::string loopLabel; + // With isLoopBodyScope: the label of the loop this is the body of. Kept apart from `loopLabel` + // because a loop's context is a copy of the scope around it and stands in for that scope in the + // walk outwards - it overwrites `loopLabel` for its own body, and keeps this for the scope it copies. + std::string loopBodyLabel; + // Set on the scope of a labelled statement that is not a loop (`a: { ... }`): where a + // `break a` or `continue a` walking outwards stops. + std::string labelledScopeLabel; // Set by the block that becomes the loop's own body scope, and cleared for anything nested // further in. That is the distinction a `break` or `continue` needs: it owes a dispose and // a release to every scope between itself and the loop, so the walk outwards can only stop diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 5f70461f2..4a516808b 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -471,8 +471,8 @@ struct ResultOpLowering : public TsPattern }; // Populates tsContext->jumps for break/continue ops that belong to this loop/label scope. -// An unlabeled break/continue binds to the *nearest* enclosing loop/label (continue) or -// loop/label/switch (break), so once the walk descends past such a boundary it must stop +// An unlabeled break/continue binds to the *nearest* enclosing loop (continue) or +// loop/switch (break), so once the walk descends past such a boundary it must stop // treating unlabeled break/continue as claimable by this (outer) scope -- otherwise it can // steal a jump target that belongs to the nested scope, depending on which scope's pattern // happens to run last in the dialect-conversion worklist. A nested Switch is a boundary for @@ -514,14 +514,15 @@ static void visitBreakContinueInScope(mlir::Region ®ion, mlir::StringAttr sco continue; } - auto isLoopOrLabelBoundary = isa(op); + // not a LabelOp: an unlabelled break or continue never targets a labelled block + auto isLoopBoundary = isa(op); auto isSwitchBoundary = isa(op); for (auto &nested : op.getRegions()) { visitBreakContinueInScope(nested, scopeLabel, - eligibleForUnlabeledBreak && !isLoopOrLabelBoundary && !isSwitchBoundary, - eligibleForUnlabeledContinue && !isLoopOrLabelBoundary, onMatch); + eligibleForUnlabeledBreak && !isLoopBoundary && !isSwitchBoundary, + eligibleForUnlabeledContinue && !isLoopBoundary, onMatch); } } } @@ -737,8 +738,10 @@ struct LabelOpLowering : public TsPattern // logic to support continue/break - visitBreakContinueInScope(labelOp.getLabelRegion(), labelAttr, /*eligibleForUnlabeledBreak*/ true, - /*eligibleForUnlabeledContinue*/ true, [&](Operation *op, bool isBreak) { + // only its own label: in JS an unlabelled break or continue in a labelled block belongs to the + // loop or switch around it + visitBreakContinueInScope(labelOp.getLabelRegion(), labelAttr, /*eligibleForUnlabeledBreak*/ false, + /*eligibleForUnlabeledContinue*/ false, [&](Operation *op, bool isBreak) { tsContext->jumps[op] = isBreak ? continuation : begin; }); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 738eb613e..e5d08cca0 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -748,18 +748,24 @@ class MLIRGenImpl return false; } - // The label comparison is left exactly as it was, including the empty label the - // recursion below hands the parent. It looks like it should carry the target label - // outwards instead, but the loop sites clear `label` before storing it, so a labelled - // loop's context holds an empty one too - and `continue cont1` then relies on the outer - // loop matching that empty label. 02disposable.ts is the case that proves it. - // (The `for` site now takes the label before its initializer and stores "" outright.) - // Known gap: a labelled `continue` written inside an `if` in an inner loop stops at the - // inner loop's body, so the outer body's `using` is not disposed. Carrying real labels - // needs the loop context, which stands in for the enclosing body scope in this walk, to - // keep that scope's label apart from the label of the loop it starts. - auto isTargetLoop = genContext->isLoopBodyScope && genContext->loopLabel == loopLabel; - return !isTargetLoop; + // The target label is carried outwards unchanged. An unlabelled jump stops at the nearest + // loop body. A labelled one stops at the body of the loop of that label, or at the scope of + // the labelled statement (`a: { ... break a; }`), and owes every scope it passes on the way, + // the bodies of inner loops included. + // + // This used to hand the parent an empty label, and every loop's context held an empty one + // too, so a labelled jump stopped at the first loop body after the one it started in: a + // `continue outer` written in an `if` in an inner loop skipped the outer body's `using`. + // A jump to a labelled block matched nothing and ran on to the function's scope, + // disposing its `using` and releasing its locals there and then. + if (loopLabel.empty()) + { + return !genContext->isLoopBodyScope; + } + + auto isTarget = (genContext->isLoopBodyScope && genContext->loopBodyLabel == loopLabel) || + genContext->labelledScopeLabel == loopLabel; + return !isTarget; } // Drops the reference each local of this scope took when it was declared. Shaped after @@ -814,7 +820,7 @@ class MLIRGenImpl // OwnedReturnConsumptionPass, not this. if (scopeExitContinuesOutwards(disposeDepth, loopLabel, genContext)) { - EXIT_IF_FAILED(mlirGenReleaseOwned(location, disposeDepth, {}, genContext->parentBlockContext)); + EXIT_IF_FAILED(mlirGenReleaseOwned(location, disposeDepth, loopLabel, genContext->parentBlockContext)); } return mlir::success(); @@ -1225,7 +1231,7 @@ class MLIRGenImpl // something a program was found to need. if (scopeExitContinuesOutwards(disposeDepth, loopLabel, genContext)) { - EXIT_IF_FAILED(mlirGenDisposable(location, disposeDepth, {}, genContext->parentBlockContext)); + EXIT_IF_FAILED(mlirGenDisposable(location, disposeDepth, loopLabel, genContext->parentBlockContext)); } return mlir::success(); diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 97b42a856..3ad649cd2 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -184,6 +184,9 @@ namespace mlirgen // the context it handed down. Take it, and clear the offer so a block nested further in // does not claim to be the loop as well - see GenContext::isLoopBodyScope. genContextUsing.isLoopBodyScope = genContext.isLoop; + genContextUsing.loopBodyLabel = genContext.loopLabel; + // a scope of its own, not the labelled statement's: see labelledContext in mlirGen(LabeledStatement) + genContextUsing.labelledScopeLabel.clear(); genContextUsing.isLoop = false; DITableScopeT debugBlockScope(debugScope); @@ -256,6 +259,8 @@ namespace mlirgen // as in mlirGen(Block): this is the block's own scope, so it is where a break or // continue walking outwards stops tryBodyGenContext.isLoopBodyScope = tryGenContext.isLoop; + tryBodyGenContext.loopBodyLabel = tryGenContext.loopLabel; + tryBodyGenContext.labelledScopeLabel.clear(); tryBodyGenContext.isLoop = false; auto usingVars = std::make_unique>(); @@ -662,6 +667,10 @@ namespace mlirgen // generator's iterator, a `using` - belongs to the branch, whose region it is defined in, not // to the block around the `if`, whose end is outside that region (#458). A braced branch is a // Block and makes this scope itself. + // + // A loop's body written without braces (`for (...) if (c) continue;`) is made a scope here too, + // and, as in mlirGen(Block), the loop's body scope: without one a `break` or `continue` found no + // body to stop at and disposed and released the scopes around the loop. mlir::LogicalResult MLIRGenImpl::mlirGenBranchStatement(Statement statement, const GenContext &genContext) { if (statement == SyntaxKind::Block) @@ -673,7 +682,10 @@ namespace mlirgen GenContext branchContext(genContext); branchContext.parentBlockContext = &genContext; - branchContext.isLoopBodyScope = false; + branchContext.isLoopBodyScope = genContext.isLoop; + branchContext.loopBodyLabel = genContext.loopLabel; + branchContext.isLoop = false; + branchContext.labelledScopeLabel.clear(); auto usingVars = std::make_unique>(); branchContext.usingVars = usingVars.get(); @@ -696,22 +708,23 @@ namespace mlirgen SmallVector operands; auto doWhileOp = builder.create(location, types, operands); - if (!label.empty()) + auto loopLabel = label; + label = ""; + if (!loopLabel.empty()) { - doWhileOp->setAttr(LABEL_ATTR_NAME, builder.getStringAttr(label)); - label = ""; + doWhileOp->setAttr(LABEL_ATTR_NAME, builder.getStringAttr(loopLabel)); } GenContext loopGenContext(genContext); loopGenContext.isLoop = true; - loopGenContext.loopLabel = label; + loopGenContext.loopLabel = loopLabel; /*auto *cond =*/builder.createBlock(&doWhileOp.getCond(), {}, types); /*auto *body =*/builder.createBlock(&doWhileOp.getBody(), {}, types); // body in condition builder.setInsertionPointToStart(&doWhileOp.getBody().front()); - auto result2 = mlirGen(doStatementAST->statement, loopGenContext); + auto result2 = mlirGenBranchStatement(doStatementAST->statement, loopGenContext); EXIT_IF_FAILED(result2) // just simple return, as body in cond builder.create(location); @@ -742,15 +755,16 @@ namespace mlirgen SmallVector operands; auto whileOp = builder.create(location, types, operands); - if (!label.empty()) + auto loopLabel = label; + label = ""; + if (!loopLabel.empty()) { - whileOp->setAttr(LABEL_ATTR_NAME, builder.getStringAttr(label)); - label = ""; + whileOp->setAttr(LABEL_ATTR_NAME, builder.getStringAttr(loopLabel)); } GenContext loopGenContext(genContext); loopGenContext.isLoop = true; - loopGenContext.loopLabel = label; + loopGenContext.loopLabel = loopLabel; /*auto *cond =*/builder.createBlock(&whileOp.getCond(), {}, types); /*auto *body =*/builder.createBlock(&whileOp.getBody(), {}, types); @@ -782,7 +796,7 @@ namespace mlirgen SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); checkSafeCast(whileStatementAST->expression, conditionValue, nullptr, loopGenContext); - auto result2 = mlirGen(whileStatementAST->statement, loopGenContext); + auto result2 = mlirGenBranchStatement(whileStatementAST->statement, loopGenContext); EXIT_IF_FAILED(result2) } @@ -835,8 +849,7 @@ namespace mlirgen GenContext loopGenContext(genContext); loopGenContext.isLoop = true; - // empty, as at the other loop sites: see scopeExitContinuesOutwards - loopGenContext.loopLabel = ""; + loopGenContext.loopLabel = loopLabel; /*auto *cond =*/builder.createBlock(&forOp.getCond(), {}, types); /*auto *body =*/builder.createBlock(&forOp.getBody(), {}, types); @@ -889,7 +902,7 @@ namespace mlirgen checkSafeCast(forStatementAST->condition, testedValue, nullptr, loopGenContext); } - auto bodyResult = mlirGen(forStatementAST->statement, loopGenContext); + auto bodyResult = mlirGenBranchStatement(forStatementAST->statement, loopGenContext); EXIT_IF_FAILED(bodyResult) } @@ -1049,7 +1062,27 @@ namespace mlirgen builder.setInsertionPointToStart(mergeBlock); - auto res = mlirGen(labeledStatementAST->statement, genContext); + // The labelled statement's own scope, made as mlirGenBranchStatement makes one: where a + // `break label` or `continue label` walking outwards stops (scopeExitContinuesOutwards). + // `isLoopBodyScope` is cleared because this context is a copy of the scope around it - one that + // may stand in for a loop's body, and an unlabelled jump would stop here, short of the body's + // own lists. + GenContext labelledContext(genContext); + labelledContext.parentBlockContext = &genContext; + labelledContext.labelledScopeLabel = label; + labelledContext.isLoopBodyScope = false; + + auto usingVars = std::make_unique>(); + labelledContext.usingVars = usingVars.get(); + + auto ownedVars = std::make_unique>(); + labelledContext.ownedVars = ownedVars.get(); + + auto res = mlirGen(labeledStatementAST->statement, labelledContext); + if (mlir::succeeded(res)) + { + res = mlirGenScopeExit(location, DisposeDepth::CurrentScope, {}, &labelledContext); + } builder.setInsertionPointAfter(labelOp); @@ -1337,6 +1370,8 @@ namespace mlirgen // as in mlirGen(Block): this is the block's own scope, so it is where a break or // continue walking outwards stops tryBodyGenContext.isLoopBodyScope = tryGenContext.isLoop; + tryBodyGenContext.loopBodyLabel = tryGenContext.loopLabel; + tryBodyGenContext.labelledScopeLabel.clear(); tryBodyGenContext.isLoop = false; auto usingVars = std::make_unique>(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1ca1b91b2..16b12b23c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -333,6 +333,7 @@ tslang_add_test(NAME test-compile-00-for COMMAND test-runner "${PROJECT_SOURCE_D tslang_add_test(NAME test-compile-00-for-infinite-result COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_infinite_result.ts") tslang_add_test(NAME test-compile-00-break-continue COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00break_continue.ts") tslang_add_test(NAME test-compile-00-label-chain-loop COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00label_chain_loop.ts") +tslang_add_test(NAME test-compile-00-labelled-scope-exit COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00labelled_scope_exit.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") @@ -872,6 +873,7 @@ tslang_add_test(NAME test-jit-00-for COMMAND test-runner -jit "${PROJECT_SOURCE_ tslang_add_test(NAME test-jit-00-for-infinite-result COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_infinite_result.ts") tslang_add_test(NAME test-jit-00-break-continue COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00break_continue.ts") tslang_add_test(NAME test-jit-00-label-chain-loop COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00label_chain_loop.ts") +tslang_add_test(NAME test-jit-00-labelled-scope-exit COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00labelled_scope_exit.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") @@ -1822,6 +1824,7 @@ set(TSLANG_CORPUS 00break_continue_scope_exit.ts 00break_continue.ts 00label_chain_loop.ts + 00labelled_scope_exit.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00labelled_scope_exit.ts b/tslang/test/tester/tests/00labelled_scope_exit.ts new file mode 100644 index 000000000..705e0b55b --- /dev/null +++ b/tslang/test/tester/tests/00labelled_scope_exit.ts @@ -0,0 +1,142 @@ +// A labelled `break` or `continue` owes every scope between itself and the statement it names: +// the disposals a `using` declared, and the references those scopes' locals took. +// +// The walk outwards used to forget the label after its first step, so a labelled jump stopped at +// the first loop body it met: `continue outer` written in an `if` in an inner loop never disposed +// the outer body's `using`. A jump to a labelled block matched nothing at all and ran on to the +// function's scope: it disposed the function's `using` there and then, and under rc released the +// function's locals - the string built in the block was freed before it was returned. +// +// An unlabelled `break` in a labelled block left only the block, not the loop around it, and a +// loop body written without braces had no scope for a jump to stop at. + +let trail = ""; + +class Res { + constructor(public name: string) {} + + [Symbol.dispose]() { + trail += this.name + ";"; + } +} + +function continueOuterFromIf() { + outer: for (let i = 0; i < 2; i++) { + using a = new Res("outer" + i); + for (let j = 0; j < 2; j++) { + using b = new Res("inner" + j); + if (j == 0) { + continue outer; + } + } + } +} + +function breakOuterFromIf() { + outer: while (true) { + using a = new Res("outer"); + while (true) { + using b = new Res("inner"); + if (true) { + break outer; + } + } + } +} + +function breakLabelledBlock() { + using f = new Res("fn"); + a: { + using x = new Res("blk"); + if (x.name == "blk") break a; + trail += "unreached;"; + } + + trail += "after;"; +} + +function continueLabelledBlock() { + let n = 0; + b: { + using y = new Res("blk" + n); + n++; + if (n < 3) continue b; + } + + trail += "n" + n + ";"; +} + +function unlabelledBreakInLabelledBlock() { + for (let i = 0; i < 2; i++) { + using a = new Res("body" + i); + l: { + using c = new Res("blk" + i); + if (i == 0) break; + trail += "unreached;"; + } + + trail += "unreached;"; + } + + trail += "after;"; +} + +function bracelessBody() { + using f = new Res("fn"); + for (let i = 0; i < 2; i++) if (i == 0) continue; + let i = 0; + while (i++ < 3) if (i == 2) break; + trail += "after;"; +} + +function stringInBreakBlock() { + let s = ""; + a: { + s += "x"; + break a; + } + + return s; +} + +function stringInContinueBlock() { + let s = ""; + a: b: { + s += "x"; + if (s.length < 3) continue a; + } + + return s; +} + +function stringInLabelledIf(c: boolean) { + let s = "y"; + a: if (c) { + s += "x"; + break a; + } + + return s; +} + +function check(f: () => void, expected: string, what: string) { + trail = ""; + f(); + print(what + ": " + trail); + assert(trail == expected, what); +} + +function main() { + check(continueOuterFromIf, "inner0;outer0;inner0;outer1;", "continue outer from an if"); + check(breakOuterFromIf, "inner;outer;", "break outer from an if"); + check(breakLabelledBlock, "blk;after;fn;", "break a labelled block"); + check(continueLabelledBlock, "blk0;blk1;blk2;n3;", "continue a labelled block"); + check(unlabelledBreakInLabelledBlock, "blk0;body0;after;", "unlabelled break in a labelled block"); + check(bracelessBody, "after;fn;", "loop bodies without braces"); + + assert(stringInBreakBlock() == "x", "string, break a block"); + assert(stringInContinueBlock() == "xxx", "string, continue a block"); + assert(stringInLabelledIf(true) == "yx", "string, break a labelled if"); + + print("done."); +}