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/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 3d3a25caf..d0f0a113b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -356,7 +356,9 @@ 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") 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") @@ -870,7 +872,9 @@ 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") 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") @@ -1713,7 +1717,9 @@ 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 00array_length_valid.ts 00string_view_copy.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."); +} 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."); +}