From cfc4b07749986e539910d4bea455cf67c4c67ebf Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:04:18 +0000 Subject: [PATCH 1/9] own: a value pushed only where it is not null, then used; a union of string literals read as a string (TSLANG 67, 68) Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 9 ++ .../OwnershipSignatureFieldLocals.cpp | 98 +++++++++++++++++++ tslang/test/tester/CMakeLists.txt | 6 +- .../own/own_err_pushed_or_null_popped.ts | 24 +++++ tslang/test/tester/tests/00tslang_issue_67.ts | 76 ++++++++++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_err_pushed_or_null_popped.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_67.ts diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index d346397ea..acd156195 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -366,6 +366,8 @@ inline llvm::SmallVector valuesStoredInto(mlir_ts::VariableOp varOp // interned, nobody's block). rc's retain or release of // either is one of the block, so the pass looks through them. A cast that converts (a number // printed into a string) or boxes (into `any`) makes a new block and is not one. +inline bool isCopyableString(mlir::Type type); + inline bool isView(mlir::Operation *op) { // (`ts.ValueOrDefault` of a class: the optional's object, or null - the same block or none, spec 77) @@ -407,6 +409,13 @@ inline bool isView(mlir::Operation *op) auto out = castOp.getType(); auto isNullableView = (isStringOrArray(in) && holdsAsIs(out, in, false)) || (isStringOrArray(out) && holdsAsIs(in, out, true)); + // ...or text seen as other text, each one pointer: a string, a string literal, or a union of them + // (and null) that needs no tag - `"yes" | "no"` read as a `string` (TSLANG 68). An optional is + // `{ptr, i1}`, not one of them. + auto isOnePointerText = [](mlir::Type type) { + return !mlir::isa(type) && isCopyableString(type); + }; + isNullableView = isNullableView || (isOnePointerText(in) && isOnePointerText(out)); // a `@value` value seen as the tuple it is laid out as, or a tuple seen as the value: the same // tuple (the cast lowers to nothing) auto isValueStorage = [](mlir::Type value, mlir::Type tuple) { diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index 029a93e42..eec7192e9 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -134,6 +134,99 @@ namespace own_signature loadOp.getReference().getDefiningOp()); } + // The truthiness test of `value` a branch makes: `cond_br(DialectCast(Cast(value -> boolean)))`. + static bool testsTruthOf(mlir::cf::CondBranchOp branch, mlir::Value value) + { + auto dialectCast = branch.getCondition().getDefiningOp(); + auto cast = dialectCast ? dialectCast->getOperand(0).getDefiningOp() : mlir_ts::CastOp(); + return cast && mlir::isa(cast.getType()) && cast->getOperand(0) == value; + } + + // `if (value) array.push(value);` and uses of `value` after it (TSLANG 67): the block the test's + // false edge goes to, reached otherwise only through the push, gets an argument - the element + // read back after the push (`read`) on those paths, null on the false edge - and the uses of + // `value` that block dominates read it. + void mergePushedOrNull(mlir::Operation *pushOp, mlir::Value value, mlir::Value read, mlir::DominanceInfo &dominance) + { + auto *pushBlock = pushOp->getBlock(); + mlir::cf::CondBranchOp test; + for (auto *user : value.getUsers()) + { + auto cast = mlir::dyn_cast(user); + if (!cast) + { + continue; + } + + for (auto *castUser : cast->getUsers()) + { + for (auto *dialectUser : castUser->getUsers()) + { + auto branch = mlir::dyn_cast(dialectUser); + if (branch && testsTruthOf(branch, value) && dominance.dominates(branch.getTrueDest(), pushBlock)) + { + test = branch; + } + } + } + } + + if (!test || test.getFalseDest() == test.getTrueDest()) + { + return; + } + + auto *merge = test.getFalseDest(); + for (auto *pred : merge->getPredecessors()) + { + if (pred == test->getBlock()) + { + continue; + } + + // every other way in has pushed it, and goes in by a plain branch + if (!mlir::isa(pred->getTerminator()) || !dominance.dominates(pushOp, pred->getTerminator())) + { + return; + } + } + + llvm::SmallVector uses; + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (!mlir::isa(user) && user->getBlock() != test->getBlock() && + dominance.dominates(merge, user->getBlock())) + { + uses.push_back(&use); + } + } + + if (uses.empty()) + { + return; + } + + auto location = pushOp->getLoc(); + auto argument = merge->addArgument(value.getType(), location); + mlir::OpBuilder builder(test); + mlir::Value null = builder.create(location, mlir_ts::NullType::get(builder.getContext())); + null = builder.create(location, value.getType(), null); + test.getFalseDestOperandsMutable().append(null); + for (auto *pred : merge->getPredecessors()) + { + if (auto branch = mlir::dyn_cast(pred->getTerminator())) + { + branch.getDestOperandsMutable().append(read); + } + } + + for (auto *use : uses) + { + use->set(argument); + } + } + // `value` seen through the views that keep its block: an optional made of it or opened, a // widening or narrowing cast that is a view. mlir::Value stripViews(mlir::Value value) @@ -848,6 +941,11 @@ namespace own_signature } movedTo[root].push_back({pushOp.getOperation(), read}); + + // ...and pushed only where it is not null (TSLANG 67): `const view = make(); if (view) + // this.views.push(view); return view;`. Where the paths meet again it is the element + // read back, or the null it was: a merge of the two, which each use there reads. + mergePushedOrNull(pushOp, root, read, dominance); } followFieldStores(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index b824c8243..caed4959d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1920,11 +1920,12 @@ endforeach() # TSLANG_ISSUES 62: an array changed through a local that borrows its field # TSLANG_ISSUES 63-65: a lazy getter with a @nodrops C call is a fill; a field of a pushed object; a local or element store then used # TSLANG_ISSUES 66: a `let` borrowing a field, given fresh values on some paths, borrows them from a slot of their own +# TSLANG_ISSUES 67-68: a value pushed only where it is not null; a union of string literals read as a string foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 00tslang_issue_58 00tslang_issue_59 00tslang_issue_60 - 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66) + 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() @@ -5485,7 +5486,8 @@ set(own_error_cases "own_err_local_stored_element_reset|:22:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_pushed_object_field_popped|:24:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_split_let_stored|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" - "own_err_split_let_field_overwritten|:14:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten") + "own_err_split_let_field_overwritten|:14:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_pushed_or_null_popped|:17:23: error: '[^']*' borrows an element but is used here after it may be released or overwritten") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_pushed_or_null_popped.ts b/tslang/test/tester/own/own_err_pushed_or_null_popped.ts new file mode 100644 index 000000000..f7ebd150a --- /dev/null +++ b/tslang/test/tester/own/own_err_pushed_or_null_popped.ts @@ -0,0 +1,24 @@ +// TSLANG_ISSUES 67: a value pushed only where it is not null borrows the element where the paths meet, +// so the element popped before the use frees it. +class View { + constructor(public n: int) {} +} +function make(n: int): View | null { + return n > 0 ? new View(n) : null; +} +class App { + views: View[] = []; + count(n: int): int { + const view = make(n); + if (view) { + this.views.push(view); + } + this.views.pop(); + return view ? view.n : -1; + } +} +function main() { + const app = new App(); + print(app.count(4)); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_67.ts b/tslang/test/tester/tests/00tslang_issue_67.ts new file mode 100644 index 000000000..f0bbeeb67 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_67.ts @@ -0,0 +1,76 @@ +// TSLANG_ISSUES 67-68 (-mm=own), from native_ts_graphics: +// 67: examples/nri/rt_shadows_nri.ts, `const view = buffer.createConstantView(...); if (view) +// this.constantViews.push(view); return view;` - a value pushed only where it is not null, then used +// where the paths meet: the element pushed, or null. +// 68: examples/conservative_rasterization.ts, `ImGui.text(`...: ${this.yesNo(p[3])}`)` with `yesNo` +// returning `v != 0.0 ? "yes" : "no"` - a union of string literals read as a `string`. + +class View { + constructor(public n: int) {} +} + +function make(n: int): View | null { + return n > 0 ? new View(n) : null; +} + +function yesNo(v: number) { + return v != 0.0 ? "yes" : "no"; +} + +function show(s: string): int { + return s.length; +} + +class App { + views: View[] = []; + + view(n: int): View | null { + const view = make(n); + if (view) { + this.views.push(view); + } + return view; + } + + count(n: int): int { + const view = make(n); + if (view) { + this.views.push(view); + } + if (!view) { + return -1; + } + return view.n; + } + + fill(n: int): int { + let total = 0; + for (let i = 0; i < n; i++) { + const view = make(i % 2); + if (view) { + this.views.push(view); + } + total += view ? view.n : 100; + } + return total; + } + + yesNo(v: number) { + return v != 0.0 ? "yes" : "no"; + } + + texts(p: number[]): int { + const s = yesNo(p[0]); + return show(`a: ${yesNo(p[0])}`) + show(`b: ${this.yesNo(p[1])}`) + show("c: " + s) + show(yesNo(p[1])); + } +} + +function main() { + const app = new App(); + const v = app.view(3); + assert(v != null && v.n == 3 && app.view(0) == null); + assert(app.count(4) == 4 && app.count(0) == -1); + assert(app.fill(4) == 202 && app.views.length == 4); + assert(app.texts([1.0, 0.0]) == 6 + 5 + 6 + 2); + print("done."); +} From bc0d3dd1684557268abda1e8b5c71bd4620498ab Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:09:03 +0000 Subject: [PATCH 2/9] A union of string literals allows only its literals: another literal or a string given to it is an error (TS2322, TS2345) Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenImpl.h | 80 ++++++++++++++++++- tslang/test/tester/CMakeLists.txt | 13 +++ tslang/test/tester/tests/00tslang_issue_67.ts | 10 ++- .../argument_string_literal_union_error.ts | 8 ++ .../assign_string_literal_union_error.ts | 10 +++ .../assign_string_to_literal_union_error.ts | 8 ++ 6 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/tests/argument_string_literal_union_error.ts create mode 100644 tslang/test/tester/tests/assign_string_literal_union_error.ts create mode 100644 tslang/test/tester/tests/assign_string_to_literal_union_error.ts diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 3d7077798..03eb775e0 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -3278,8 +3278,84 @@ class MLIRGenImpl auto to = stringOrNumber(destType); // a partial resolve may stand in a call's first overload for the one it will pick (an @cpp // class's `write(s32)` for `write(string)`): the full pass checks the one picked - return from != StringOrNumber::Neither && to != StringOrNumber::Neither && from != to - && !untypedParametersInScope(genContext) && !genContext.allowPartialResolve; + if (untypedParametersInScope(genContext) || genContext.allowPartialResolve) + { + return false; + } + + return (from != StringOrNumber::Neither && to != StringOrNumber::Neither && from != to) + || isStringLiteralMismatch(valueType, destType); + } + + // The string literals a type allows when it allows only string literals (`"no" | "yes"`, with + // null or undefined beside them), or none. + static bool stringLiteralsOf(mlir::Type type, llvm::SmallVectorImpl &literals) + { + auto isStringLiteral = [](mlir::Type member) { + auto literalType = dyn_cast(member); + return literalType && isa(literalType.getElementType()); + }; + + if (auto optionalType = dyn_cast(type)) + { + type = optionalType.getElementType(); + } + + if (isStringLiteral(type)) + { + literals.push_back(type); + return true; + } + + auto unionType = dyn_cast(type); + if (!unionType) + { + return false; + } + + for (auto member : unionType.getTypes()) + { + if (isStringLiteral(member)) + { + literals.push_back(member); + } + else if (!isa(member)) + { + return false; + } + } + + return !literals.empty(); + } + + // `let answer: "no" | "yes" = "maybe"`, or a `string` given to it: TypeScript rejects them (TS2322), + // the values it allows being those literals only. A literal, or a union of literals, it allows + // each of passes. + bool isStringLiteralMismatch(mlir::Type valueType, mlir::Type destType) + { + llvm::SmallVector allowed; + if (!stringLiteralsOf(destType, allowed)) + { + return false; + } + + if (auto optionalType = dyn_cast(valueType)) + { + valueType = optionalType.getElementType(); + } + + if (isa(valueType)) + { + return true; + } + + llvm::SmallVector given; + if (!stringLiteralsOf(valueType, given)) + { + return false; + } + + return llvm::any_of(given, [&](mlir::Type literal) { return !llvm::is_contained(allowed, literal); }); } // `x = "1"` with `x: number`, `let s: string = 5`: TypeScript rejects them (TS2322). Converted, diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index caed4959d..bd3f0e13b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2037,6 +2037,19 @@ add_test(NAME test-compile-argument-boolean-to-number-error COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/argument_boolean_to_number_error.ts") set_tests_properties(test-compile-argument-boolean-to-number-error PROPERTIES PASS_REGULAR_EXPRESSION "argument of type 'boolean' is not assignable to parameter of type 'number'" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +# ...and a value a union of string literals does not allow: another literal, or a string (TSLANG_ISSUES 68) +add_test(NAME test-compile-assign-string-literal-union-error + COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/assign_string_literal_union_error.ts") +set_tests_properties(test-compile-assign-string-literal-union-error PROPERTIES PASS_REGULAR_EXPRESSION "type '\"maybe\"' is not assignable to type '\"no\" . \"yes\"'" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +add_test(NAME test-compile-assign-string-to-literal-union-error + COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/assign_string_to_literal_union_error.ts") +set_tests_properties(test-compile-assign-string-to-literal-union-error PROPERTIES PASS_REGULAR_EXPRESSION "type 'string' is not assignable to type '\"no\" . \"yes\"'" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") +add_test(NAME test-compile-argument-string-literal-union-error + COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/argument_string_literal_union_error.ts") +set_tests_properties(test-compile-argument-string-literal-union-error PROPERTIES PASS_REGULAR_EXPRESSION "argument of type '\"maybe\"' is not assignable to parameter of type '\"no\" . \"yes\"'" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed") # ...a class extending a class value known at run time only is an error add_test(NAME test-compile-mixin-runtime-base-error COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/mixin_runtime_base_error.ts") diff --git a/tslang/test/tester/tests/00tslang_issue_67.ts b/tslang/test/tester/tests/00tslang_issue_67.ts index f0bbeeb67..c1efa7b12 100644 --- a/tslang/test/tester/tests/00tslang_issue_67.ts +++ b/tslang/test/tester/tests/00tslang_issue_67.ts @@ -3,7 +3,8 @@ // this.constantViews.push(view); return view;` - a value pushed only where it is not null, then used // where the paths meet: the element pushed, or null. // 68: examples/conservative_rasterization.ts, `ImGui.text(`...: ${this.yesNo(p[3])}`)` with `yesNo` -// returning `v != 0.0 ? "yes" : "no"` - a union of string literals read as a `string`. +// returning `v != 0.0 ? "yes" : "no"` - a union of string literals read as a `string`. It allows +// those literals only: `"maybe"`, or a `string`, given to it is an error (TS2322). class View { constructor(public n: int) {} @@ -60,7 +61,10 @@ class App { } texts(p: number[]): int { - const s = yesNo(p[0]); + let s = yesNo(p[0]); + if (p[1] == 0.0) { + s = "no"; + } return show(`a: ${yesNo(p[0])}`) + show(`b: ${this.yesNo(p[1])}`) + show("c: " + s) + show(yesNo(p[1])); } } @@ -71,6 +75,6 @@ function main() { assert(v != null && v.n == 3 && app.view(0) == null); assert(app.count(4) == 4 && app.count(0) == -1); assert(app.fill(4) == 202 && app.views.length == 4); - assert(app.texts([1.0, 0.0]) == 6 + 5 + 6 + 2); + assert(app.texts([1.0, 0.0]) == 6 + 5 + 5 + 2); print("done."); } diff --git a/tslang/test/tester/tests/argument_string_literal_union_error.ts b/tslang/test/tester/tests/argument_string_literal_union_error.ts new file mode 100644 index 000000000..d319fb32f --- /dev/null +++ b/tslang/test/tester/tests/argument_string_literal_union_error.ts @@ -0,0 +1,8 @@ +// a literal a parameter of a union of string literals does not allow is an error (TS2345) +function show(a: "no" | "yes") { + print(a); +} + +function main() { + show("maybe"); +} diff --git a/tslang/test/tester/tests/assign_string_literal_union_error.ts b/tslang/test/tester/tests/assign_string_literal_union_error.ts new file mode 100644 index 000000000..d4059a056 --- /dev/null +++ b/tslang/test/tester/tests/assign_string_literal_union_error.ts @@ -0,0 +1,10 @@ +// "no" | "yes" allows only its two literals: another literal is an error, as in TypeScript (TS2322) +function answer(v: number) { + return v != 0.0 ? "yes" : "no"; +} + +function main() { + let a = answer(1.0); + a = "maybe"; + print(a); +} diff --git a/tslang/test/tester/tests/assign_string_to_literal_union_error.ts b/tslang/test/tester/tests/assign_string_to_literal_union_error.ts new file mode 100644 index 000000000..e22984740 --- /dev/null +++ b/tslang/test/tester/tests/assign_string_to_literal_union_error.ts @@ -0,0 +1,8 @@ +// a `string` given to a union of string literals is an error, as in TypeScript (TS2322) +type Answer = "no" | "yes"; + +function main() { + const s: string = "yes"; + const a: Answer = s; + print(a); +} From 2653cd451c7c28bd1a5d976f0ea12b5fde4787b6 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:15:13 +0000 Subject: [PATCH 3/9] 68: a cast of a bare literal stays a constant's read; the literal-union error tests are errors by design for the verifier Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 8 ++++---- tslang/test/tester/verify-ownership.cmake | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index acd156195..4f41d699f 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -409,11 +409,11 @@ inline bool isView(mlir::Operation *op) auto out = castOp.getType(); auto isNullableView = (isStringOrArray(in) && holdsAsIs(out, in, false)) || (isStringOrArray(out) && holdsAsIs(in, out, true)); - // ...or text seen as other text, each one pointer: a string, a string literal, or a union of them - // (and null) that needs no tag - `"yes" | "no"` read as a `string` (TSLANG 68). An optional is - // `{ptr, i1}`, not one of them. + // ...or text seen as other text, each one pointer: a string, or a union of strings and string + // literals (and null) that needs no tag - `"yes" | "no"` read as a `string` (TSLANG 68). An optional + // is `{ptr, i1}`, not one of them; a cast of a literal is a constant's read, which stays one. auto isOnePointerText = [](mlir::Type type) { - return !mlir::isa(type) && isCopyableString(type); + return mlir::isa(type) && isCopyableString(type); }; isNullableView = isNullableView || (isOnePointerText(in) && isOnePointerText(out)); // a `@value` value seen as the tuple it is laid out as, or a tuple seen as the value: the same diff --git a/tslang/test/tester/verify-ownership.cmake b/tslang/test/tester/verify-ownership.cmake index ebe14774a..3ab40979b 100644 --- a/tslang/test/tester/verify-ownership.cmake +++ b/tslang/test/tester/verify-ownership.cmake @@ -66,6 +66,11 @@ set(not_compilable_alone assign_number_to_string_error.ts # an error by design (test-compile-argument-string-to-number-error) argument_string_to_number_error.ts + # errors by design (test-compile-assign-string-literal-union-error, -string-to-literal-union-error, + # test-compile-argument-string-literal-union-error) + assign_string_literal_union_error.ts + assign_string_to_literal_union_error.ts + argument_string_literal_union_error.ts # errors by design (test-compile-assign-boolean-to-number-error, -boolean-to-string-error, # -number-to-boolean-error, test-compile-argument-boolean-to-number-error) assign_boolean_to_number_error.ts From 92c9707a0c852df14032923acf18670d7d1070b2 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:17:47 +0000 Subject: [PATCH 4/9] A parameter property typed by its default is of the default's widened type: `private flags = ""` is a string Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 7 +++++++ tslang/test/tester/tests/00tslang_issue_67.ts | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index fa57483f3..06835d9d4 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -2686,6 +2686,13 @@ namespace mlirgen return mlir::failure(); } + // `constructor(private flags = "")` is a field of the parameter's type: the default's, + // widened - a `string`, not the literal `""` (no other value could be given to it) + if (!typeProvided) + { + type = mth.wideStorageType(type); + } + // `constructor(public m?: T)` is a field `m?: T`: T | undefined, as a declared one // (mlirGenClassDataFieldMember) - `new Error().message` is undefined if (parameter->questionToken) diff --git a/tslang/test/tester/tests/00tslang_issue_67.ts b/tslang/test/tester/tests/00tslang_issue_67.ts index c1efa7b12..425933e4d 100644 --- a/tslang/test/tester/tests/00tslang_issue_67.ts +++ b/tslang/test/tester/tests/00tslang_issue_67.ts @@ -22,6 +22,14 @@ function show(s: string): int { return s.length; } +// a parameter property typed by its default holds any string, not only that literal +class Pattern { + constructor(private source: string, private flags = "") {} + text() { + return this.source + "/" + this.flags; + } +} + class App { views: View[] = []; @@ -76,5 +84,7 @@ function main() { assert(app.count(4) == 4 && app.count(0) == -1); assert(app.fill(4) == 202 && app.views.length == 4); assert(app.texts([1.0, 0.0]) == 6 + 5 + 5 + 2); + const flags: string = "g"; + assert(new Pattern("a", flags).text() == "a/g" && new Pattern("b").text() == "b/"); print("done."); } From b1b8fc6303691d3d47813a657a2af33cb95b2528 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:17:55 +0000 Subject: [PATCH 5/9] TSLANG_ISSUES 67-68 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index f3b6229fc..e0be94257 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -479,6 +479,33 @@ ports 25 to 6), and no source has more. Regression tests `tslang/test/tester/tes (every model; loops, a class-typed `let`, a throw), `own/own_err_split_let_stored.ts` and `own/own_err_split_let_field_overwritten.ts`. +67 (a value pushed only where it is not null, then used: `const view = constantBuffer.createConstantView(offset, +size); if (view) this.constantViews.push(view); return view;` in `examples/nri/rt_shadows_nri.ts` and its +NRI ports, refused "is used here after its value was moved") is fixed. TSLANG 52 made each use the push +dominates a read of the array's last element; one after the paths meet again is now one too. The block the +test's false edge goes to, entered otherwise only from blocks the push dominates, takes the element read +back on those paths and null on the false edge, and the uses there read it. A function returning it returns +a borrow of `this`, as one returning the element read does. The array popped before the use is still +refused, and so is a push under a test that is not the value's own truth (`if (view && keep)`), where the +value is not null on the other path. A `let` pushed this way (`let pipeline = null; if (loaded) pipeline = +create(); if (pipeline) this.pipelines.push(pipeline); return pipeline;` in `msaa_nri.ts`) is not handled +yet. In the sweep, 6 errors less; their sources fail for others too. Regression tests +`tslang/test/tester/tests/00tslang_issue_67.ts` (every model; it holds 68 too) and +`own/own_err_pushed_or_null_popped.ts`. + +68 (a union of string literals read as a `string`: `ImGui.text(`primitiveUnderestimation: ${this.yesNo( +p[3])}`)` in `examples/conservative_rasterization.ts`, with `yesNo` returning `value != 0.0 ? "yes" : "no"`, +refused "takes a second reference") is fixed. `"no" | "yes"` is one pointer, like a `string`, and the cast +between them lowers to nothing, but -mm=own took it for a conversion making a new string, so the call +result it was cast from was released while the string was used. A cast between a string, a string literal +and a union of them (and null) that needs no tag is now a view. In the sweep, 2 sources more compile +(`conservative_rasterization.ts`, `multi_draw_indirect.ts`) and 20 errors less. A union of string literals +also allows only its literals now, under every model: `let a: "no" | "yes" = "maybe"`, a `string` given +to it, or `a = "maybe"` after `let a = yesNo(v)`, compiled and held "maybe"; each is now refused, as in +TypeScript (TS2322, and TS2345 for an argument). Regression tests +`tslang/test/tester/tests/assign_string_literal_union_error.ts`, `assign_string_to_literal_union_error.ts` +and `argument_string_literal_union_error.ts`. + For native_ts_graphics, from the same sweep (its code, not the compiler): - `core/ray_tracing_extended_scene.ts`: the exported classes' constructors keep their array parameters (`this.origin = origin`), which an exported constructor has to say: `@keeps("origin", "direction")` @@ -489,7 +516,7 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): takes it; the next release won't. `desc.setDepthClip(clip != 0)` fixes it. - Under `-mm=own`, 3 `core/` modules do not compile yet (`nri_donut_textures.ts`, `ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and nor do many examples, - which #310 did not port: with every facts object built and 50-66 in, 118 of the 276 sources fail (664 errors). The + which #310 did not port: with every facts object built and 50-68 in, 116 of the 276 sources fail (639 errors). The largest group left is "borrows a field but is used here after it may be released or overwritten" (396, - 93 of them `this.sample.run` callbacks), then "used here after its value was moved" (58: mostly a value a pushed object or an + 93 of them `this.sample.run` callbacks), then "used here after its value was moved" (52: mostly a value a pushed object or an earlier call keeps, then returned or used). From 83f9b35bd78dc1cff47c1937d591c7e93fa7c184 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:18:47 +0000 Subject: [PATCH 6/9] TSLANG_ISSUES 68: the parameter property widening Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index e0be94257..dd7864335 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -502,7 +502,10 @@ and a union of them (and null) that needs no tag is now a view. In the sweep, 2 (`conservative_rasterization.ts`, `multi_draw_indirect.ts`) and 20 errors less. A union of string literals also allows only its literals now, under every model: `let a: "no" | "yes" = "maybe"`, a `string` given to it, or `a = "maybe"` after `let a = yesNo(v)`, compiled and held "maybe"; each is now refused, as in -TypeScript (TS2322, and TS2345 for an argument). Regression tests +TypeScript (TS2322, and TS2345 for an argument). The check found a parameter property typed by its +default left unwidened: `constructor(private source: string, private flags = "")` in the default library's +`RegExp` made a field of type `""`; it is a `string` now, as a declared field and a parameter already were. +Regression tests `tslang/test/tester/tests/assign_string_literal_union_error.ts`, `assign_string_to_literal_union_error.ts` and `argument_string_literal_union_error.ts`. From 9318de7d8d01ea6481e015229e50f47f01daba18 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:24:15 +0000 Subject: [PATCH 7/9] own: a value moved on every path, by a different move on each (TSLANG 69) Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnershipInferenceMoves.cpp | 7 ++- tslang/test/tester/CMakeLists.txt | 6 ++- .../own_err_moved_on_two_of_three_paths.ts | 24 ++++++++++ tslang/test/tester/tests/00tslang_issue_69.ts | 47 +++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tslang/test/tester/own/own_err_moved_on_two_of_three_paths.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_69.ts diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 2afa5b01b..428b3359b 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -1016,9 +1016,12 @@ namespace own_inference continue; } - // moved on some ways only: on the others it must be empty (TSLANG 49) + // moved on some ways only: on the others it must be empty (TSLANG 49) - or moved by + // another taker: `if (c) this.a.push(node); else this.b.push(node);` reaches the + // release only past one move or the other (TSLANG 69) if (!dominance->properlyDominates(acquire, release) && - !emptyWhereNotMoved(value, definition, acquire, release)) + !emptyWhereNotMoved(value, definition, acquire, release) && + !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers))) { reportMovedOnSomePaths(taker, release, name); return false; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bd3f0e13b..f41d8127c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1921,11 +1921,12 @@ endforeach() # TSLANG_ISSUES 63-65: a lazy getter with a @nodrops C call is a fill; a field of a pushed object; a local or element store then used # TSLANG_ISSUES 66: a `let` borrowing a field, given fresh values on some paths, borrows them from a slot of their own # TSLANG_ISSUES 67-68: a value pushed only where it is not null; a union of string literals read as a string +# TSLANG_ISSUES 69: a value moved on every path, by a different move on each foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 00tslang_issue_58 00tslang_issue_59 00tslang_issue_60 - 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67) + 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69) tslang_add_test(NAME test-jit-${issues_test}-${issues_mm} COMMAND test-runner -jit -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") tslang_add_test(NAME test-compile-${issues_test}-${issues_mm} COMMAND test-runner -mm=${issues_mm} "${PROJECT_SOURCE_DIR}/test/tester/tests/${issues_test}.ts") endforeach() @@ -5500,7 +5501,8 @@ set(own_error_cases "own_err_pushed_object_field_popped|:24:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_split_let_stored|:14:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_split_let_field_overwritten|:14:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" - "own_err_pushed_or_null_popped|:17:23: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_pushed_or_null_popped|:17:23: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name) diff --git a/tslang/test/tester/own/own_err_moved_on_two_of_three_paths.ts b/tslang/test/tester/own/own_err_moved_on_two_of_three_paths.ts new file mode 100644 index 000000000..6a0f5434a --- /dev/null +++ b/tslang/test/tester/own/own_err_moved_on_two_of_three_paths.ts @@ -0,0 +1,24 @@ +// TSLANG_ISSUES 69: moved by one move or another on some paths, but on a third path by none, a value +// is still moved on some paths only. +class Node2 { + constructor(public kind: int) {} +} + +class App { + a: Node2[] = []; + b: Node2[] = []; + + pick(o: int) { + const node = new Node2(o); + if (o == 1) { + this.a.push(node); + } else if (o == 2) { + this.b.push(node); + } + } +} + +function main() { + new App().pick(3); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_69.ts b/tslang/test/tester/tests/00tslang_issue_69.ts new file mode 100644 index 000000000..9dde96657 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_69.ts @@ -0,0 +1,47 @@ +// TSLANG_ISSUES 69 (-mm=own), from native_ts_graphics' examples/nri/extended_dynamic_state2_nri.ts: +// `const node = new SceneNode(...); if (tessellated) this.tessNodes.push(node); else +// this.baselineNodes.push(node);` - a value moved on every path, by a different move on each. + +class Node2 { + constructor(public kind: int) {} +} + +class App { + a: Node2[] = []; + b: Node2[] = []; + x: Node2 | null = null; + y: Node2 | null = null; + + load(n: int) { + for (let o = 0; o < n; o++) { + const node = new Node2(o); + if (o % 2 == 0) { + this.a.push(node); + } else { + this.b.push(node); + } + } + } + + pick(o: int) { + const node = new Node2(o); + if (o == 1) { + this.a.push(node); + } else if (o == 2) { + this.x = node; + } else { + this.y = node; + } + } +} + +function main() { + const app = new App(); + app.load(5); + assert(app.a.length == 3 && app.b.length == 2 && app.b[1].kind == 3); + app.pick(1); + app.pick(2); + app.pick(3); + assert(app.a.length == 4 && app.x != null && app.x.kind == 2 && app.y != null && app.y.kind == 3); + print("done."); +} From e004442b1c53f157836e3ce4f02bec842661cb96 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:28:19 +0000 Subject: [PATCH 8/9] own: a constant global of a string literal's type, given to a call that keeps it (TSLANG 70) Co-Authored-By: Claude Opus 5.5 --- .../TypeScript/OwnershipInferenceMoves.cpp | 24 +++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 2 +- tslang/test/tester/tests/00tslang_issue_69.ts | 23 ++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 428b3359b..8df5cb296 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -381,8 +381,28 @@ namespace own_inference } auto castOp = rootOf(value).getDefiningOp(); - return castOp && mlir::isa(castOp.getType()) && - castOp.getIn().getDefiningOp(); + if (!castOp || !mlir::isa(castOp.getType())) + { + return false; + } + + if (castOp.getIn().getDefiningOp()) + { + return true; + } + + // ...or a read of a constant global of a string literal's type (`const PATH = "flame.dds"`, + // TSLANG 70): it holds that literal, in read-only data, and nothing else + auto loadOp = castOp.getIn().getDefiningOp(); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + auto literalType = mlir::dyn_cast(castOp.getIn().getType()); + if (!addressOfOp || !literalType || !mlir::isa(literalType.getElementType())) + { + return false; + } + + auto globalOp = mlir::SymbolTable::lookupNearestSymbolFrom(addressOfOp, addressOfOp.getGlobalNameAttr()); + return globalOp && globalOp.getConstant(); } bool OwnershipInferencePass::isBorrow(mlir::Operation *user, mlir::Value value) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f41d8127c..136340ef2 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1921,7 +1921,7 @@ endforeach() # TSLANG_ISSUES 63-65: a lazy getter with a @nodrops C call is a fill; a field of a pushed object; a local or element store then used # TSLANG_ISSUES 66: a `let` borrowing a field, given fresh values on some paths, borrows them from a slot of their own # TSLANG_ISSUES 67-68: a value pushed only where it is not null; a union of string literals read as a string -# TSLANG_ISSUES 69: a value moved on every path, by a different move on each +# TSLANG_ISSUES 69-70: a value moved on every path, by a different move on each; a constant literal global given to a keeping call foreach(issues_mm gc rc none own) foreach(issues_test 00tslang_issue_44 00tslang_issue_45 00tslang_issue_46 00tslang_issue_47 00tslang_issue_48 00tslang_issue_49 00tslang_issue_50 00tslang_issue_52 00tslang_issue_53 00tslang_issue_54 00tslang_issue_55 00tslang_issue_56 00tslang_issue_57 diff --git a/tslang/test/tester/tests/00tslang_issue_69.ts b/tslang/test/tester/tests/00tslang_issue_69.ts index 9dde96657..d85cb8e70 100644 --- a/tslang/test/tester/tests/00tslang_issue_69.ts +++ b/tslang/test/tester/tests/00tslang_issue_69.ts @@ -1,6 +1,28 @@ // TSLANG_ISSUES 69 (-mm=own), from native_ts_graphics' examples/nri/extended_dynamic_state2_nri.ts: // `const node = new SceneNode(...); if (tessellated) this.tessNodes.push(node); else // this.baselineNodes.push(node);` - a value moved on every path, by a different move on each. +// 70: examples/nri/ray_tracing_extended_nri.ts, `this.loadTexture(device, FLAME_TEXTURE_PATH)` with +// `const FLAME_TEXTURE_PATH = "..."` in its namespace, and `loadTexture` keeping the path - a constant +// global of a string literal's type holds that literal only, given to a call that keeps it. + +namespace Textures { + const PATH = "flame.dds"; + export class Cache { + names: string[] = []; + load(path: string): int { + for (let t = 0; t < this.names.length; t++) { + if (this.names[t] == path) { + return t; + } + } + this.names.push(path); + return this.names.length - 1; + } + run(): int { + return this.load(PATH) + this.load(PATH) + this.load("x"); + } + } +} class Node2 { constructor(public kind: int) {} @@ -43,5 +65,6 @@ function main() { app.pick(2); app.pick(3); assert(app.a.length == 4 && app.x != null && app.x.kind == 2 && app.y != null && app.y.kind == 3); + assert(new Textures.Cache().run() == 1); print("done."); } From c557668744f0a49b06d17af226ebf60025e43e1a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:30:00 +0000 Subject: [PATCH 9/9] TSLANG_ISSUES 69-70 and the ntg sweep status Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index dd7864335..953203fd7 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -509,6 +509,24 @@ Regression tests `tslang/test/tester/tests/assign_string_literal_union_error.ts`, `assign_string_to_literal_union_error.ts` and `argument_string_literal_union_error.ts`. +69 (a value moved on every path, by a different move on each: `const node = new SceneNode(...); if +(SCENE_ORDER[o] == TESSELLATED) this.tessNodes.push(node); else { ... this.baselineNodes.push(node); }` in +`examples/nri/extended_dynamic_state2_nri.ts`, refused "is moved here on some paths only; -mm=own cannot +release it on the others yet") is fixed. Each move was judged alone: the value's release where the paths meet +was past this move on some paths, and on the others past a move it did not look at. A release no path reaches +from the value's making without passing one of its moves is past a move on each, and goes. A path that moves +it nowhere (`if ... else if ...` with no `else`) is still refused, and so is a use after the moves. In the +sweep, 4 errors less. Regression tests `tslang/test/tester/tests/00tslang_issue_69.ts` (every model; it holds +70 too) and `own/own_err_moved_on_two_of_three_paths.ts`. + +70 (a constant global of a string literal's type given to a call that keeps it: `this.loadTexture(device, +FLAME_TEXTURE_PATH)` in `examples/nri/ray_tracing_extended_nri.ts`, with `const FLAME_TEXTURE_PATH = "..."` in +its namespace and `loadTexture` pushing the path into `textureNames`, refused "is given to ..., which keeps +it, but -mm=own cannot move it here: this function does not own it") is fixed. The global is a constant whose +type is the literal, so it holds that literal and nothing else, in read-only data: given to a keeper, it is the +literal itself, as `"..."` written in place is. 69 exposed it: `loadTexture` is now found to keep its path. In the sweep, the 2 errors it gave +(in `ray_tracing_extended_nri.ts` and `ray_tracing_invocation_reorder_nri.ts`) are gone. + For native_ts_graphics, from the same sweep (its code, not the compiler): - `core/ray_tracing_extended_scene.ts`: the exported classes' constructors keep their array parameters (`this.origin = origin`), which an exported constructor has to say: `@keeps("origin", "direction")` @@ -519,7 +537,7 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): takes it; the next release won't. `desc.setDepthClip(clip != 0)` fixes it. - Under `-mm=own`, 3 `core/` modules do not compile yet (`nri_donut_textures.ts`, `ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and nor do many examples, - which #310 did not port: with every facts object built and 50-68 in, 116 of the 276 sources fail (639 errors). The + which #310 did not port: with every facts object built and 50-70 in, 116 of the 276 sources fail (635 errors). The largest group left is "borrows a field but is used here after it may be released or overwritten" (396, 93 of them `this.sample.run` callbacks), then "used here after its value was moved" (52: mostly a value a pushed object or an earlier call keeps, then returned or used).