From 1ea239feb1b6dd80099ce9d3dc49a46a9a7cd234 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 17 Oct 2026 14:53:23 +0100 Subject: [PATCH] A break or continue naming a label no statement around it has is an error `continue nolabel;` and `break nolabel;` inside a loop compiled with no diagnostic: the jump op carried a label nothing matched. MLIRGen now keeps the labels of the statements around the code it generates (a function body starts with none, since a jump can't leave a function) and rejects a label that isn't one of them. A continue to the label of a block, which starts the block again (tests/00break_continue.ts), stays: an extension TypeScript doesn't have. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenFunctions.cpp | 7 +++++++ tslang/lib/TypeScript/MLIRGenImpl.h | 5 +++++ tslang/lib/TypeScript/MLIRGenStatements.cpp | 17 +++++++++++++++++ tslang/test/tester/CMakeLists.txt | 17 +++++++++++++++++ .../tester/diagnostic-location/break_label.ts | 12 ++++++++++++ .../diagnostic-location/continue_label.ts | 10 ++++++++++ 6 files changed, 68 insertions(+) create mode 100644 tslang/test/tester/diagnostic-location/break_label.ts create mode 100644 tslang/test/tester/diagnostic-location/continue_label.ts diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index ed55967de..50fc9f78f 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -2,6 +2,8 @@ #include "MLIRGenImpl.h" +#include "llvm/ADT/ScopeExit.h" + namespace typescript { namespace mlirgen @@ -1633,6 +1635,11 @@ namespace mlirgen SymbolTableScopeT varScope(symbolTable); BoundRefCacheScopeT boundRefCacheScope(boundRefMaterializedCache); + // the labels around a function are not the body's to jump to + auto outerLabels = std::move(activeLabels); + activeLabels.clear(); + auto restoreLabels = llvm::make_scope_exit([&]() { activeLabels = std::move(outerLabels); }); + auto location = loc(functionLikeDeclarationBaseAST); // Debug Info diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b62a7b92e..140bb2f89 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -14384,6 +14384,11 @@ class MLIRGenImpl Stages stage; + // The labels of the statements around the code being generated, innermost last: a labeled + // `break` or `continue` has to name one of them. A function body starts with none: a jump + // can't leave a function. + SmallVector activeLabels; + private: std::string label; mlir::Block* tempEntryBlock; diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 6eae033d5..d070e5e60 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -6,6 +6,8 @@ #include "mlir/Dialect/Async/IR/Async.h" +#include "llvm/ADT/ScopeExit.h" + #undef DEBUG_TYPE #define DEBUG_TYPE "mlir" @@ -996,6 +998,9 @@ namespace mlirgen kind == SyntaxKind::ForStatement || kind == SyntaxKind::ForInStatement || kind == SyntaxKind::ForOfStatement; + activeLabels.push_back(label); + auto popLabel = llvm::make_scope_exit([&]() { activeLabels.pop_back(); }); + if (noLabelOp) { return mlirGen(labeledStatementAST->statement, genContext); @@ -1029,6 +1034,13 @@ namespace mlirgen auto location = loc(continueStatementAST); auto label = MLIRHelper::getName(continueStatementAST->label); + // a `continue` to the label of a block (not a loop) starts the block again: an extension + // TypeScript doesn't have (tests/00break_continue.ts) + if (!label.empty() && !llvm::is_contained(activeLabels, label)) + { + emitError(location, "A 'continue' statement can only jump to a label of an enclosing statement"); + return mlir::failure(); + } EXIT_IF_FAILED(mlirGenScopeExit(location, DisposeDepth::LoopScope, label, &genContext)); @@ -1041,6 +1053,11 @@ namespace mlirgen auto location = loc(breakStatementAST); auto label = MLIRHelper::getName(breakStatementAST->label); + if (!label.empty() && !llvm::is_contained(activeLabels, label)) + { + emitError(location, "A 'break' statement can only jump to a label of an enclosing statement"); + return mlir::failure(); + } EXIT_IF_FAILED(mlirGenScopeExit(location, DisposeDepth::LoopScope, label, &genContext)); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e92db2eb9..b4f116a6d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2698,6 +2698,23 @@ foreach(call_arity_case ${call_arity_cases}) FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") endforeach() +# A `continue` or `break` naming a label that no statement around it has is an error (a label +# outside the function doesn't count); it compiled to a jump with nowhere to go. +add_test(NAME test-compile-diagnostic-location-continue-label + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/diagnostic-location/continue_label.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/diagnostic-location-continue-label.obj") +set_tests_properties(test-compile-diagnostic-location-continue-label + PROPERTIES PASS_REGULAR_EXPRESSION "continue_label\\.ts:7:13: error: A 'continue' statement can only jump to a label of an enclosing statement" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|continue_label\\.ts:6:") +add_test(NAME test-compile-diagnostic-location-break-label + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/diagnostic-location/break_label.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/diagnostic-location-break-label.obj") +set_tests_properties(test-compile-diagnostic-location-break-label + PROPERTIES PASS_REGULAR_EXPRESSION "break_label\\.ts:7:17: error: A 'break' statement can only jump to a label of an enclosing statement" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|break_label\\.ts:10:") + # Storing a fraction into a variable whose type was inferred from an integer literal (`let s = 0` # is an s32) drops it, and is warned about; the inference itself stays. foreach(truncation_case "global|3:1" "local|5:9") diff --git a/tslang/test/tester/diagnostic-location/break_label.ts b/tslang/test/tester/diagnostic-location/break_label.ts new file mode 100644 index 000000000..0163f31e5 --- /dev/null +++ b/tslang/test/tester/diagnostic-location/break_label.ts @@ -0,0 +1,12 @@ +// A continue or break naming a label no statement around it has compiled with no error. + +function main() { + outer: for (let i = 0; i < 3; i++) { + const f = () => { + for (;;) { + break outer; + } + }; + break outer; + } +} diff --git a/tslang/test/tester/diagnostic-location/continue_label.ts b/tslang/test/tester/diagnostic-location/continue_label.ts new file mode 100644 index 000000000..d06b31fdb --- /dev/null +++ b/tslang/test/tester/diagnostic-location/continue_label.ts @@ -0,0 +1,10 @@ +// A continue or break naming a label no statement around it has compiled with no error. + +function main() { + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j == 1) continue outer; + continue nolabel; + } + } +}