From d0e7d48295d685cc739f871376ca9d1a274c09ee Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 17:09:05 +0100 Subject: [PATCH] A `using` in a switch case is disposed where the switch ends; `break` in an `if` in a case compiles The cases of a switch were generated with a copy of the enclosing context that shared its dispose and release lists, while their names lived only in the switch's symbol table scope. Any `using` in a case crashed the compiler at the end of the enclosing scope, which looked the name up and did not find it. The switch now has a scope of its own (GenContext::isSwitchScope, its own lists), exited at the start of the merge block: a `break` at the end of a case, the last case falling off, and no case matching all come through there. A `using` in a case the path did not run has a dispose guard that is still off, and an owned local there is still null. - `break` and `continue` walk outwards differently now (DisposeDepth::BreakScope): an unlabelled `break` stops at the switch; a `continue` passes through it to the loop. - SwitchOpLowering gives a `break` written inside a case (in an `if`, say) a jump target. It had none, and lowering it crashed. - A case's lone `{ }` block that declares a `using` keeps its scope, so it is disposed before a case falling through runs the next one. - A switch whose cases declare a `using` (or hold a typed catch, on Itanium) is wrapped in a catch-less TryOp as a block is, so an exception after a case's `using` runs its cleanup. - The safe-cast narrowing of a switch on a property cleared insertIntoParentScope on the switch's context instead of its own copy, so later cases' variables were gone by the merge. - The ownership verifier takes a hoisted owned slot (no initializer) as acquired at its stores rather than at the hoisted declaration, where the slot is still null. This also stops it reporting a `break` ahead of a declaration in a try body. Test: 00switch_case_scope_exit.ts (compile + JIT, and in the rc/none corpus). Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/MLIRLogic/MLIRGenContext.h | 4 + tslang/lib/TypeScript/LowerToAffineLoops.cpp | 12 ++ tslang/lib/TypeScript/MLIRGenImpl.h | 42 +++- tslang/lib/TypeScript/MLIRGenStatements.cpp | 106 +++++++++- .../lib/TypeScript/OwnershipVerifierPass.cpp | 21 +- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00switch_case_scope_exit.ts | 196 ++++++++++++++++++ 7 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 tslang/test/tester/tests/00switch_case_scope_exit.ts diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h index 9d28aee3f..873065083 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenContext.h @@ -230,6 +230,10 @@ struct GenContext // at the loop's own scope - and `isLoop` alone reads true for all of them, which is what // made a `break` written inside an `if` skip the lot. bool isLoopBodyScope = false; + // Set on the scope of a `switch` - the one its cases share - and cleared, as isLoopBodyScope + // is, for every scope nested in it. An unlabelled `break` walking outwards stops here; a + // `continue` passes through it to the loop. + bool isSwitchScope = false; // 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 4a516808b..b685d2df3 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -850,6 +850,18 @@ struct SwitchOpLowering : public TsPattern auto *casesRegion = &switchOp.getCasesRegion().front(); + // An unlabelled `break` at the end of a case is already a branch to the merge block + // (mlirGenSwitchCase); one written further in - in an `if`, say - is a BreakOp, and had + // no target: lowering it crashed. It goes past the merge block, whose scope exit the + // `break` has already done on its way out. A labelled one belongs to its LabelOp. + visitBreakContinueInScope(switchOp.getCasesRegion(), mlir::StringAttr(), /*eligibleForUnlabeledBreak*/ true, + /*eligibleForUnlabeledContinue*/ false, [&](Operation *op, bool isBreak) { + if (isBreak) + { + tsContext->jumps[op] = continuation; + } + }); + auto *casesRegionWithMerge = &switchOp.getCasesRegion().back(); for (auto &block : switchOp.getCasesRegion()) { diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index e5d08cca0..f6b854957 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -124,7 +124,10 @@ enum class DisposeDepth { CurrentScope, CurrentScopeKeepAfterUse, + // a `continue`: up to the body of the loop it targets LoopScope, + // a `break`: as LoopScope, except that an unlabelled one stops at a `switch` too + BreakScope, FullStack }; @@ -469,9 +472,15 @@ class MLIRGenImpl // know a `using` was declared, the block's ops are already emitted at the current // insertion point and there is no clean way to wrap them retroactively. bool blockDeclaresUsing(ts::Block blockAST, int skipStatements = 0) + { + return blockDeclaresUsing(blockAST->statements, skipStatements); + } + + // the same over a list of statements: a `switch`'s case clauses have no block of their own + bool blockDeclaresUsing(const NodeArray &statements, int skipStatements = 0) { auto index = 0; - for (auto statement : blockAST->statements) + for (auto statement : statements) { if (index++ < skipStatements) { @@ -504,9 +513,14 @@ class MLIRGenImpl // blockDeclaresUsing for why the check has to run before that. `new X(...)` is the // syntactic proxy, and it costs nothing to check. bool blockUsingInitializersAreAllNewExpr(ts::Block blockAST, int skipStatements = 0) + { + return blockUsingInitializersAreAllNewExpr(blockAST->statements, skipStatements); + } + + bool blockUsingInitializersAreAllNewExpr(const NodeArray &statements, int skipStatements = 0) { auto index = 0; - for (auto statement : blockAST->statements) + for (auto statement : statements) { if (index++ < skipStatements) { @@ -558,6 +572,11 @@ class MLIRGenImpl // A block with nothing to release costs nothing: mlirGenScopeExit writes no operations into // the cleanup region, TryOpLowering sees an empty one, and the wrapping leaves no trace. bool blockHasTypedCatch(ts::Block blockAST, int skipStatements = 0) + { + return blockHasTypedCatch(blockAST->statements, skipStatements); + } + + bool blockHasTypedCatch(const NodeArray &statements, int skipStatements = 0) { auto found = false; ts::FilterVisitorSkipFuncsAST visitor(SyntaxKind::CatchClause, [&](CatchClause catchClauseNode) { @@ -568,7 +587,7 @@ class MLIRGenImpl }); auto index = 0; - for (auto statement : blockAST->statements) + for (auto statement : statements) { if (index++ < skipStatements) { @@ -726,7 +745,8 @@ class MLIRGenImpl // Whether a scope exit that has just finished with this scope still owes the scopes outside // it. `FullStack` - a `return` - owes all of them. A `break` or `continue` owes every scope - // up to and including the body scope of the loop it targets. + // up to and including the body scope of the loop it targets - or, for an unlabelled + // `break` in a case, the scope of the switch. // // The loop test belongs here, and on `isLoopBodyScope`. Written as // `disposeDepth == LoopScope && genContext->isLoop && genContext->loopLabel != loopLabel` @@ -743,7 +763,7 @@ class MLIRGenImpl return true; } - if (disposeDepth != DisposeDepth::LoopScope) + if (disposeDepth != DisposeDepth::LoopScope && disposeDepth != DisposeDepth::BreakScope) { return false; } @@ -758,9 +778,15 @@ class MLIRGenImpl // `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. + // + // An unlabelled `break` belongs to a `switch` as much as to a loop, and stops at the + // nearer of the two; a `continue` passes through a switch. Without the switch's scope the + // walk took a `break` out of a case on to the scopes around the switch - releasing their + // locals - and found nothing of the case's own. if (loopLabel.empty()) { - return !genContext->isLoopBodyScope; + return !genContext->isLoopBodyScope && + !(disposeDepth == DisposeDepth::BreakScope && genContext->isSwitchScope); } auto isTarget = (genContext->isLoopBodyScope && genContext->loopBodyLabel == loopLabel) || @@ -4571,10 +4597,12 @@ class MLIRGenImpl auto statements = caseBlock->statements; // inline block // TODO: should I inline block as it is isolator of local vars? + // Not one that declares a `using`: that is disposed where the block ends, before a case + // falling through runs the next one, so the block keeps its scope (mlirGen(Block)). if (statements.size() == 1) { auto firstStatement = statements.front(); - if ((SyntaxKind)firstStatement == SyntaxKind::Block) + if ((SyntaxKind)firstStatement == SyntaxKind::Block && !blockDeclaresUsing(firstStatement.as())) { statements = statements.front().as()->statements; } diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 3ad649cd2..a52a8758f 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -187,6 +187,8 @@ namespace mlirgen genContextUsing.loopBodyLabel = genContext.loopLabel; // a scope of its own, not the labelled statement's: see labelledContext in mlirGen(LabeledStatement) genContextUsing.labelledScopeLabel.clear(); + // nor the switch's, when this block is a case's + genContextUsing.isSwitchScope = false; genContextUsing.isLoop = false; DITableScopeT debugBlockScope(debugScope); @@ -261,6 +263,7 @@ namespace mlirgen tryBodyGenContext.isLoopBodyScope = tryGenContext.isLoop; tryBodyGenContext.loopBodyLabel = tryGenContext.loopLabel; tryBodyGenContext.labelledScopeLabel.clear(); + tryBodyGenContext.isSwitchScope = false; tryBodyGenContext.isLoop = false; auto usingVars = std::make_unique>(); @@ -686,6 +689,7 @@ namespace mlirgen branchContext.loopBodyLabel = genContext.loopLabel; branchContext.isLoop = false; branchContext.labelledScopeLabel.clear(); + branchContext.isSwitchScope = false; auto usingVars = std::make_unique>(); branchContext.usingVars = usingVars.get(); @@ -1071,6 +1075,7 @@ namespace mlirgen labelledContext.parentBlockContext = &genContext; labelledContext.labelledScopeLabel = label; labelledContext.isLoopBodyScope = false; + labelledContext.isSwitchScope = false; auto usingVars = std::make_unique>(); labelledContext.usingVars = usingVars.get(); @@ -1129,7 +1134,7 @@ namespace mlirgen } label = resolveLoopLabelAlias(label); - EXIT_IF_FAILED(mlirGenScopeExit(location, DisposeDepth::LoopScope, label, &genContext)); + EXIT_IF_FAILED(mlirGenScopeExit(location, DisposeDepth::BreakScope, label, &genContext)); builder.create(location, builder.getStringAttr(label)); return mlir::success(); @@ -1146,19 +1151,66 @@ namespace mlirgen EXIT_IF_FAILED_OR_NO_VALUE(result) auto switchValue = V(result); + auto &clauses = switchStatementAST->caseBlock->clauses; + + // The cases' scope needs an unwind cleanup on the same terms a block does (mlirGen(Block)): + // without one, an exception after a case's `using` was stored reached the cleanup of an + // enclosing scope, which knows nothing of the switch's locals. + auto declaresUsing = false; + auto usingIsDisposable = true; + auto hasTypedCatch = false; + for (auto &clause : clauses) + { + declaresUsing |= blockDeclaresUsing(clause->statements); + usingIsDisposable &= blockUsingInitializersAreAllNewExpr(clause->statements); + hasTypedCatch |= !compileOptions.isWindows && blockHasTypedCatch(clause->statements); + } + + mlir_ts::TryOp tryOp; + if (usingIsDisposable && (declaresUsing || hasTypedCatch) && !blockIsInsideCatchOrFinally()) + { + mlir_ts::FuncOp funcOp = genContext.funcOp; + funcOp.setPersonalityAttr(builder.getBoolAttr(true)); + + tryOp = builder.create(location); + + SmallVector types; + builder.createBlock(&tryOp.getBody(), {}, types); + builder.createBlock(&tryOp.getCleanup(), {}, types); + builder.createBlock(&tryOp.getCatches(), {}, types); + builder.createBlock(&tryOp.getFinally(), {}, types); + + builder.setInsertionPointToStart(&tryOp.getBody().front()); + } + auto switchOp = builder.create(location, switchValue); GenContext switchGenContext(genContext); switchGenContext.allocateVarsOutsideOfOperation = true; - switchGenContext.currentOperation = switchOp; + // in front of the TryOp when there is one, so that its cleanup region sees the storage + switchGenContext.currentOperation = tryOp ? tryOp.getOperation() : switchOp.getOperation(); switchGenContext.insertIntoParentScope = true; + // The scope the cases share, made as mlirGen(Block) makes one. The cases used to be generated + // with a copy of the enclosing context, sharing its lists: a `using` in a case was disposed + // at the end of the enclosing scope, by a name the switch's symbol table scope no longer + // had, which crashed the compiler. + switchGenContext.parentBlockContext = &genContext; + switchGenContext.isSwitchScope = true; + switchGenContext.isLoopBodyScope = false; + switchGenContext.labelledScopeLabel.clear(); + switchGenContext.isLoop = false; + + auto usingVars = std::make_unique>(); + switchGenContext.usingVars = usingVars.get(); + + auto ownedVars = std::make_unique>(); + switchGenContext.ownedVars = ownedVars.get(); + // add merge block switchOp.addMergeBlock(); auto *mergeBlock = switchOp.getMergeBlock(); - auto &clauses = switchStatementAST->caseBlock->clauses; - SmallVector pendingConditions; SmallVector pendingBranches; mlir::Operation *previousConditionOrFirstBranchOp = nullptr; @@ -1174,14 +1226,18 @@ namespace mlirgen auto name = propertyAccessExpressionOp->name; safeCastLogic = [=, &switchGenContext](Expression caseExpr, mlir::Value constVal) { + // The narrowed binding is the case's own. This used to clear the flag on + // switchGenContext itself, so every later case declared its variables in its own + // symbol table scope too - gone by the time the switch's scope exit looks a + // `using` up by name. GenContext safeCastGenContext(switchGenContext); - switchGenContext.insertIntoParentScope = false; + safeCastGenContext.insertIntoParentScope = false; // Safe Cast - if (mlir::failed(checkSafeCastTypeOf(switchExpr, caseExpr, false, nullptr, switchGenContext))) + if (mlir::failed(checkSafeCastTypeOf(switchExpr, caseExpr, false, nullptr, safeCastGenContext))) { checkSafeCastPropertyAccessLogic(caseExpr, objAccessExpression, typeOfObject, name, constVal, - false, nullptr, switchGenContext); + false, nullptr, safeCastGenContext); } }; } @@ -1201,6 +1257,41 @@ namespace mlirgen } } + // Every way out of a case but a jump ends in the merge block - a `break` at the end of a + // case, the last case falling off its end, and no case matching. A `break` written + // further in, a `continue` or a `return` has done its own share of this on the way out + // (scopeExitContinuesOutwards) and does not come here. A `using` in a case the path did + // not run is skipped: its storage is hoisted in front of the switch, and so is a guard + // that only the initializer's store arms (createLocalVariable). An owned local there is + // released as null, which is nothing to do (VariableOpLowering). + { + mlir::OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPoint(mergeBlock->getTerminator()); + EXIT_IF_FAILED(mlirGenScopeExit(location, + tryOp ? DisposeDepth::CurrentScopeKeepAfterUse : DisposeDepth::CurrentScope, + {}, &switchGenContext)); + } + + if (tryOp) + { + // as in mlirGenBlockWithUnwindCleanup: the cleanup does what the merge block does, + // reached only from the unwind edge; no catch clause, no finally + builder.setInsertionPointAfter(switchOp); + builder.create(location); + + builder.setInsertionPointToStart(&tryOp.getCleanup().front()); + EXIT_IF_FAILED(mlirGenScopeExit(location, DisposeDepth::CurrentScope, {}, &switchGenContext)); + builder.create(location); + + builder.setInsertionPointToStart(&tryOp.getCatches().front()); + builder.create(location); + + builder.setInsertionPointToStart(&tryOp.getFinally().front()); + builder.create(location); + + builder.setInsertionPointAfter(tryOp); + } + LLVM_DEBUG(llvm::dbgs() << "\n!! SWITCH: " << switchOp << "\n"); return mlir::success(); @@ -1372,6 +1463,7 @@ namespace mlirgen tryBodyGenContext.isLoopBodyScope = tryGenContext.isLoop; tryBodyGenContext.loopBodyLabel = tryGenContext.loopLabel; tryBodyGenContext.labelledScopeLabel.clear(); + tryBodyGenContext.isSwitchScope = false; tryBodyGenContext.isLoop = false; auto usingVars = std::make_unique>(); diff --git a/tslang/lib/TypeScript/OwnershipVerifierPass.cpp b/tslang/lib/TypeScript/OwnershipVerifierPass.cpp index 8eb433bac..2a4db364b 100644 --- a/tslang/lib/TypeScript/OwnershipVerifierPass.cpp +++ b/tslang/lib/TypeScript/OwnershipVerifierPass.cpp @@ -66,8 +66,27 @@ class OwnershipVerifierPass : public mlir::PassWrapperhasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) { - acquisitions.emplace_back(varOp.getResult(), varOp.getOperation()); acquiredSlots.insert(varOp.getResult()); + if (varOp.getInitializer()) + { + acquisitions.emplace_back(varOp.getResult(), varOp.getOperation()); + } + else + { + // Storage hoisted out in front of a TryOp or a SwitchOp: the declaration + // is the store that stayed behind, and the slot is null until it runs + // (VariableOpLowering) - a release has nothing to give back on a path + // that never reaches it. Blaming the hoisted slot itself reported a + // `break` written ahead of the declaration, in a case or a try body. + for (auto *user : varOp.getResult().getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == varOp.getResult()) + { + acquisitions.emplace_back(varOp.getResult(), storeOp.getOperation()); + } + } + } } } }); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 16b12b23c..a26bc0ba6 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -334,6 +334,7 @@ tslang_add_test(NAME test-compile-00-for-infinite-result COMMAND test-runner "${ 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-switch-case-scope-exit COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_case_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") @@ -874,6 +875,7 @@ tslang_add_test(NAME test-jit-00-for-infinite-result COMMAND test-runner -jit "$ 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-switch-case-scope-exit COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00switch_case_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") @@ -1825,6 +1827,7 @@ set(TSLANG_CORPUS 00break_continue.ts 00label_chain_loop.ts 00labelled_scope_exit.ts + 00switch_case_scope_exit.ts 00capture_in_new_arguments.ts 00cast_to_f32.ts 00catch_untyped_value.ts diff --git a/tslang/test/tester/tests/00switch_case_scope_exit.ts b/tslang/test/tester/tests/00switch_case_scope_exit.ts new file mode 100644 index 000000000..ea5aa6206 --- /dev/null +++ b/tslang/test/tester/tests/00switch_case_scope_exit.ts @@ -0,0 +1,196 @@ +// The cases of a `switch` share a scope of their own: a `using` in a case is disposed where the +// switch ends - by a `break`, by the last case falling off its end, or by no case matching - and +// a jump out of a case disposes it on the way. +// +// The cases used to be generated in the enclosing scope's lists, while their names lived only in +// the switch's symbol table scope: any `using` in a case crashed the compiler when the enclosing +// scope ended and looked the name up. A `break` written inside an `if` in a case had no target to +// lower to and crashed the compiler too, and on its way out it released the locals of every scope +// around the switch. + +let trail = ""; + +class Res { + constructor(public name: string) {} + + [Symbol.dispose]() { + trail += this.name + ";"; + } +} + +function caseWithBreak(i: number) { + switch (i) { + case 0: + using a = new Res("a"); + trail += "body0;"; + break; + case 1: + trail += "body1;"; + break; + } + + trail += "after;"; +} + +function fallThrough(i: number) { + switch (i) { + case 0: + using a = new Res("a"); + trail += "body0;"; + case 1: + trail += "body1;"; + break; + default: + trail += "default;"; + } + + trail += "after;"; +} + +function bracedCases(i: number) { + switch (i) { + case 0: { + using a = new Res("a"); + trail += "body0;"; + } + case 1: { + using b = new Res("b"); + trail += "body1;"; + } + } + + trail += "after;"; +} + +function breakFromIf(i: number) { + using f = new Res("fn"); + switch (i) { + case 0: + using a = new Res("a"); + if (a.name == "a") break; + trail += "unreached;"; + case 1: + trail += "body1;"; + } + + trail += "after;"; +} + +function inLoop() { + for (let j = 0; j < 4; j++) { + using l = new Res("loop" + j); + switch (j) { + case 0: + using c = new Res("case" + j); + continue; + case 1: + using d = new Res("case" + j); + break; + default: + using e = new Res("default" + j); + if (j == 2) break; + trail += "default;"; + } + + trail += "end" + j + ";"; + } + + trail += "after;"; +} + +class Shape { + constructor(public kind: string) {} +} + +function onProperty(s: Shape) { + switch (s.kind) { + case "circle": + trail += "circle;"; + break; + case "square": + using q = new Res("square"); + trail += "body;"; + break; + } + + trail += "after;"; +} + +// strings: under rc a case's locals are released by the switch, and a `break` from an `if` +// no longer releases the locals around the switch +function stringsInCases(i: number, k: string) { + let outer = "o" + k; + switch (i) { + case 0: + let s = "zero" + k; + if (s.length > 4) break; + outer += s; + case 1: + let t = "one" + k; + outer += t; + break; + default: + let u = "def" + k; + outer += u; + } + + return outer; +} + +function stringsInLoop() { + let acc = ""; + for (let j = 0; j < 4; j++) { + let v = "v" + j; + switch (j) { + case 0: + let w = v + "w"; + acc += w; + continue; + case 1: + acc += v; + break; + default: + let x = v + "x"; + if (j == 2) { + acc += x; + break; + } + + acc += "!"; + } + + acc += ";"; + } + + return acc; +} + +function check(f: () => void, expected: string, what: string) { + trail = ""; + f(); + print(what + ": " + trail); + assert(trail == expected, what); +} + +function main() { + check(() => caseWithBreak(0), "body0;a;after;", "using + break"); + check(() => caseWithBreak(1), "body1;after;", "using in a case not taken"); + check(() => caseWithBreak(5), "after;", "no case matched"); + check(() => fallThrough(0), "body0;body1;a;after;", "falling through"); + check(() => fallThrough(7), "default;after;", "default"); + check(() => bracedCases(0), "body0;a;body1;b;after;", "braced cases falling through"); + check(() => breakFromIf(0), "a;after;fn;", "break from an if in a case"); + check(() => breakFromIf(1), "body1;after;fn;", "break from an if, case not taken"); + check(inLoop, "case0;loop0;case1;end1;loop1;default2;end2;loop2;default;default3;end3;loop3;after;", + "switch in a loop body with a using"); + check(() => onProperty(new Shape("square")), "body;square;after;", "switch on a property"); + check(() => onProperty(new Shape("circle")), "circle;after;", "switch on a property, other case"); + + assert(stringsInCases(0, "") == "ozeroone", "strings, fall through"); + assert(stringsInCases(0, "long") == "olong", "strings, break from an if"); + assert(stringsInCases(1, "a") == "oaonea", "strings, case 1"); + assert(stringsInCases(9, "b") == "obdefb", "strings, default"); + assert(stringsInLoop() == "v0wv1;v2x;!;", "strings in a loop"); + + print("done."); +}