From d214e589eb16c5f16cfcb8a859c97ac3570616e2 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 5 Oct 2026 16:32:28 +0100 Subject: [PATCH 1/2] An object literal assigned to a narrowed variable is typed as declared After `s.kind === "sq"` narrows `s: Sq | Ci` to `Sq`, `s = { kind: "ci", r: 1 }` was generated with the narrowed `Sq` as its receiver type: the literal became a `{ kind: "sq", r }` and nothing was stored, with no error. An `if` kept "sq", and a `while` that wrote the variable to end the loop never ended (or crashed the compiler). mlirGenSaveLogic now takes the receiver type of a narrowed variable (a SafeCastOp) from the value it narrows, so the literal is typed as the variable is declared. The store already saw through the SafeCastOp. A variable on the right, or a literal of the narrowed member, was already right. 00narrowed_assign_other_member.ts: the other member assigned in an if and in a while (which must end), the same member, and the other member's field read after the if. On main the first assertion fails. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 11 +++- tslang/test/tester/CMakeLists.txt | 3 ++ .../tests/00narrowed_assign_other_member.ts | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tslang/test/tester/tests/00narrowed_assign_other_member.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 3498e57a6..8e6b9693f 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -6074,7 +6074,16 @@ class MLIRGenImpl rightExprGenContext.receiverFuncType = leftExpressionValue.getType(); } - rightExprGenContext.receiverType = leftExpressionValue.getType(); + // a narrowed variable (addSafeCastStatement) takes a value of the type it is declared with: + // after `s.kind === "sq"`, `s = { kind: "ci", r: 1 }` is an `Sq | Ci`, and typed as the narrowed + // `Sq` it became a `{ kind: "sq", r }` that was never stored + auto receiverType = leftExpressionValue.getType(); + if (auto safeCastOp = leftExpressionValue.getDefiningOp()) + { + receiverType = safeCastOp.getValue().getType(); + } + + rightExprGenContext.receiverType = receiverType; auto result2 = mlirGen(rightExpression, rightExprGenContext); EXIT_IF_FAILED_OR_NO_VALUE(result2) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 3d3a25caf..4f2f2e407 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -357,6 +357,7 @@ tslang_add_test(NAME test-compile-00-for-await-sequential COMMAND test-runner "$ tslang_add_test(NAME test-compile-00-for-await-control COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-compile-00-for-optional-class-condition COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") tslang_add_test(NAME test-compile-00-string-empty-falsy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") +tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-compile-00-string-view-copy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -871,6 +872,7 @@ tslang_add_test(NAME test-jit-00-for-await-sequential COMMAND test-runner -jit " tslang_add_test(NAME test-jit-00-for-await-control COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-jit-00-for-optional-class-condition COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") tslang_add_test(NAME test-jit-00-string-empty-falsy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") +tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") tslang_add_test(NAME test-jit-00-string-view-copy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_view_copy.ts") @@ -1714,6 +1716,7 @@ set(TSLANG_CORPUS 00for_await_control.ts 00for_optional_class_condition.ts 00string_empty_falsy.ts + 00narrowed_assign_other_member.ts 00const_record_owned_fields.ts 00array_length_valid.ts 00string_view_copy.ts diff --git a/tslang/test/tester/tests/00narrowed_assign_other_member.ts b/tslang/test/tester/tests/00narrowed_assign_other_member.ts new file mode 100644 index 000000000..ccb766b82 --- /dev/null +++ b/tslang/test/tester/tests/00narrowed_assign_other_member.ts @@ -0,0 +1,50 @@ +// an object literal assigned to a variable narrowed by a discriminant (`s.kind === "sq"`) was typed as the +// narrowed member, so `s = { kind: "ci", r: 1 }` made a `{ kind: "sq", r }` and stored nothing: an `if` kept +// "sq", and a `while` never ended (or crashed the compiler) +type Sq = { kind: "sq"; side: number }; +type Ci = { kind: "ci"; r: number }; + +function inIf(s: Sq | Ci) { + if (s.kind === "sq") { + s = { kind: "ci", r: 1 }; + } + + return s.kind; +} + +function inWhile(s: Sq | Ci) { + let n = 0; + while (s.kind === "sq") { + n++; + s = { kind: "ci", r: 2 }; + } + + return n; +} + +function sameMember(s: Sq | Ci) { + if (s.kind === "sq") { + s = { kind: "sq", side: 9 }; + } + + return s.kind === "sq" ? s.side : -1; +} + +function readAfter(s: Sq | Ci) { + if (s.kind === "sq") { + s = { kind: "ci", r: 7 }; + } + + return s.kind === "ci" ? s.r : -1; +} + +function main() { + assert(inIf({ kind: "sq", side: 4 }) == "ci", "if: assigned the other member"); + assert(inIf({ kind: "ci", r: 3 }) == "ci", "if: not taken"); + assert(inWhile({ kind: "sq", side: 4 }) == 1, "while: assigned the other member ends the loop"); + assert(inWhile({ kind: "ci", r: 3 }) == 0, "while: not entered"); + assert(sameMember({ kind: "sq", side: 4 }) == 9, "assigned the same member"); + assert(readAfter({ kind: "sq", side: 4 }) == 7, "the other member's field after the if"); + + print("done."); +} From fa757a4450ee76763573426c8772b8425c817a0e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 5 Oct 2026 16:33:36 +0100 Subject: [PATCH 2/2] A for body sees the variable its condition narrows mlirGen(WhileStatement) narrows the tested variable for its body (checkSafeCast); mlirGen(ForStatement) did not, so `for (; typeof v === "string"; v = 1) { v.length }` failed with "Can't resolve property 'length' of type number", where the same `while` compiled. A type guard (`isStr(v)`) failed the same way. The body is now generated in a scope of its own that narrows as a while body does. The scope ends before the incrementor, which assigns the variable as it is declared (`node = node.next`). The narrowing reads the condition before its boolean cast, as mlirGen(IfStatement) does, so a type guard's predicate still narrows. A condition known to be false narrows nothing, as in an if; the body is still generated, so a `var` in it is declared. Writing the narrowed variable in the body relies on the previous commit: `for (; s.kind === "sq"; ) { s = { kind: "ci", r: 1 } }` works on main because the for did not narrow, and would otherwise have never ended. 00for_condition_narrowing.ts: typeof with the incrementor or the body writing the variable, a type guard, instanceof, a discriminant with the body writing the other member, an optional list walk (with continue, and nested), a condition known to be false, and a var in an unbraced body. On main it fails to compile. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenStatements.cpp | 21 ++- tslang/test/tester/CMakeLists.txt | 3 + .../tester/tests/00for_condition_narrowing.ts | 137 ++++++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00for_condition_narrowing.ts diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index ca05ff994..c77e5015b 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -838,6 +838,11 @@ namespace mlirgen } auto conditionValue = V(result); + // the condition as written: a type guard's predicate is what narrows, and the boolean cast loses it + auto testedValue = conditionValue; + // a condition known to be false narrows nothing: as in mlirGen(IfStatement), narrowing would cast the + // tested value to a type it cannot have + auto narrowBody = conditionValue && getStaticBoolean(conditionValue).value_or(true); if (conditionValue) { // a truthiness test (`for (let n = head; n; n = n.next)`) is made a boolean as in mlirGen(WhileStatement) @@ -859,8 +864,20 @@ namespace mlirgen // async.execute and waits for it, so one iteration runs at a time (#498). Generated inside an // async.execute of its own, a `break` or `continue` in it had no loop in its region, and a // nested `for await` crashed the compiler (#502). - auto bodyResult = mlirGen(forStatementAST->statement, loopGenContext); - EXIT_IF_FAILED(bodyResult) + { + // the body sees the variable the condition narrows, as a `while` body does + // (`for (; typeof v === "string"; v = 1) { v.length }`); the scope ends before the incrementor, + // which assigns the variable as it is declared + SymbolTableScopeT varScopeBody(symbolTable); + SafeTypesMapScopeT safeTypesMapScope(safeTypesMap); + if (narrowBody) + { + checkSafeCast(forStatementAST->condition, testedValue, nullptr, loopGenContext); + } + + auto bodyResult = mlirGen(forStatementAST->statement, loopGenContext); + EXIT_IF_FAILED(bodyResult) + } builder.create(location); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4f2f2e407..d0f0a113b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -356,6 +356,7 @@ tslang_add_test(NAME test-compile-00-await-void COMMAND test-runner "${PROJECT_S tslang_add_test(NAME test-compile-00-for-await-sequential COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_sequential.ts") tslang_add_test(NAME test-compile-00-for-await-control COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-compile-00-for-optional-class-condition COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") +tslang_add_test(NAME test-compile-00-for-condition-narrowing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_condition_narrowing.ts") tslang_add_test(NAME test-compile-00-string-empty-falsy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") @@ -871,6 +872,7 @@ tslang_add_test(NAME test-jit-00-await-void COMMAND test-runner -jit "${PROJECT_ tslang_add_test(NAME test-jit-00-for-await-sequential COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_sequential.ts") tslang_add_test(NAME test-jit-00-for-await-control COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_await_control.ts") tslang_add_test(NAME test-jit-00-for-optional-class-condition COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_optional_class_condition.ts") +tslang_add_test(NAME test-jit-00-for-condition-narrowing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00for_condition_narrowing.ts") tslang_add_test(NAME test-jit-00-string-empty-falsy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") @@ -1715,6 +1717,7 @@ set(TSLANG_CORPUS 00for_await_sequential.ts 00for_await_control.ts 00for_optional_class_condition.ts + 00for_condition_narrowing.ts 00string_empty_falsy.ts 00narrowed_assign_other_member.ts 00const_record_owned_fields.ts diff --git a/tslang/test/tester/tests/00for_condition_narrowing.ts b/tslang/test/tester/tests/00for_condition_narrowing.ts new file mode 100644 index 000000000..03c0b0d7b --- /dev/null +++ b/tslang/test/tester/tests/00for_condition_narrowing.ts @@ -0,0 +1,137 @@ +// a `for` body sees the variable its condition narrows, as a `while` body does: `typeof v === "string"` made +// `v.length` "Can't resolve property 'length' of type number". The incrementor still assigns the variable as +// it is declared. +class Node { + constructor(public value: number, public next: Node | undefined) {} +} + +class A { + constructor(public a: number) {} + onlyA() { + return this.a; + } +} + +class B { + constructor(public b: number) {} +} + +type Sq = { kind: "sq"; side: number }; +type Ci = { kind: "ci"; r: number }; + +function isStr(v: string | number): v is string { + return typeof v === "string"; +} + +function typeofIncrementor(v: string | number) { + let n = 0; + for (; typeof v === "string"; v = 1) { + n += v.length; + } + + return n; +} + +function typeofBodyWrite(v: string | number) { + let n = 0; + for (; typeof v === "string"; ) { + n += v.length; + v = 1; + } + + return n; +} + +function typeGuard(v: string | number) { + let n = 0; + for (; isStr(v); v = 1) { + n += v.length; + } + + return n; +} + +function instanceOf(x: A | B) { + let n = 0; + // a local: reassigning the parameter itself in this loop crashes under -mm=rc, narrowed or not (#512) + for (let y = x; y instanceof A; y = new B(1)) { + n += y.onlyA(); + } + + return n; +} + +function discriminant(s: Sq | Ci) { + let n = 0; + for (; s.kind === "sq"; ) { + n += s.side; + s = { kind: "ci", r: 1 }; + } + + return n; +} + +function listSum(head: Node | undefined) { + let sum = 0; + for (let node = head; node; node = node.next) { + const n: Node = node; + sum += n.value; + } + + return sum; +} + +function listSkip(head: Node | undefined) { + let sum = 0; + for (let node = head; node; node = node.next) { + if (node.value == 2) { + continue; + } + + sum += node.value; + } + + return sum; +} + +function nested(head: Node | undefined) { + let sum = 0; + for (let a = head; a; a = a.next) { + for (let b = head; b; b = b.next) { + sum += a.value * b.value; + } + } + + return sum; +} + +function staticFalse() { + const a = [1, 2]; + let n = 0; + for (; typeof a === "string"; ) { + n += a.length; + } + + return n; +} + +function main() { + assert(typeofIncrementor("abcd") == 4, "typeof, incrementor writes"); + assert(typeofIncrementor(7) == 0, "typeof, not entered"); + assert(typeofBodyWrite("abcde") == 5, "typeof, body writes"); + assert(typeGuard("abc") == 3, "type guard"); + assert(instanceOf(new A(5)) == 5, "instanceof"); + assert(discriminant({ kind: "sq", side: 4 }) == 4, "discriminant, body writes the other member"); + + let head: Node | undefined = new Node(1, new Node(2, new Node(3, undefined))); + assert(listSum(head) == 6, "optional list walk"); + assert(listSum(undefined) == 0, "empty list"); + assert(listSkip(head) == 4, "continue runs the incrementor"); + assert(nested(head) == 36, "nested loops"); + assert(staticFalse() == 0, "a condition known to be false"); + + for (let i = 0; i < 1; i++) var x = 5; + assert(x == 5, "var in an unbraced body"); + + print("done."); +}