From 98a0e23eae638b0933e4130ea5eb75c95409cffc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 16:33:54 +0000 Subject: [PATCH 1/4] own: name the overridable method a call reaches where a borrow ends (TSLANG 60) `this.props[i].tick(t)` (native_ts_graphics core/rtxpt_game.ts), where `Prop.tick` reaches a method of an exported class through `setNodeTransform`, ended borrows with no reason given: TSLANG 48 named the method only for a direct virtual call. The signature pass now finds the one overridable method a call's callees reach (markOverridableReached, when it is the only reason they may destroy anything) and the error notes it with the way to it. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 18 +++ tslang/lib/TypeScript/OwnershipFacts.h | 4 + .../OwnershipInferenceDiagnostics.cpp | 15 ++ .../lib/TypeScript/OwnershipInferencePass.cpp | 2 +- .../lib/TypeScript/OwnershipSignaturePass.cpp | 147 ++++++++++++++++++ .../lib/TypeScript/OwnershipSignaturePass.h | 3 + tslang/test/tester/CMakeLists.txt | 6 +- .../tester/own/own_err_overridable_reached.ts | 41 +++++ tslang/test/tester/tests/00tslang_issue_60.ts | 52 +++++++ 9 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 tslang/test/tester/own/own_err_overridable_reached.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_60.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 3dbd83fe5..1aa2c6309 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -384,6 +384,24 @@ own errors are left (its `@keeps` and `Shared`, below). One site of this shap does not reproduce. Regression tests `tslang/test/tester/tests/00tslang_issue_59.ts` (every model) and `own/own_err_element_field_moved_replaced.ts`. +60 (the error at a borrow a call ends did not say why, when the callee is a function defined here: `const +name = this.name; for (...) this.props[i].tick(this.gameTime); ... name` in `core/rtxpt_game.ts`, "borrows a +field but is used here after it may be released or overwritten", with only "it may be released or +overwritten here" at the call) is fixed. `Prop.tick` calls `setNodeTransform`, which calls +`world.setGameNodeTransform(...)`, a method of the exported `GameWorld`. Another module may extend +`GameWorld` and override it with a method that destroys anything, so `Prop.tick` may too, and the borrows +of `this.props[i]` and `this.name` end at it. 48 named the method only when the call itself was the +virtual one. The signature pass now finds, for a call whose callees may destroy anything, the overridable +call they reach, breadth first through calls that may destroy anything, and the error notes it: "'Prop.tick' +calls 'setNodeTransform', which calls 'GameWorld.setGameNodeTransform'; that method may be overridden in +another module by a method that does; '@final' on the method, or on its class, says it is not". Nothing +compiles that did not: it is the diagnostic. The note is given only when that method is the one +reason (no other overridable method, unknown or imported callee, or body that destroys anything on the +way), so `@final` on it is enough. In the sweep, 48 errors carry it, beside the 51 at a direct call of +such a method: native_ts_graphics' own `@final`s to add (`GameWorld`, `Pose`, `MsvcRandom`, ...). Regression tests +`tslang/test/tester/tests/00tslang_issue_60.ts` (every model; the shape with `@final`, which compiles) and +`own/own_err_overridable_reached.ts` (the note). + 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")` diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index fb9923743..572f5ba31 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -73,6 +73,10 @@ namespace mlir_ts = mlir::typescript; // A virtual call that may destroy anything only because another module may override the method it // names (TSLANG 48): the method's symbol, for the error that a borrow ends there #define OWN_OVERRIDABLE_CALL_ATTR_NAME "__own_overridable_call" +// A call of functions defined here that may destroy anything because a call they reach, directly or +// through others, is such a virtual call (TSLANG 60): [the method's symbol, then the functions on the +// way to it, the callee first], for the same error +#define OWN_OVERRIDABLE_VIA_ATTR_NAME "__own_overridable_via" // On a `ts.ReleaseSlot` of a field: a fill of the field, tested empty on the way (fillsEmptyField, // TSLANG 47), which frees nothing. The signature pass decides it, with what calls in between may write. #define OWN_FILL_ATTR_NAME "__own_fill" diff --git a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp index 492885553..02ecf8f50 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp @@ -758,6 +758,21 @@ namespace own_inference << "'" << method.getValue() << "' may be overridden in another module by a method that does; '@" FINAL_DECORATOR << "' on the method, or on its class, says it is not"; } + // ...or a call of a function here that reaches such a call (TSLANG 60) + else if (auto via = drop->getAttrOfType(OWN_OVERRIDABLE_VIA_ATTR_NAME); via && via.size() >= 2) + { + // `'Prop.tick' calls 'setNodeTransform', which calls 'GameWorld.setGameNodeTransform'` + auto ¬e = diag.attachNote(drop->getLoc()); + note << "'" << mlir::cast(via[1]).getValue() << "' calls '"; + for (auto index = 2u; index < via.size(); ++index) + { + note << mlir::cast(via[index]).getValue() << "', which calls '"; + } + + note << mlir::cast(via[0]).getValue() + << "'; that method may be overridden in another module by a method that does; '@" << FINAL_DECORATOR + << "' on the method, or on its class, says it is not"; + } signalPassFailure(); } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index b8ade0d49..cea770bc2 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -818,7 +818,7 @@ namespace own_inference OWN_RESULT_RECORD_ATTR_NAME, OWN_RESULT_RECORD_UNBOXED_ATTR_NAME, OWN_RECORD_UNBOXED_FIELDS_ATTR_NAME, OWN_RECORD_TEMPORARY_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME, OWN_DROP_FIELDS_ATTR_NAME, OWN_FACTS_LOST_ATTR_NAME, - OWN_OVERRIDABLE_CALL_ATTR_NAME, OWN_FILL_ATTR_NAME, + OWN_OVERRIDABLE_CALL_ATTR_NAME, OWN_OVERRIDABLE_VIA_ATTR_NAME, OWN_FILL_ATTR_NAME, OWN_FRESH_RESULT_ATTR_NAME, OWN_ASSIGNS_CAPTURES_ATTR_NAME, OWN_RESULT_BOUNDED_ATTR_NAME, OWN_STATE_BORROW_ATTR_NAME, diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index a84a5c11e..0e40f2edd 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -343,6 +343,7 @@ namespace own_signature } } + markOverridableReached(); exportNoDrops(); ownGlobalInitializerCalls(module); @@ -1819,6 +1820,152 @@ namespace own_signature } } + void OwnershipSignaturePass::markOverridableReached() + { + // `this.props[i].tick(t)`, where `Prop.tick` calls `setNodeTransform`, which calls + // `world.setGameNodeTransform(...)` of an exported class: the outer call may destroy anything + // only because of that virtual call, which the error at a borrow it ends names (TSLANG 60). The + // way to it is found breadth first, through calls that may destroy anything, so it is a short one. + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + auto mayDropAnything = [&](const Call &call) { + if (call.op->hasAttr(OWN_NO_DROPS_ATTR_NAME) || call.op->hasAttr(OWN_DROP_FIELDS_ATTR_NAME)) + { + return false; + } + + auto drops = calleesFieldDrops(call.callees); + return !drops || drops->any; + }; + + // a function's own body destroying anything (not through its calls), as the last round decided it + llvm::DenseMap bodyAny; + auto bodyDropsAnything = [&](mlir_ts::FuncOp funcOp) { + auto [found, inserted] = bodyAny.try_emplace(funcOp, false); + if (inserted) + { + found->second = dropsInBody(funcOp, true) && fieldDropsInBody(funcOp, true).any; + } + + return found->second; + }; + + // The one overridable method the functions reach, and the way to it - none when they reach + // another, or something else that may destroy anything (an unknown or imported callee, a body + // that does): `@final` on that method would not be enough then + auto explain = [&](llvm::ArrayRef funcs) -> mlir::ArrayAttr { + // each function reached, and the one that called it on the way + llvm::DenseMap reachedFrom; + llvm::SmallVector work; + for (auto callee : funcs) + { + if (reachedFrom.try_emplace(callee, nullptr).second) + { + work.push_back(callee); + } + } + + mlir::FlatSymbolRefAttr method; + mlir::Operation *holder = nullptr; + for (auto next = 0u; next < work.size(); ++next) + { + auto funcOp = work[next]; + if (work.size() > 256 || bodyDropsAnything(funcOp)) + { + return {}; + } + + for (auto *inner : callsIn[funcOp]) + { + if (auto overridable = inner->op->getAttrOfType(OWN_OVERRIDABLE_CALL_ATTR_NAME)) + { + if (method && method != overridable) + { + return {}; + } + + if (!method) + { + method = overridable; + holder = funcOp; + } + + continue; + } + + if (!mayDropAnything(*inner)) + { + continue; + } + + if (!inner->callees.known || inner->callees.imported || inner->callees.funcs.empty()) + { + return {}; + } + + for (auto callee : inner->callees.funcs) + { + if (reachedFrom.try_emplace(callee, funcOp).second) + { + work.push_back(callee); + } + } + } + } + + if (!method) + { + return {}; + } + + llvm::SmallVector path; + for (auto *at = holder; at; at = reachedFrom.lookup(at)) + { + path.push_back(mlir::cast(at).getSymNameAttr()); + } + + std::reverse(path.begin(), path.end()); + path.insert(path.begin(), method); + return mlir::ArrayAttr::get(&getContext(), path); + }; + + // a call of one function, the common case, explained once per function + llvm::DenseMap explained; + for (auto &call : calls) + { + if (!call.callees.known || call.callees.imported || call.callees.funcs.empty() || + call.op->hasAttr(OWN_OVERRIDABLE_CALL_ATTR_NAME) || !mayDropAnything(call)) + { + continue; + } + + mlir::ArrayAttr via; + if (call.callees.funcs.size() == 1) + { + auto [found, inserted] = explained.try_emplace(call.callees.funcs.front(), mlir::ArrayAttr()); + if (inserted) + { + found->second = explain(call.callees.funcs); + } + + via = found->second; + } + else + { + via = explain(call.callees.funcs); + } + + if (via) + { + call.op->setAttr(OWN_OVERRIDABLE_VIA_ATTR_NAME, via); + } + } + } + std::optional OwnershipSignaturePass::calleesFieldDrops(const Callees &callees) { if (!callees.known || callees.instanceOf) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 86d1827b6..7d67ff8d0 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -636,6 +636,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper calleesFieldDrops(const Callees &callees); + // OWN_OVERRIDABLE_VIA_ATTR_NAME on each call whose callees reach an overridable virtual call + // (TSLANG 60) + void markOverridableReached(); // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index c9dd59d1a..bbfcbbc13 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1913,10 +1913,11 @@ endforeach() # TSLANG_ISSUES 57: a `let` that only holds parameters owns nothing # TSLANG_ISSUES 58: a `const` declared from a merge of owning locals borrows them; a merge made in a loop's body # TSLANG_ISSUES 59: a value moved into a field of an element or field read, then used, borrows that field +# TSLANG_ISSUES 60: a call that reaches an overridable method names it where a borrow ends 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_58 00tslang_issue_59 00tslang_issue_60) 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() @@ -5469,7 +5470,8 @@ set(own_error_cases "own_err_parameter_let_stored|:10:5: error: '[^']*' takes a second reference" "own_err_merged_locals_overwritten|:11:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_merged_locals_moved|:12:5: error: '[^']*' is moved here while '[^']*' borrows it" - "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_overridable_reached|:34:13: note: 'Prop.tick' calls 'setNodeTransform', which calls 'World.setTransform'") 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_overridable_reached.ts b/tslang/test/tester/own/own_err_overridable_reached.ts new file mode 100644 index 000000000..9c5e9dea3 --- /dev/null +++ b/tslang/test/tester/own/own_err_overridable_reached.ts @@ -0,0 +1,41 @@ +// TSLANG_ISSUES 60: a call of a function defined here that may destroy anything only because a call it +// reaches is of a method another module may override: the error at a borrow it ends names that method +// and the way to it. +export class World { + count = 0; + setTransform(node: int) { + this.count += node; + } +} + +function setNodeTransform(world: World, node: int) { + world.setTransform(node); +} + +class Prop { + world: World; + node: int = 1; + constructor(world: World) { + this.world = world; + } + + tick() { + setNodeTransform(this.world, this.node); + } +} + +export class Game { + props: Prop[] = []; + name = "game"; + + tick(): int { + const name = this.name; + for (let i = 0; i < this.props.length; i++) { + this.props[i].tick(); + } + + return name.length; + } +} + +function main() { print(new Game().tick()); } diff --git a/tslang/test/tester/tests/00tslang_issue_60.ts b/tslang/test/tester/tests/00tslang_issue_60.ts new file mode 100644 index 000000000..e3e44eae5 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_60.ts @@ -0,0 +1,52 @@ +// TSLANG_ISSUES 60 (-mm=own), from native_ts_graphics' core/rtxpt_game.ts: `this.props[i].tick(t)`, where +// `Prop.tick` calls `setNodeTransform`, which calls a method of the exported `GameWorld`. Another module +// may override that method, so the call may destroy anything, and the error said so only for a direct call +// of it (own_err_overridable_reached). `@final` on the class says no module does: the borrows of `this.props[i]`, +// `this.world` and `this.name` then hold over the call. + +@final +export class World { + count = 0; + setTransform(node: int) { + this.count += node; + } +} + +function setNodeTransform(world: World, node: int) { + world.setTransform(node); +} + +class Prop { + node: int; + constructor(node: int) { + this.node = node; + } + + tick(world: World) { + setNodeTransform(world, this.node); + } +} + +export class Game { + world = new World(); + props: Prop[] = []; + name = "game"; + + tick(): int { + const name = this.name; + for (let i = 0; i < this.props.length; i++) { + this.props[i].tick(this.world); + } + + return name.length + this.world.count; + } +} + +function main() { + const game = new Game(); + game.props.push(new Prop(2)); + game.props.push(new Prop(3)); + assert(game.tick() == 9); + assert(game.tick() == 14); + print("done."); +} From fbd9eb8a2713213333acf1d7b72697e2cc2d87ca Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 16:34:08 +0000 Subject: [PATCH 2/4] TSLANG_ISSUES 60: the classes the new notes name Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 1aa2c6309..aea09e22e 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -398,7 +398,8 @@ another module by a method that does; '@final' on the method, or on its class, s compiles that did not: it is the diagnostic. The note is given only when that method is the one reason (no other overridable method, unknown or imported callee, or body that destroys anything on the way), so `@final` on it is enough. In the sweep, 48 errors carry it, beside the 51 at a direct call of -such a method: native_ts_graphics' own `@final`s to add (`GameWorld`, `Pose`, `MsvcRandom`, ...). Regression tests +such a method. They name native_ts_graphics' `@final`s to add: `MsvcRandom` (41, through the +examples' `randomFloat`) and `GameWorld` (7). Regression tests `tslang/test/tester/tests/00tslang_issue_60.ts` (every model; the shape with `@final`, which compiles) and `own/own_err_overridable_reached.ts` (the note). From fc7f6e29a374254f8709a2a8d939fd319e3b3ab8 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 16:42:30 +0000 Subject: [PATCH 3/4] own: an array global nothing assigns is immortal (TSLANG 61) `[CUBE_VERTICES, PLANE_VERTICES]`, `return GREEN;` and `const p = CAMERA_POSITION;` over module and namespace arrays (native_ts_graphics examples) were refused as borrows of a mortal global: only a class-typed global could be immortal (own spec 25). An array global written once by its initializer is now one too; its header is the immortal block (marked only when mortal: a constant array's is in read-only data), and its elements are still freed when overwritten. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 21 +++++- .../2026-09-24-own-memory-model-design.md | 12 ++++ tslang/lib/TypeScript/LowerToLLVM.cpp | 10 ++- .../TypeScript/OwnershipSignatureGlobals.cpp | 6 +- tslang/test/tester/CMakeLists.txt | 7 +- ..._err_immortal_array_element_overwritten.ts | 18 +++++ tslang/test/tester/tests/00tslang_issue_61.ts | 71 +++++++++++++++++++ 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_61.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index aea09e22e..bc8627562 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -403,6 +403,25 @@ examples' `randomFloat`) and `GameWorld` (7). Regression tests `tslang/test/tester/tests/00tslang_issue_60.ts` (every model; the shape with `@final`, which compiles) and `own/own_err_overridable_reached.ts` (the note). +61 (a module or namespace array nothing assigns, read where a borrow cannot go: `[CUBE_VERTICES, +PLANE_VERTICES, CUBE_VERTICES]` and `[[MAT_RED, MAT_GREEN, ...], [MAT_GREY]]` in +`examples/nri/ray_tracing_reflection_nri.ts`, `return GREEN;` in `examples/nri/collision_nri.ts`, `const p += CAMERA_POSITION;` held across calls in `examples/nri/async_compute_bloom_nri.ts`, refused "the value read +from 'X' is kept where its end at an assignment of 'X' cannot be checked" and "borrows the global 'X' and +cannot be stored, returned or captured") is fixed. Even a plain `const A = [1.0, 2.0]` at the top level was +refused: only a global of a class type could be immortal (own spec 25). An array global is now one on the +same terms - not exported, written once by its initializer, given a fresh array, nothing, or another +immortal global's value - and a read of it owns nothing and may go anywhere. The block made immortal is the +array's header, which every array release tests first, so neither its data nor its elements are released +through it; an element overwritten, popped or spliced out still frees the old element, and a borrow of +`G[i]` stays bounded. A global pushed to, or written in any other way through its address, stays mortal. +The initializing store marks the header only when it is mortal: a constant array's header is in read-only +data, immortal already. Spec 25 has the amendment. In the sweep, 126 sources failing went to 119 +(`ptlas.ts`, `ray_tracing_reflection.ts`, `collision.ts`, `nvrhi_direct.ts`, `async_compute_bloom.ts`, +`msaa.ts`, `texture_compression_comparison.ts` now compile, and `ray_tracing_reflection_nri.ts` went from +29 errors to 1), and errors from 907 to 811. Regression tests `tslang/test/tester/tests/00tslang_issue_61.ts` +(every model) and `own/own_err_immortal_array_element_overwritten.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")` @@ -413,7 +432,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`, 4 `core/` modules do not compile yet (`donut_first_person_camera.ts`, `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-59 in, 126 of the 276 sources fail. The + which #310 did not port: with every facts object built and 50-61 in, 119 of the 276 sources fail. The largest group left is "borrows a field but is used here after it may be released or overwritten" (about 200 sites), then "used here after its value was moved" (37: mostly a value a pushed object or an earlier call keeps, then returned or used). diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 6157ad039..dd7172e8d 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -3057,6 +3057,18 @@ above, in a static block whose only store of `K.w` is the loop's) and `own_err_g programs exit 139 on Windows and under `MALLOC_PERTURB_` on Linux. Newly rejected beside them: only the unsafe shapes; the corpus keeps 526 files and the ray tracer has no error. +**Arrays (TSLANG 61), 2026-11-14.** A global of an array type is a candidate too, on the same terms +(not exported, written once by its initializer, given a fresh array, nothing, or another immortal +global's value): `const CUBE_VERTICES = [...]` read into `[CUBE_VERTICES, PLANE_VERTICES]`, or +`return GREEN;`. The block made immortal is the array's header, which every array release tests +first (`buildArrayBody`'s `emitIfLastReference`), so neither the data block nor the elements are +ever released through it. As with a class's fields, only that block: an element overwritten, popped +or spliced out frees the old element, and a borrow of `G[i]` stays bounded +(`own_err_immortal_array_element_overwritten`). A global given a push or any other write through its +address stays mortal (its `ts.AddressOf` is then not only loaded). The initializing store marks the +header with `emitMarkImmortalIfMortal`: a constant array's header is in read-only data, immortal +already. Test: `00tslang_issue_61`. + ## 26. `Shared` of an object literal (G4 of the ray tracer plan), 2026-10-21 The ray tracer's surfaces are module-level `new Shared({ ...function fields... })`. The diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index 645ccc767..9a2fcafb7 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5522,7 +5522,15 @@ struct StoreOpLowering : public TsLlvmPattern if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && storeOp->hasAttr(OWN_IMMORTAL_GLOBAL_ATTR_NAME)) { OwnershipRoutineLogic orl(storeOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - orl.emitMarkImmortal(transformed.getValue()); + // an array's header may be a constant one in read-only data, immortal already (TSLANG 61) + if (mlir::isa(storeOp.getValue().getType())) + { + orl.emitMarkImmortalIfMortal(transformed.getValue()); + } + else + { + orl.emitMarkImmortal(transformed.getValue()); + } } const bool isInvariantGroup = false; // TODO: Determine if this should be set based on some condition diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp index 17a9f31b8..12c9664e3 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -141,7 +141,11 @@ namespace own_signature llvm::MapVector> candidates; auto mth = typeHelper(&getContext()); module.walk([&](mlir_ts::GlobalOp globalOp) { - if (!mlir::isa(globalOp.getType()) || !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || + // a class's object, or an array (TSLANG 61: `const CUBE_VERTICES = [...]`, read into + // `[CUBE_VERTICES, PLANE_VERTICES]`): one block the global keeps, whose elements stay the + // array's, freed as any place's old value is when overwritten + if (!mlir::isa(globalOp.getType()) || + !mth.ownsHeapMemory(globalOp.getLoc(), globalOp.getType()) || globalOp->hasAttr("export") || globalOp->hasAttr("import") || globalOp->hasAttr(DLL_EXPORT) || globalOp->hasAttr(DLL_IMPORT) || !globalOp.getInitializerRegion().hasOneBlock()) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index bbfcbbc13..a67133ad7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1914,10 +1914,12 @@ endforeach() # TSLANG_ISSUES 58: a `const` declared from a merge of owning locals borrows them; a merge made in a loop's body # TSLANG_ISSUES 59: a value moved into a field of an element or field read, then used, borrows that field # TSLANG_ISSUES 60: a call that reaches an overridable method names it where a borrow ends +# TSLANG_ISSUES 61: an array global nothing assigns is immortal 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_58 00tslang_issue_59 00tslang_issue_60 + 00tslang_issue_61) 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() @@ -5471,7 +5473,8 @@ set(own_error_cases "own_err_merged_locals_overwritten|:11:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_merged_locals_moved|:12:5: error: '[^']*' is moved here while '[^']*' borrows it" "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_overridable_reached|:34:13: note: 'Prop.tick' calls 'setNodeTransform', which calls 'World.setTransform'") + "own_err_overridable_reached|:34:13: note: 'Prop.tick' calls 'setNodeTransform', which calls 'World.setTransform'" + "own_err_immortal_array_element_overwritten|:15:12: error: '[^']*' borrows 'G\\[\\]' 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_immortal_array_element_overwritten.ts b/tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts new file mode 100644 index 000000000..ce4ddf5f8 --- /dev/null +++ b/tslang/test/tester/own/own_err_immortal_array_element_overwritten.ts @@ -0,0 +1,18 @@ +// TSLANG_ISSUES 61: an array global nothing assigns is immortal, but only its own block: an element +// overwritten frees the old element, so a borrow of it ends there. +class V { + n: int; + constructor(n: int) { + this.n = n; + } +} + +const G = [new V(1), new V(2)]; + +export function f(): int { + const v = G[0]; + G[0] = new V(5); + return v.n; +} + +function main() { print(f()); } diff --git a/tslang/test/tester/tests/00tslang_issue_61.ts b/tslang/test/tester/tests/00tslang_issue_61.ts new file mode 100644 index 000000000..454990f37 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_61.ts @@ -0,0 +1,71 @@ +// TSLANG_ISSUES 61 (-mm=own), from native_ts_graphics' examples (nri/ray_tracing_reflection_nri.ts, ptlas.ts, +// nri/async_compute_bloom_nri.ts): module and namespace arrays nothing assigns, read into an array literal +// (`[CUBE_VERTICES, PLANE_VERTICES, CUBE_VERTICES]`), returned (`return GREEN;`) or held across calls +// (`const p = CAMERA_POSITION;`). An array global written once, by its initializer, is immortal, as a +// class's object global is (own spec 25): a read of it owns nothing and may go anywhere. + +class Mat { + r: number; + constructor(r: number) { + this.r = r; + } +} + +const CUBE = [1.0, 2.0, 3.0]; +const PLANE = [4.0, 5.0]; +const MATERIALS = [new Mat(0.5), new Mat(0.25)]; + +namespace Palette { + const GREEN = [0.0, 1.0, 0.0]; + const ORANGE = [1.0, 0.5, 0.0]; + + export function color(hot: boolean): number[] { + if (hot) { + return ORANGE; + } + + return GREEN; + } +} + +function touch(n: int): int { + return n + 1; +} + +function lists(): number { + const meshes = [CUBE, PLANE, CUBE]; + let total = 0.0; + for (const mesh of meshes) { + total += mesh[0]; + } + + return total; +} + +function held(): number { + const p = CUBE; + touch(1); + return p[2] + MATERIALS[1].r; +} + +class Model { + vertices: number[]; + materials: Mat[]; + constructor() { + this.vertices = PLANE; + this.materials = MATERIALS; + } +} + +function main() { + assert(lists() == 6.0); + assert(Palette.color(true)[1] == 0.5 && Palette.color(false)[1] == 1.0); + assert(held() == 3.25); + for (let i = 0; i < 3; i++) { + const m = new Model(); + assert(m.vertices[1] == 5.0 && m.materials[0].r == 0.5); + } + + assert(CUBE.length == 3 && MATERIALS.length == 2); + print("done."); +} From b87354bd989aa6fd68bb3a2aaa0dc1280019d6ec Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 16:47:11 +0000 Subject: [PATCH 4/4] own: an array changed through a local that borrows its field is a use of the borrow (TSLANG 62) `const model = this.refractionModel; model.push(0.0)` (native_ts_graphics examples) was refused as the local keeping the field's value: push, pop, unshift, shift and splice take the local's slot. The slot holds the field's array, so they are uses of the borrow (collectBorrowerUses). Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 15 ++++++- .../OwnershipInferenceBorrowers.cpp | 12 ++++++ tslang/test/tester/CMakeLists.txt | 6 ++- .../own_err_borrowed_array_pushed_returned.ts | 17 ++++++++ tslang/test/tester/tests/00tslang_issue_62.ts | 43 +++++++++++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 tslang/test/tester/own/own_err_borrowed_array_pushed_returned.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_62.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index bc8627562..a67bbe141 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -422,6 +422,19 @@ data, immortal already. Spec 25 has the amendment. In the sweep, 126 sources fai 29 errors to 1), and errors from 907 to 811. Regression tests `tslang/test/tester/tests/00tslang_issue_61.ts` (every model) and `own/own_err_immortal_array_element_overwritten.ts`. +62 (an array changed in place through a local that borrows the field holding it: `const model = +this.refractionModel; ... model.push(0.0);` in `examples/ray_tracing_extended.ts` and its NRI ports, refused +"borrows a field and cannot be stored, returned or captured" at the push) is fixed. `push`, `pop`, +`unshift`, `shift` and `splice` take the slot that holds the array's header, and the borrowing local's +slot was taken to keep the field's value. The header is the field's array, so the change is the field's +too: each such op through the local is now a use of the borrow, bounded as a read is. An element read +before a `pop` and used after, the field given another array before a use, and the local returned or +stored are still refused. A field that holds a null header (never given an array) gets one in the local's +slot only, which nothing frees: that is `undefined` in TypeScript terms, where `.push` throws, as the +array spec's ruling T4-D accepts for a parameter. In the sweep, 4 errors less (807); the two NRI ports +keep others. Regression tests `tslang/test/tester/tests/00tslang_issue_62.ts` (every model) and +`own/own_err_borrowed_array_pushed_returned.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")` @@ -432,7 +445,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`, 4 `core/` modules do not compile yet (`donut_first_person_camera.ts`, `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-61 in, 119 of the 276 sources fail. The + which #310 did not port: with every facts object built and 50-62 in, 119 of the 276 sources fail. The largest group left is "borrows a field but is used here after it may be released or overwritten" (about 200 sites), then "used here after its value was moved" (37: mostly a value a pushed object or an earlier call keeps, then returned or used). diff --git a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp index c3fc74463..7e48140fe 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp @@ -362,6 +362,18 @@ namespace own_inference continue; } + // `const model = this.model; model.push(0.0)` (TSLANG 62): the array changed in place, + // through the slot that holds its header - the borrowed array, which the field sees. A + // null header (an unset field) gets one in the local's slot only, which nothing frees: + // that is `undefined` in TypeScript terms, where `.push` throws (array spec, ruling T4-D) + if (mlir::isa(user) && + user->getOperand(0) == borrower.getResult()) + { + uses.push_back({user, {kills.begin(), kills.end()}}); + continue; + } + auto readOp = mlir::dyn_cast(user); if (!readOp) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a67133ad7..364fb8c77 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1915,11 +1915,12 @@ endforeach() # TSLANG_ISSUES 59: a value moved into a field of an element or field read, then used, borrows that field # TSLANG_ISSUES 60: a call that reaches an overridable method names it where a borrow ends # TSLANG_ISSUES 61: an array global nothing assigns is immortal +# TSLANG_ISSUES 62: an array changed through a local that borrows its field 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_61 00tslang_issue_62) 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() @@ -5474,7 +5475,8 @@ set(own_error_cases "own_err_merged_locals_moved|:12:5: error: '[^']*' is moved here while '[^']*' borrows it" "own_err_element_field_moved_replaced|:11:12: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_overridable_reached|:34:13: note: 'Prop.tick' calls 'setNodeTransform', which calls 'World.setTransform'" - "own_err_immortal_array_element_overwritten|:15:12: error: '[^']*' borrows 'G\\[\\]' but is used here after it may be released or overwritten") + "own_err_immortal_array_element_overwritten|:15:12: error: '[^']*' borrows 'G\\[\\]' but is used here after it may be released or overwritten" + "own_err_borrowed_array_pushed_returned|:14:5: error: '[^']*' borrows a field and cannot be stored, returned or captured") 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_borrowed_array_pushed_returned.ts b/tslang/test/tester/own/own_err_borrowed_array_pushed_returned.ts new file mode 100644 index 000000000..067e2bad1 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_array_pushed_returned.ts @@ -0,0 +1,17 @@ +// TSLANG_ISSUES 62: an array changed through a local that borrows the field holding it is still the +// field's: the local cannot be returned. +class V { + n = 1; +} + +class H { + arr: V[] = []; +} + +export function f(h: H): V[] { + const a = h.arr; + a.push(new V()); + return a; +} + +function main() { print(f(new H()).length); } diff --git a/tslang/test/tester/tests/00tslang_issue_62.ts b/tslang/test/tester/tests/00tslang_issue_62.ts new file mode 100644 index 000000000..6887e4d3e --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_62.ts @@ -0,0 +1,43 @@ +// TSLANG_ISSUES 62 (-mm=own), from native_ts_graphics' examples/ray_tracing_extended.ts: `const model = +// this.refractionModel; ... model.push(0.0);` - an array changed in place through a local that borrows +// the field holding it. The local's slot holds the field's array, so the field sees each change. + +class V { + n: int; + constructor(n: int) { + this.n = n; + } +} + +class Holder { + model: number[] = []; + items: V[] = [new V(1)]; + + fill(): number { + const model = this.model; + if (model.length == 0) { + for (let i = 0; i < 4; i++) { + model.push(1.5); + } + } + + return model.length; + } + + churn(): int { + const items = this.items; + items.push(new V(2)); + items.unshift(new V(0)); + items.splice(1, 1); + const first = items.shift(); + const last = items.pop(); + return (first ? first.n : -1) * 10 + (last ? last.n : -1) + items.length * 100; + } +} + +function main() { + const h = new Holder(); + assert(h.fill() == 4 && h.fill() == 4 && h.model.length == 4); + assert(h.churn() == 2 && h.items.length == 0); + print("done."); +}