From 98eb3eec7bdf7110312507719c0ae2be561db50a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:36:56 +0000 Subject: [PATCH 01/21] own: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read (TSLANG 71, 72) Co-Authored-By: Claude Opus 5.5 --- .../OwnershipSignatureFieldLocals.cpp | 143 ++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 6 +- .../own/own_err_kept_local_array_popped.ts | 23 +++ tslang/test/tester/tests/00tslang_issue_71.ts | 90 +++++++++++ 4 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 tslang/test/tester/own/own_err_kept_local_array_popped.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_71.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index eec7192e9..c5ffb7781 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -720,6 +720,71 @@ namespace own_signature // comes before the store (TSLANG 65: `let pipeline = null; if (ok) pipeline = make(); this. // pipelines[i] = pipeline; return pipeline;`), every later read of the local the store // dominates. False when the value is neither. + // Can control go from `from` to `to`, `to` after `from` in its block or in a block after - + // not through `avoided` (a local's declaration, which starts it afresh)? + auto reaches = [&](mlir::Operation *from, mlir::Operation *to, mlir::Block *avoided = nullptr) { + if (from->getBlock() == to->getBlock() && from->isBeforeInBlock(to)) + { + return true; + } + + llvm::SmallPtrSet seen; + llvm::SmallVector work(from->getBlock()->getSuccessors().begin(), from->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == to->getBlock()) + { + return true; + } + + if (next == avoided) + { + continue; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + + return false; + }; + + // Does control come back to `op` on a way that does not declare `slot` again first? + auto runsAgainUndeclared = [&](mlir::Operation *op, mlir_ts::VariableOp slot) { + if (op->getParentRegion() != slot->getParentRegion() || !dominance.properlyDominates(slot.getOperation(), op)) + { + return true; + } + + // the declaration's block, between it and `op` (in the same block: before it) + auto *declared = slot->getBlock(); + llvm::SmallPtrSet seen; + llvm::SmallVector work(op->getBlock()->getSuccessors().begin(), op->getBlock()->getSuccessors().end()); + while (!work.empty()) + { + auto *next = work.pop_back_val(); + if (next == declared) + { + continue; + } + + if (next == op->getBlock()) + { + return true; + } + + if (seen.insert(next).second) + { + work.append(next->getSuccessors().begin(), next->getSuccessors().end()); + } + } + + return false; + }; + auto usesAfterMove = [&](mlir::Operation *store, mlir::Value root, llvm::SmallVectorImpl &uses) { auto loadOp = root.getDefiningOp(); auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); @@ -743,9 +808,10 @@ namespace own_signature } // run once (not in a loop), so an assignment the store does not dominate runs before it - // or on a way that does not reach the reads after it + // or on a way that does not reach the reads after it - or once for each time the local + // is declared: `for (...) { const t = []; ...; new Node(t); t[0] }` (TSLANG 72) if (!own_facts::isOwningVariable(slot) || slot.getCaptured().value_or(false) || slot->getParentRegion() != body || - mayRunAgain(store)) + (mayRunAgain(store) && runsAgainUndeclared(store, slot))) { return false; } @@ -758,6 +824,14 @@ namespace own_signature return false; } + // an array filled in place through the local before the move: `t.push(x)` (TSLANG 72) + if (mlir::isa(user) && + !reaches(store, user, slot->getBlock())) + { + continue; + } + if (!mlir::isa(user)) { @@ -894,7 +968,10 @@ namespace own_signature auto arrayType = mlir::dyn_cast(mlir::cast(field.getType()).getElementType()); auto root = stripViews(pushOp.getItems().front()); auto *def = root.getDefiningOp(); - if (!arrayType || !ownValue(root, def) || !own_facts::ownsHeap(root) || + // ...or a read of an owning local (TSLANG 72), its later reads the uses (usesAfterMove) + auto rootLoad = def ? mlir::dyn_cast(def) : mlir_ts::LoadOp(); + auto slotRead = rootLoad && rootLoad.getReference().getDefiningOp(); + if (!arrayType || (!ownValue(root, def) && !slotRead) || !own_facts::ownsHeap(root) || own_facts::isCopyableValueAt(pushOp, root.getType()) || MLIRTypeHelper::isSharedHandleType(root.getType())) { continue; @@ -909,12 +986,22 @@ namespace own_signature } llvm::SmallVector uses; - for (auto &use : root.getUses()) + if (slotRead) + { + if (!usesAfterMove(pushOp, root, uses) || uses.empty()) + { + continue; + } + } + else { - auto *user = use.getOwner(); - if (!mlir::isa(user) && dominance.properlyDominates(pushOp.getOperation(), user)) + for (auto &use : root.getUses()) { - uses.push_back(&use); + auto *user = use.getOwner(); + if (!mlir::isa(user) && dominance.properlyDominates(pushOp.getOperation(), user)) + { + uses.push_back(&use); + } } } @@ -1010,9 +1097,18 @@ namespace own_signature auto *def = root.getDefiningOp(); auto fieldRefType = mlir::cast(kept.second); auto fieldType = fieldRefType.getElementType(); - auto narrowed = fieldType != root.getType() && mlir::isa(root.getType()) && - mlir::isa(fieldType); - if (!ownValue(root, def) || !own_facts::ownsHeap(root) || + // ...or a value narrowed to the field's class from that class or null: `const texture = + // load(); if (!texture) return; new Target(texture)` (TSLANG 71). Read back, the field + // is widened to the value's type again. + auto narrowed = fieldType != root.getType() && + ((mlir::isa(root.getType()) && + mlir::isa(fieldType)) || + (mlir::isa(fieldType) && + own_facts::holdsAsIs(root.getType(), fieldType, true))); + // ...or a read of an owning local (TSLANG 72), its later reads the uses (usesAfterMove) + auto rootLoad = def ? mlir::dyn_cast(def) : mlir_ts::LoadOp(); + auto slotRead = rootLoad && rootLoad.getReference().getDefiningOp(); + if ((!ownValue(root, def) && !slotRead) || !own_facts::ownsHeap(root) || own_facts::isCopyableValueAt(callOp, root.getType()) || MLIRTypeHelper::isSharedHandleType(root.getType()) || (fieldType != root.getType() && !narrowed)) { @@ -1040,12 +1136,31 @@ namespace own_signature }); }; - for (auto &use : root.getUses()) + if (slotRead) { - if (!mlir::isa(use.getOwner()) && dominance.properlyDominates(callOp, use.getOwner()) && - !afterRelease(use.getOwner())) + llvm::SmallVector later; + if (!usesAfterMove(callOp, root, later)) { - uses.push_back(&use); + continue; + } + + for (auto *use : later) + { + if (!afterRelease(use->getOwner())) + { + uses.push_back(use); + } + } + } + else + { + for (auto &use : root.getUses()) + { + if (!mlir::isa(use.getOwner()) && dominance.properlyDominates(callOp, use.getOwner()) && + !afterRelease(use.getOwner())) + { + uses.push_back(&use); + } } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 136340ef2..5f66ef72d 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1922,11 +1922,12 @@ endforeach() # 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-70: a value moved on every path, by a different move on each; a constant literal global given to a keeping call +# TSLANG_ISSUES 71-72: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read 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_69) + 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71) 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() @@ -5502,7 +5503,8 @@ set(own_error_cases "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_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only") + "own_err_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only" + "own_err_kept_local_array_popped|:16:22: 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_kept_local_array_popped.ts b/tslang/test/tester/own/own_err_kept_local_array_popped.ts new file mode 100644 index 000000000..3dd9f0064 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_local_array_popped.ts @@ -0,0 +1,23 @@ +// TSLANG_ISSUES 72: a local kept by a constructor reads the object's field after, so the object popped +// before the read frees it. +class Node2 { + constructor(public kind: int, public t: number[]) {} +} + +class App { + nodes: Node2[] = []; + first = 0.0; + + build(o: int) { + const t: number[] = [1.0]; + const node = new Node2(o, t); + this.nodes.push(node); + this.nodes.pop(); + this.first = t[0]; + } +} + +function main() { + new App().build(1); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_71.ts b/tslang/test/tester/tests/00tslang_issue_71.ts new file mode 100644 index 000000000..efaae6df2 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_71.ts @@ -0,0 +1,90 @@ +// TSLANG_ISSUES 71-72 (-mm=own), from native_ts_graphics: +// 71: examples/nri/raytracing_ao_nri.ts, `const texture = device.createStorageTexture(...); if (!texture) +// return null; const target = new Target(texture, ...); target.readView = texture.createView(format);` - +// a value narrowed from `T | null`, kept by a constructor, then used. +// 72: examples/nri/extended_dynamic_state2_nri.ts, `const t: number[] = []; for (...) t.push(...); const +// node = new SceneNode(..., t, ...); ... this.movingX = t[12];` in a loop - a local kept by a +// constructor, or pushed, then read; and `let p = make(); ...; this.ps.push(p); return p.k;`. + +class View { + constructor(public n: int) {} +} + +class Tex { + constructor(public n: int) {} + view(): View { + return new View(this.n); + } +} + +function load(n: int): Tex | null { + return n > 0 ? new Tex(n) : null; +} + +class Target { + view: View | null = null; + constructor(public tex: Tex, public state: int) {} +} + +class Node2 { + constructor(public kind: int, public t: number[]) {} +} + +class App { + targets: Target[] = []; + nodes: Node2[] = []; + others: Node2[] = []; + texs: Tex[] = []; + first = 0.0; + + target(n: int): Target | null { + const tex = load(n); + if (!tex) { + return null; + } + const target = new Target(tex, 1); + target.view = tex.view(); + this.targets.push(target); + return target; + } + + build(n: int) { + for (let o = 0; o < n; o++) { + const t: number[] = []; + for (let i = 0; i < 3; i++) { + t.push(o * 10 + i); + } + const node = new Node2(o % 2, t); + if (o % 2 == 0) { + this.nodes.push(node); + } else { + if (o == 1) { + this.first = t[0]; + } + this.others.push(node); + } + } + } + + keep(n: int, again: boolean): int { + let tex = load(n); + if (again) { + tex = load(n + 1); + } + if (!tex) { + return -1; + } + this.texs.push(tex); + return tex.n; + } +} + +function main() { + const app = new App(); + const t = app.target(3); + assert(t != null && t.view != null && t.view.n == 3 && app.target(0) == null); + app.build(4); + assert(app.first == 10.0 && app.nodes.length == 2 && app.others[1].t[2] == 32.0); + assert(app.keep(2, false) == 2 && app.keep(2, true) == 3 && app.keep(0, false) == -1 && app.texs.length == 2); + print("done."); +} From 6d342b9b0879180c93616438093fd92a8ee00653 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:37:42 +0000 Subject: [PATCH 02/21] TSLANG_ISSUES 71-72 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 953203fd7..4d844c2d4 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -527,6 +527,26 @@ type is the literal, so it holds that literal and nothing else, in read-only dat 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. +71 (a value narrowed from `T | null`, kept by a constructor, then used: `const texture = +device.createStorageTexture(...); if (!texture) return null; const target = new Target(texture, ...); +target.readView = texture.createView(format);` in `examples/nri/raytracing_ao_nri.ts`, refused "is used here +after its value was moved") is fixed. TSLANG 53 reads the field the constructor keeps a value in for each use +after the call, but only for a value of the field's type, or a class value kept in a union field. A value of +the field's class or null, narrowed by the test before, is now one too: the field read back is widened to +the value's type again. The object popped before the use is still refused. Regression test +`tslang/test/tester/tests/00tslang_issue_71.ts` (every model; it holds 72 too). + +72 (a local, rather than a value, kept by a constructor or pushed, then read: `const t: number[] = []; for (let +i = 0; i < 16; i++) t.push(transform[i]); const node = new SceneNode(..., t, ...); ... this.movingX = t[12];` +in a loop in `examples/nri/extended_dynamic_state2_nri.ts`, and `let pipeline = ...; ...; +this.pipelines.push(pipeline); return pipeline.n;`) is fixed. TSLANG 65's reads of a local after a store (the +local assigned nowhere after it, its address taken nowhere) now serve the constructor and the push too. A +store in a loop is taken when the local is declared in that loop, so each time round starts it afresh, and an +array filled in place through the local is taken when the filling comes before the move. The local pushed to +after the move, declared outside the loop that moves it, or its new owner popped before the read, is still +refused. In the sweep, 71 and 72 take 20 errors, and `extended_dynamic_state2.ts` compiles. Regression test +`own/own_err_kept_local_array_popped.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")` @@ -537,7 +557,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-70 in, 116 of the 276 sources fail (635 errors). The + which #310 did not port: with every facts object built and 50-72 in, 115 of the 276 sources fail (615 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). From 63ae7e9c7603ee68a0ae6441139a13eb0f809d85 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:44:24 +0000 Subject: [PATCH 03/21] own: a private field nothing writes after the constructor is not overwritten by a call (TSLANG 73) Co-Authored-By: Claude Opus 5.5 --- tslang/include/TypeScript/Defines.h | 2 + tslang/lib/TypeScript/OwnershipFacts.h | 19 ++++++++ .../OwnershipInferencePlaceReads.cpp | 17 ++++++++ .../lib/TypeScript/OwnershipSignaturePass.cpp | 39 +++++++++++++++++ tslang/test/tester/CMakeLists.txt | 6 ++- .../own_err_private_field_written_callback.ts | 43 +++++++++++++++++++ tslang/test/tester/tests/00tslang_issue_73.ts | 41 ++++++++++++++++++ 7 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_err_private_field_written_callback.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_73.ts diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 83d9a0dd7..e31e9a952 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -160,6 +160,8 @@ #define OWN_BORROWING_FIELDS_ATTR_NAME "ts.own_borrowing_fields" // [class storage type, field position] of each field a back-reference is stored into (own spec 70) #define OWN_BACK_REFERENCE_FIELDS_ATTR_NAME "ts.own_back_reference_fields" +// the private fields of this module's classes nothing writes after the constructor (TSLANG 73) +#define OWN_STABLE_FIELDS_ATTR_NAME "ts.own_stable_fields" // -mm=own only, set by OwnershipSignaturePass on the module, in the same shape: the fields of a // generator's state object - its locals - read only to be yielded (`const groups = [...r]; yield // groups`). A yield moves what such a field holds to the caller, so the field never owns it: an diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 4f41d699f..16de18f11 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -2327,6 +2327,25 @@ inline mlir_ts::ClassStorageType fieldOwnerStorage(mlir_ts::PropertyRefOp proper return mlir::dyn_cast(type); } +// Is `ref` a private field nothing writes after its object's constructor (OWN_STABLE_FIELDS_ATTR_NAME, the +// signature pass's, TSLANG 73)? +inline bool isStableField(mlir::Value ref) +{ + auto propertyRefOp = ref.getDefiningOp(); + auto storage = propertyRefOp ? fieldOwnerStorage(propertyRefOp) : mlir_ts::ClassStorageType(); + auto module = propertyRefOp ? propertyRefOp->getParentOfType() : mlir::ModuleOp(); + auto entries = module ? module->getAttrOfType(OWN_STABLE_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!storage || !entries) + { + return false; + } + + return llvm::any_of(entries.getAsRange(), [&](mlir::ArrayAttr entry) { + return mlir::cast(entry[0]).getValue() == storage && + mlir::cast(entry[1]).getInt() == propertyRefOp.getPosition(); + }); +} + // Is `ref` a field some back-reference is stored into (OWN_BACK_REFERENCE_FIELDS_ATTR_NAME, the // signature pass's)? What it holds may borrow the object it is in, so it is never taken out of it inline bool isBackReferenceField(mlir::Value ref) diff --git a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp index cb9f1c104..1c92464e9 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePlaceReads.cpp @@ -691,6 +691,23 @@ namespace own_inference return false; } + // a private field nothing writes after construction, read straight off a parameter or `this` + // (TSLANG 73): no call overwrites it, and it is released only with its object, which the + // caller holds while this function runs + if (read && chain.places.size() == 1 && !chain.anyPlace) + { + auto loadOp = mlir::dyn_cast(read); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto objectLoad = propertyRefOp ? propertyRefOp.getObjectRef().getDefiningOp() : mlir_ts::LoadOp(); + auto objectSlot = objectLoad ? objectLoad.getReference().getDefiningOp() : mlir_ts::VariableOp(); + int index = -1; + if (propertyRefOp && isStableField(propertyRefOp.getResult()) && objectSlot && isParameterSlot(objectSlot, index) && + isUnassignedSlot(objectSlot.getResult())) + { + return false; + } + } + // a call given a closure may run it, and its box may hold anything auto givenClosure = llvm::any_of(drop->getOperands(), holdsCaptures); auto mayDrop = true; diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 393da39c8..a71110e7a 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -144,6 +144,45 @@ namespace own_signature mlir::ArrayAttr::get(&getContext(), backReferenceFields.getArrayRef())); } + // the private fields nothing writes but their class's constructor (TSLANG 73): `constructor(private + // sample: NriSample)`. Only the class's own methods may write a private field, and they are all here + // (its constructor is), so once the object is made no call - a callback included - overwrites it: + // the field is released only with its object. + llvm::DenseMap, bool> writtenAfterConstruction; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + auto storage = fieldOwnerStorage(propertyRefOp); + if (!storage) + { + return; + } + + auto &written = writtenAfterConstruction[{storage, propertyRefOp.getPosition()}]; + auto funcOp = propertyRefOp->getParentOfType(); + auto inConstructor = funcOp && funcOp.getSymName() == (storage.getName().getAttr().getValue() + ".constructor").str(); + auto onlyRead = llvm::all_of(propertyRefOp->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); + written = written || (!onlyRead && !inConstructor); + }); + llvm::SetVector stableFields; + for (auto &[key, written] : writtenAfterConstruction) + { + auto storage = mlir::cast(key.first); + auto constructor = functions.lookup((storage.getName().getAttr().getValue() + ".constructor").str()); + auto fields = storage.getFields(); + if (written || !constructor || constructor.isExternal() || key.second < 0 || + static_cast(key.second) >= fields.size() || fields[key.second].accessLevel != mlir_ts::AccessLevel::Private) + { + continue; + } + + stableFields.insert(mlir::ArrayAttr::get( + &getContext(), {mlir::TypeAttr::get(storage), mlir::IntegerAttr::get(mlir::IntegerType::get(&getContext(), 32), key.second)})); + } + + if (!stableFields.empty()) + { + module->setAttr(OWN_STABLE_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(&getContext(), stableFields.getArrayRef())); + } + if (auto functionsDropping = module->getAttrOfType(SHARED_LIB_OWN_DROP_FIELDS_ATTR_NAME)) { for (auto entry : functionsDropping) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 5f66ef72d..47d40caa0 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1923,11 +1923,12 @@ endforeach() # 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-70: a value moved on every path, by a different move on each; a constant literal global given to a keeping call # TSLANG_ISSUES 71-72: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read +# TSLANG_ISSUES 73: a private field nothing writes after the constructor, borrowed across a call that runs callbacks 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_69 00tslang_issue_71) + 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73) 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() @@ -5504,7 +5505,8 @@ set(own_error_cases "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_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only" - "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field 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_private_field_written_callback.ts b/tslang/test/tester/own/own_err_private_field_written_callback.ts new file mode 100644 index 000000000..86338753e --- /dev/null +++ b/tslang/test/tester/own/own_err_private_field_written_callback.ts @@ -0,0 +1,43 @@ +// TSLANG_ISSUES 73: a private field another method writes may be overwritten by a callback the call +// runs, so a borrow of it across the call is refused. +const retained: ((n: int) => void)[] = []; + +class Sample { + frames = 0; + callback: ((n: int) => void) | null = null; + run(count: int): int { + for (let i = 0; i < count; i++) { + const callback = this.callback; + if (callback) { + callback(i); + } + this.frames++; + } + return this.frames; + } +} + +class App { + total = 0; + constructor(private sample: Sample) {} + reset() { + this.sample = new Sample(); + } + frame(n: int) { + this.total += n; + } + run(): int { + const frame: (n: int) => void = this.frame; + retained.push(frame); + this.sample.callback = frame; + const frames = this.sample.run(3); + this.sample.callback = null; + return frames + this.total; + } +} + +function main() { + const app = new App(new Sample()); + assert(app.run() == 3 + 0 + 1 + 2); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_73.ts b/tslang/test/tester/tests/00tslang_issue_73.ts new file mode 100644 index 000000000..24274189e --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_73.ts @@ -0,0 +1,41 @@ +// TSLANG_ISSUES 73 (-mm=own), from native_ts_graphics' NRI examples: `constructor(private sample: NriSample)` +// and `this.sample.run(...)`, which runs callbacks into the app. A private field nothing writes after the +// constructor is not overwritten by them, so the borrow of it holds across the call. +const retained: ((n: int) => void)[] = []; + +class Sample { + frames = 0; + callback: ((n: int) => void) | null = null; + run(count: int): int { + for (let i = 0; i < count; i++) { + const callback = this.callback; + if (callback) { + callback(i); + } + this.frames++; + } + return this.frames; + } +} + +class App { + total = 0; + constructor(private sample: Sample) {} + frame(n: int) { + this.total += n; + } + run(): int { + const frame: (n: int) => void = this.frame; + retained.push(frame); + this.sample.callback = frame; + const frames = this.sample.run(3); + this.sample.callback = null; + return frames + this.total; + } +} + +function main() { + const app = new App(new Sample()); + assert(app.run() == 3 + 0 + 1 + 2); + print("done."); +} From 24a77745b4f81867256920ed51faa7a292910ac5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:46:18 +0000 Subject: [PATCH 04/21] TSLANG_ISSUES: 73, ntg changes and the sweep's state Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 4d844c2d4..adb348340 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -547,6 +547,16 @@ after the move, declared outside the loop that moves it, or its new owner popped refused. In the sweep, 71 and 72 take 20 errors, and `extended_dynamic_state2.ts` compiles. Regression test `own/own_err_kept_local_array_popped.ts`. +73 (a private field read off a parameter or `this` around a call: `constructor(private sample: NriSample)`, then +`this.sample.run(() => ...)` and a later `this.sample` read in the examples' `App` classes, refused "borrows a +field but is used here after it may be released or overwritten") is fixed. Any call could have overwritten the +field (a callback that writes it, say), so the read before it was not trusted after it. Only the class's own +methods can write a private field, and they are all in the module. So the signature pass now marks each private +field that nothing but its class's constructor writes, and a call no longer drops such a field read off a +parameter (`this` included) the function does not assign. The object is the caller's, so the field lives as long +as the call does. A private field a method writes, called back from the call, is still refused. Regression tests +`tslang/test/tester/tests/00tslang_issue_73.ts` (every model) and `own/own_err_private_field_written_callback.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")` @@ -555,9 +565,14 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): - `examples/dynamic_primitive_clipping.ts:223`, `desc.setDepthClip(clip)` with `clip` the loop's `int`, fails under every model since #846 (a number is not a `boolean` argument, TS2345). alpha127 still 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-72 in, 115 of the 276 sources fail (615 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). +- `core/nri_donut_textures.ts`: `MipBlit` held the texture it blits, and `recordBlits` read it back after the + blits array kept it, so the module and the 9 examples importing it failed under `-mm=own` (199 errors). A + `MipBlit` now holds the texture's index into `this.textures`. `MsvcRandom` (`core/random.ts`) and `GameWorld` + (`core/rtxpt_game.ts`) are now `@final`: a class that can be derived from may have its methods overridden, so + `-mm=own` cannot read their facts (about 48 errors). These are on ntg branch `own-sweep-fixes`. +- Under `-mm=own`, 2 `core/` modules do not compile yet (`ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and + nor do many examples, which #310 did not port: with every facts object built, 50-73 in and the changes above, + 58 of the 276 sources fail (266 errors). The largest groups left are "borrows a field but is used here after + it may be released or overwritten" (64), "borrows a field and cannot be stored, returned or captured" (50), + "used here after its value was moved" (32) and "borrows the result of a call and cannot be stored, returned or + captured" (32). From 3b25bb2caca800091ccea1c4ca237c41e0089a1c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 18:58:17 +0000 Subject: [PATCH 05/21] own_err_declared_drops: a public field, which TSLANG 73 does not keep Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/own/own_err_declared_drops.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tslang/test/tester/own/own_err_declared_drops.ts b/tslang/test/tester/own/own_err_declared_drops.ts index 85eecdc8e..997001dec 100644 --- a/tslang/test/tester/own/own_err_declared_drops.ts +++ b/tslang/test/tester/own/own_err_declared_drops.ts @@ -1,5 +1,5 @@ -// -mm=own rejects: `strlen` is declared without `@nodrops`, so the method that calls it may -// destroy anything, and `this.value` cannot be lent to it (own_declared_no_drops). +// -mm=own rejects: `strlen` is declared without `@nodrops`, so the method that calls it may destroy anything, +// and `this.value` (public: not one TSLANG 73 keeps) cannot be lent to it (own_declared_no_drops). declare function strlen(s: string): index; namespace __S { @@ -9,7 +9,7 @@ namespace __S { } class W { - constructor(private value: string) {} + constructor(public value: string) {} size(): int { return this.value.size(); From bc9de723fdd7bb4c723ce409a4d3e77726cb0e84 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:00:50 +0000 Subject: [PATCH 06/21] own: a ?: of null and another module's call, pushed where not null, then used (TSLANG 74) readFieldsForLocals runs before the signature pass marks imported calls' results fresh, so a merge of null and such a call was not taken as the function's own, though the call's result alone is. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipFacts.h | 10 +++--- .../OwnershipSignatureFieldLocals.cpp | 6 ++-- tslang/test/tester/own-facts-objects.cmake | 23 ++++++++++++- tslang/test/tester/tests/own_facts_tex_lib.ts | 21 ++++++++++++ .../test/tester/tests/own_facts_tex_main.ts | 32 +++++++++++++++++++ 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 tslang/test/tester/tests/own_facts_tex_lib.ts create mode 100644 tslang/test/tester/tests/own_facts_tex_main.ts diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 16de18f11..b32f3fc2e 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -591,13 +591,15 @@ inline bool takenOutOfBorrowedRest(mlir::Operation *def) } inline bool isFresh(mlir::Value value); +inline bool isCall(mlir::Operation *op); // A value merged from branches (TSLANG 54): `const p = loaded ? device.create() : null`. It is the // function's own when every way into the merge brings a fresh value that nothing else uses, or null // or undefined, which holds no block - rc gave it the one reference the merge holds. Not across a // loop: a block that reaches one of its own predecessors carries a value from one iteration into the -// next, which is not decided here. -inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) +// next, which is not decided here. `anyCall`: a call's result counts as fresh too - for a step that runs +// before the calls' facts are known, as it takes an unmerged call's result (TSLANG 74). +inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0, bool anyCall = false) { auto *block = argument.getOwner(); if (block->isEntryBlock() || block->hasNoPredecessors() || depth > 8) @@ -648,7 +650,7 @@ inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) if (auto nested = mlir::dyn_cast(incoming)) { - if (!nested.hasOneUse() || !isFreshMerge(nested, depth + 1)) + if (!nested.hasOneUse() || !isFreshMerge(nested, depth + 1, anyCall)) { return false; } @@ -693,7 +695,7 @@ inline bool isFreshMerge(mlir::BlockArgument argument, unsigned depth = 0) continue; } - if (!isFresh(value)) + if (!isFresh(value) && !(anyCall && def && isCall(def))) { return false; } diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index c5ffb7781..b57667522 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -297,12 +297,14 @@ namespace own_signature } // A value the steps below may follow to where it moves: made here (not a read of a local or a - // place), or merged from branches that each bring a fresh value or null (TSLANG 54). + // place), or merged from branches that each bring a fresh value or null (TSLANG 54) - a call's + // result among them, as unmerged: `cond ? null : device.loadTexture(...)` of another module, whose + // facts are not read yet (TSLANG 74). bool ownValue(mlir::Value root, mlir::Operation *def) { if (auto argument = mlir::dyn_cast(root)) { - return own_facts::isFreshMerge(argument); + return own_facts::isFreshMerge(argument, 0, true); } return def && !mlir::isa(def); diff --git a/tslang/test/tester/own-facts-objects.cmake b/tslang/test/tester/own-facts-objects.cmake index aae48f9c8..767ad0829 100644 --- a/tslang/test/tester/own-facts-objects.cmake +++ b/tslang/test/tester/own-facts-objects.cmake @@ -19,7 +19,7 @@ set(ENV{TSLANG_LIB_PATH} "") file(REMOVE_RECURSE "${WORK_DIR}") file(MAKE_DIRECTORY "${WORK_DIR}") # the importer finds `./own_facts_lib` beside it, as a source to read declarations from -foreach(file own_facts_lib.ts own_facts_main.ts own_facts_main_drops.ts) +foreach(file own_facts_lib.ts own_facts_main.ts own_facts_main_drops.ts own_facts_tex_lib.ts own_facts_tex_main.ts) file(COPY "${SOURCE_DIR}/${file}" DESTINATION "${WORK_DIR}") endforeach() @@ -68,3 +68,24 @@ execute_process(COMMAND "${WORK_DIR}/own_facts.exe" if(NOT status EQUAL 0 OR NOT "${out}" MATCHES "^11[\r\n]+done[.]") message(FATAL_ERROR "own_facts.exe: exit ${status}\n${out}${err}") endif() + +# TSLANG 74: `cond ? null : device.load(...)` of the library's, pushed where not null, then used +tslang_run(tex_lib --emit=obj -mm=own own_facts_tex_lib.ts -o own_facts_tex_lib.obj) +if(NOT status EQUAL 0 OR output MATCHES "error:|Stack dump") + message(FATAL_ERROR "own_facts_tex_lib: exit ${status}\n${output}") +endif() + +tslang_run(tex_link --emit=exe -mm=own "--llvm-lib-path=${LLVM_LIB}" "--tslang-lib-path=${TSLANG_LIB}" + --own-facts=own_facts_tex_lib.obj --obj=own_facts_tex_lib.obj own_facts_tex_main.ts -o own_facts_tex.exe) +if(NOT status EQUAL 0 OR output MATCHES "error:|Stack dump") + message(FATAL_ERROR "own_facts_tex_main with --own-facts: exit ${status}\n${output}") +endif() + +execute_process(COMMAND "${WORK_DIR}/own_facts_tex.exe" + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) +if(NOT status EQUAL 0 OR NOT "${out}" MATCHES "^false 2 12 16[\r\n]+done[.]") + message(FATAL_ERROR "own_facts_tex.exe: exit ${status}\n${out}${err}") +endif() diff --git a/tslang/test/tester/tests/own_facts_tex_lib.ts b/tslang/test/tester/tests/own_facts_tex_lib.ts new file mode 100644 index 000000000..32ede2ab5 --- /dev/null +++ b/tslang/test/tester/tests/own_facts_tex_lib.ts @@ -0,0 +1,21 @@ +// TSLANG 74's library (own-facts-objects.cmake): a texture loader whose result is the caller's own, +// or null. +@final +export class Texture { + constructor(public size: number) {} + + format(): number { + return 4; + } + + view(format: number): number { + return this.size * format; + } +} + +@final +export class Device { + load(name: string): Texture | null { + return name.length > 1 ? new Texture(name.length) : null; + } +} diff --git a/tslang/test/tester/tests/own_facts_tex_main.ts b/tslang/test/tester/tests/own_facts_tex_main.ts new file mode 100644 index 000000000..7807f4b77 --- /dev/null +++ b/tslang/test/tester/tests/own_facts_tex_main.ts @@ -0,0 +1,32 @@ +// TSLANG 74 (own-facts-objects.cmake): a value of `cond ? null : f()`, f another module's, pushed where +// it is not null and then used, reads the array's element - the merge is the function's own, as `f()` +// alone is, though the signature pass marks the imported call's result fresh only after it looks. +import { Texture, Device } from "./own_facts_tex_lib"; + +class Materials { + textures: Texture[] = []; + views: number[] = []; + + add(device: Device, names: string[]): boolean { + let ok = true; + for (let m = 0; m < names.length; m++) { + const name = names[m]; + const texture = name == "" ? null : device.load(name); + if (!texture) { + ok = false; + } else { + this.textures.push(texture); + this.views.push(texture.view(texture.format())); + } + } + + return ok; + } +} + +function main() { + const materials = new Materials(); + const ok = materials.add(new Device(), ["abc", "", "x", "wxyz"]); + print(ok, materials.textures.length, materials.views[0], materials.views[1]); + print("done."); +} From cc460b57c9dd9bb61797e3de0348a80dee8c83cc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:01:33 +0000 Subject: [PATCH 07/21] TSLANG_ISSUES 74 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index adb348340..0057e4edd 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -556,6 +556,17 @@ field that nothing but its class's constructor writes, and a call no longer drop parameter (`this` included) the function does not assign. The object is the caller's, so the field lives as long as the call does. A private field a method writes, called back from the call, is still refused. Regression tests `tslang/test/tester/tests/00tslang_issue_73.ts` (every model) and `own/own_err_private_field_written_callback.ts`. +`own/own_err_declared_drops.ts` lends a public field now: the private one it had is a field 73 keeps. + +74 (a value of `cond ? null : f(...)`, with `f` another module's, pushed where it is not null, then used: `const +texture = textureName == "" ? null : device.loadTexture(...); if (!texture) ... else { this.materialTextures.push(texture); +this.materialViews.push(texture.createView(texture.getFormat())); }` in `examples/nri/mouse_cursor_nri.ts`, refused +"is used here after its value was moved") is fixed. TSLANG 65's push step reads such a value back from the array +when the value is the function's own. A call's result alone counts, whoever defines the function. A merge of a +call's result and null (TSLANG 54) counted only when the call's result was known to be fresh, which, for another +module's function, the signature pass decides only after this step. A merge now counts when each way into it +brings null or a call's result, as the call alone does. Regression test: TSLANG 74's step in +`tslang/test/tester/own-facts-objects.cmake` (`tests/own_facts_tex_lib.ts`, `tests/own_facts_tex_main.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 From 29032f92c1c1f78630a44ab4fd14e6b2aa0e78cc Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:02:36 +0000 Subject: [PATCH 08/21] TSLANG_ISSUES: the ntg changes and the sweep with 74 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 0057e4edd..36485c274 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -580,10 +580,16 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): blits array kept it, so the module and the 9 examples importing it failed under `-mm=own` (199 errors). A `MipBlit` now holds the texture's index into `this.textures`. `MsvcRandom` (`core/random.ts`) and `GameWorld` (`core/rtxpt_game.ts`) are now `@final`: a class that can be derived from may have its methods overridden, so - `-mm=own` cannot read their facts (about 48 errors). These are on ntg branch `own-sweep-fixes`. + `-mm=own` cannot read their facts (about 48 errors). So are `AtomOrbitals`, `OrbitalsPass`, `Pose` and + `RtxptGame` now; a class that can be derived from turns each call of its methods into one that may + destroy anything. +- The `*_nri.ts` examples' `QueuedFrame` kept `this.frames.commandBuffer(i)`, a borrow of the frame ring's, + which a field cannot hold: it is read from `this.frames` where it is recorded (16 examples). The + `atom_orbitals` examples set two fields to one `Orbital` (`this.orbitalA = this.orbitalB`), a second owner: + each field gets its own. These are on ntg branch `own-sweep-fixes`. - Under `-mm=own`, 2 `core/` modules do not compile yet (`ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and - nor do many examples, which #310 did not port: with every facts object built, 50-73 in and the changes above, - 58 of the 276 sources fail (266 errors). The largest groups left are "borrows a field but is used here after - it may be released or overwritten" (64), "borrows a field and cannot be stored, returned or captured" (50), - "used here after its value was moved" (32) and "borrows the result of a call and cannot be stored, returned or - captured" (32). + nor do many examples, which #310 did not port: with every facts object built, 50-74 in and the changes above, + 48 of the 276 sources fail (194 errors). The largest groups left are "borrows a field but is used here after + it may be released or overwritten" (36), "borrows a field and cannot be stored, returned or captured" (31), + "used here after its value was moved" (27), and an element borrowed and stored (21) or used after it may be + released (20). From 16fcc6628f4c4d1f6975aeb636452b3631f4089f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:05:32 +0000 Subject: [PATCH 09/21] own: a call through a global nothing assigns, given a function by its initializer, is a call of that function (TSLANG 75) `const f = Math.fround` in clustered_geometry made every f(...) a call that may destroy anything. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipSignaturePass.h | 2 + .../TypeScript/OwnershipSignatureResolve.cpp | 35 ++++++++++++++++ tslang/test/tester/CMakeLists.txt | 7 +++- .../tester/own/own_err_const_alias_drops.ts | 27 +++++++++++++ tslang/test/tester/tests/00tslang_issue_75.ts | 40 +++++++++++++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/own/own_err_const_alias_drops.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_75.ts diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 958d93547..929504fc5 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -715,6 +715,8 @@ class OwnershipSignaturePass : public mlir::PassWrapper(def) || (def && isCall(def))) { // a callback parameter, field or local; or what a call returned (spec 56) @@ -467,6 +473,35 @@ namespace own_signature resolveFamily(index, identifier, callees); } + mlir::StringAttr OwnershipSignaturePass::globalFunction(mlir::Operation *def) + { + // `ts.Load(ts.AddressOf @g)` of a global no store writes, whose initializer gives it a + // function's symbol, seen through its casts + auto loadOp = mlir::dyn_cast_or_null(def); + auto addressOfOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::AddressOfOp(); + if (!addressOfOp || writtenGlobals.contains(addressOfOp.getGlobalName())) + { + return {}; + } + + auto globalOp = globals.lookup(addressOfOp.getGlobalName()); + auto *block = globalOp ? globalOp.getInitializerBlock() : nullptr; + auto resultOp = block && !block->empty() ? mlir::dyn_cast(block->getTerminator()) : mlir_ts::GlobalResultOp(); + if (!resultOp || resultOp->getNumOperands() != 1) + { + return {}; + } + + auto value = resultOp->getOperand(0); + while (auto castOp = value.getDefiningOp()) + { + value = castOp.getIn(); + } + + auto symbolRefOp = value.getDefiningOp(); + return symbolRefOp ? symbolRefOp.getIdentifierAttr().getAttr() : mlir::StringAttr(); + } + void OwnershipSignaturePass::resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) { callees.known = true; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 47d40caa0..6dbeb5717 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1924,11 +1924,13 @@ endforeach() # 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 # TSLANG_ISSUES 71-72: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read # TSLANG_ISSUES 73: a private field nothing writes after the constructor, borrowed across a call that runs callbacks +# TSLANG_ISSUES 75: a call through a constant global that holds a function, a call of that function 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_69 00tslang_issue_71 00tslang_issue_73) + 00tslang_issue_61 00tslang_issue_62 00tslang_issue_63 00tslang_issue_66 00tslang_issue_67 00tslang_issue_69 00tslang_issue_71 00tslang_issue_73 + 00tslang_issue_75) 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() @@ -5506,7 +5508,8 @@ set(own_error_cases "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" "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten") + "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field 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_const_alias_drops.ts b/tslang/test/tester/own/own_err_const_alias_drops.ts new file mode 100644 index 000000000..82e5b44e5 --- /dev/null +++ b/tslang/test/tester/own/own_err_const_alias_drops.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `reset`, called through the constant `clear`, empties the mesh's clusters, so +// the cluster borrowed from them is used after it may be released (TSLANG 75 resolves the call to +// `reset`, whose facts say so). +class Cluster { + positions: number[] = [1, 2, 3]; +} + +class Mesh { + clusters: Cluster[] = [new Cluster()]; +} + +function reset(mesh: Mesh) { + mesh.clusters = []; +} + +const clear = reset; + +function sum(meshes: Mesh[]) { + const mesh = meshes[0]; + const c = mesh.clusters[0]; + clear(mesh); + print(c.positions.length); +} + +function main() { + sum([new Mesh()]); +} diff --git a/tslang/test/tester/tests/00tslang_issue_75.ts b/tslang/test/tester/tests/00tslang_issue_75.ts new file mode 100644 index 000000000..e534621c0 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_75.ts @@ -0,0 +1,40 @@ +// TSLANG 75: a call through a global nothing assigns, given a function by its initializer, is a +// call of that function: `const f = Math.fround` destroys nothing, so the cluster borrowed from a +// parameter's mesh outlives it. +namespace Clusters { + const f = Math.fround; + function twice(x: number) { + return x * 2; + } + const g = twice; + + export class Cluster { + positions: number[] = [1, 2, 3, 4]; + } + + export class Mesh { + clusters: Cluster[] = [new Cluster(), new Cluster()]; + } + + export function centroid(meshes: Mesh[]): number { + const mesh = meshes[0]; + let sum = 0.0; + for (const c of mesh.clusters) { + let y = 0.0; + for (let v = 0; v < c.positions.length; v++) { + y = f(y + c.positions[v]); + y = g(y); + } + sum = sum + y / c.positions.length; + } + + return sum; + } +} + +function main() { + const meshes = [new Clusters.Mesh()]; + const c = Clusters.centroid(meshes); + assert(c == 26, "centroid"); + print("done."); +} From 778a40de1dd0f95a5d771018995de0ae1e1e1184 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:06:42 +0000 Subject: [PATCH 10/21] TSLANG_ISSUES 75 and what clustered_geometry has left Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 36485c274..c04e0af36 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -568,6 +568,15 @@ module's function, the signature pass decides only after this step. A merge now brings null or a call's result, as the call alone does. Regression test: TSLANG 74's step in `tslang/test/tester/own-facts-objects.cmake` (`tests/own_facts_tex_lib.ts`, `tests/own_facts_tex_main.ts`). +75 (a call through a constant that holds a function: `const f = Math.fround;` in `examples/clustered_geometry.ts`, +then `centroidY = f(centroidY + c.positions[v])` with `c` a cluster of a parameter's mesh, refused "borrows a field +but is used here after it may be released or overwritten" at each later use of `c`) is fixed. The signature pass +took a call through a global's value as one through a callback it knows nothing of, so the call could destroy +anything. A global that no store writes, and that its initializer gives a function's symbol, now resolves to that +function: `f(...)` is a call of `Math.fround`, which destroys nothing. A global something assigns is still +unknown. In the sweep, `clustered_geometry.ts` goes from 44 errors to 16. Regression tests +`tslang/test/tester/tests/00tslang_issue_75.ts` (every model) and `own/own_err_const_alias_drops.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")` @@ -587,6 +596,10 @@ For native_ts_graphics, from the same sweep (its code, not the compiler): which a field cannot hold: it is read from `this.frames` where it is recorded (16 examples). The `atom_orbitals` examples set two fields to one `Orbital` (`this.orbitalA = this.orbitalB`), a second owner: each field gets its own. These are on ntg branch `own-sweep-fixes`. +- Left in `examples/clustered_geometry.ts` (ntg's call): `Cluster.clone` and `SceneObject.copy` share the + vertex, index and material arrays on purpose ("which nothing changes"). That gives each array two owners, + so either the copies copy them (`slice()`) or the arrays are `Shared`. The `add` closure keeps its mesh and + position and returns the object it pushed; a function, or the caller pushing, would say that. - Under `-mm=own`, 2 `core/` modules do not compile yet (`ray_tracing_extended_scene.ts`, `rtxpt_game.ts`), and nor do many examples, which #310 did not port: with every facts object built, 50-74 in and the changes above, 48 of the 276 sources fail (194 errors). The largest groups left are "borrows a field but is used here after From 69a824d977bb67783f99c180452cc0547f39f5b4 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:13:16 +0000 Subject: [PATCH 11/21] own: a constructor that gives `this` to a call keeps a parameter in a private field only it writes (TSLANG 76) `constructor(private sample: NriSample) { this.reset(); }`: no call can overwrite such a field, so the parameter used after `new` is read back from it. Co-Authored-By: Claude Opus 5.5 --- .../OwnershipSignatureFieldLocals.cpp | 36 ++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 6 ++- .../tester/own/own_err_kept_field_rebound.ts | 31 ++++++++++++++++ tslang/test/tester/tests/00tslang_issue_76.ts | 37 +++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/own/own_err_kept_field_rebound.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_76.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index b57667522..1ef47f3bd 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -348,7 +348,10 @@ namespace own_signature return kept; } - // `this` only read, and only for its fields, which are only read or written + // `this` only read, and only for its fields, which are only read or written. Or `this` given + // to a call (`this.reset()`), which may reach any field it can write: then only a private + // field nothing but this constructor writes (TSLANG 76), which no call can + auto escapes = false; llvm::DenseMap stores; llvm::SmallVector candidates; for (auto *user : thisSlot.getUsers()) @@ -364,7 +367,8 @@ namespace own_signature auto propertyRefOp = mlir::dyn_cast(loadUser); if (!propertyRefOp) { - return kept; + escapes = true; + continue; } for (auto *refUser : propertyRefOp->getUsers()) @@ -393,11 +397,37 @@ namespace own_signature } } + // a private field of the constructor's class that no other function writes + auto writtenOnlyHere = [&](mlir_ts::PropertyRefOp field) { + auto storage = fieldOwnerStorage(field); + auto fields = storage ? storage.getFields() : decltype(storage.getFields())(); + auto position = field.getPosition(); + if (!storage || position < 0 || static_cast(position) >= fields.size() || + fields[position].accessLevel != mlir_ts::AccessLevel::Private) + { + return false; + } + + auto module = ctor->getParentOfType(); + auto written = false; + module.walk([&](mlir_ts::PropertyRefOp propertyRefOp) { + if (written || propertyRefOp.getPosition() != position || fieldOwnerStorage(propertyRefOp) != storage || + propertyRefOp->getParentOfType() == ctor) + { + return; + } + + written = !llvm::all_of(propertyRefOp->getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); + }); + return !written; + }; + for (auto storeOp : candidates) { auto propertyRefOp = storeOp.getReference().getDefiningOp(); auto loadOp = stripViews(storeOp.getValue()).getDefiningOp(); - if (storeOp->getBlock() != &entry || stores[propertyRefOp.getPosition()] != 1 || !loadOp) + if (storeOp->getBlock() != &entry || stores[propertyRefOp.getPosition()] != 1 || !loadOp || + (escapes && !writtenOnlyHere(propertyRefOp))) { continue; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6dbeb5717..72597c2d3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1925,12 +1925,13 @@ endforeach() # TSLANG_ISSUES 71-72: a narrowed value kept by a constructor; a local kept by a constructor or pushed, then read # TSLANG_ISSUES 73: a private field nothing writes after the constructor, borrowed across a call that runs callbacks # TSLANG_ISSUES 75: a call through a constant global that holds a function, a call of that function +# TSLANG_ISSUES 76: a constructor that keeps a parameter in a private field only it writes, and gives `this` to a 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 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_69 00tslang_issue_71 00tslang_issue_73 - 00tslang_issue_75) + 00tslang_issue_75 00tslang_issue_76) 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() @@ -5509,7 +5510,8 @@ set(own_error_cases "own_err_moved_on_two_of_three_paths|:16:13: error: '[^']*' is moved here on some paths only" "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten" - "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten") + "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved") 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_kept_field_rebound.ts b/tslang/test/tester/own/own_err_kept_field_rebound.ts new file mode 100644 index 000000000..8f2aa7984 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_field_rebound.ts @@ -0,0 +1,31 @@ +// -mm=own rejects: `App`'s constructor gives `this` to a call, and `rebind` writes the field it keeps +// `sample` in, so that field is not one the call cannot overwrite (TSLANG 76): `sample`, used after +// `new`, was moved. +@final +class Sample { + options: string[] = []; + + add(option: string) { + this.options.push(option); + } +} + +@final +class App { + constructor(private sample: Sample) { + this.reset(); + } + + reset() {} + + rebind(sample: Sample) { + this.sample = sample; + } +} + +function main() { + const sample = new Sample(); + const app = new App(sample); + sample.add("-slow"); + app.rebind(new Sample()); +} diff --git a/tslang/test/tester/tests/00tslang_issue_76.ts b/tslang/test/tester/tests/00tslang_issue_76.ts new file mode 100644 index 000000000..8ad15dea3 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_76.ts @@ -0,0 +1,37 @@ +// TSLANG 76: a constructor that keeps a parameter in a private field and gives `this` to a call +// (`this.reset()`): no other function writes the field, so the call cannot overwrite it, and the +// parameter, used after `new`, is read back from the field. +@final +class Sample { + options: string[] = []; + + add(option: string) { + this.options.push(option); + } +} + +@final +class App { + private constants: number[] = [0, 0]; + + constructor(private sample: Sample) { + this.reset(); + } + + reset() { + this.constants[0] = 4.0; + } + + count(): int { + return this.sample.options.length; + } +} + +function main() { + const sample = new Sample(); + const app = new App(sample); + sample.add("-slow"); + sample.add("-noUI"); + assert(app.count() == 2, "count"); + print("done."); +} From 8a8490c05f12ffec23e66ed9138110a8694070d5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:15:28 +0000 Subject: [PATCH 12/21] own: an array kept by a constructor, then filled through the local, fills the object's array (TSLANG 77) Co-Authored-By: Claude Opus 5.5 --- .../OwnershipSignatureFieldLocals.cpp | 38 +++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 6 ++- .../own_err_kept_array_filled_after_pop.ts | 27 +++++++++++++ tslang/test/tester/tests/00tslang_issue_77.ts | 26 +++++++++++++ 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_77.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index 1ef47f3bd..2a514cb50 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -817,7 +817,10 @@ namespace own_signature return false; }; - auto usesAfterMove = [&](mlir::Operation *store, mlir::Value root, llvm::SmallVectorImpl &uses) { + // `inPlaceAfter`: an array changed in place through the local after the move is a use too, of + // the local's slot (TSLANG 77) - for a step that gives it the place the array moved to + auto usesAfterMove = [&](mlir::Operation *store, mlir::Value root, llvm::SmallVectorImpl &uses, + bool inPlaceAfter = false) { auto loadOp = root.getDefiningOp(); auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); if (!slot) @@ -864,6 +867,24 @@ namespace own_signature continue; } + // ...and after it, through the place it moved to: `const sets = []; this.frames.push(new + // Frame(sets)); for (...) sets.push(set)` (TSLANG 77) + if (inPlaceAfter && + mlir::isa(user) && + dominance.properlyDominates(store, user)) + { + for (auto &operand : user->getOpOperands()) + { + if (operand.get() == slot.getResult()) + { + uses.push_back(&operand); + } + } + + continue; + } + if (!mlir::isa(user)) { @@ -1171,7 +1192,7 @@ namespace own_signature if (slotRead) { llvm::SmallVector later; - if (!usesAfterMove(callOp, root, later)) + if (!usesAfterMove(callOp, root, later, true)) { continue; } @@ -1196,6 +1217,16 @@ namespace own_signature } } + // an array changed in place after the move changes the field itself: its reference, of the + // local's type (TSLANG 77) + if (llvm::any_of(uses, [&](mlir::OpOperand *use) { + return mlir::isa(use->get().getType()) && (narrowed || use->get().getType() != fieldRefType); + })) + { + continue; + } + + llvm::DenseMap refs; for (auto *use : uses) { auto *user = use->getOwner(); @@ -1217,6 +1248,7 @@ namespace own_signature builder.setInsertionPointAfter(after); auto ref = builder.create(callOp->getLoc(), fieldRefType, place, builder.getI32IntegerAttr(kept.first)); + refs[place] = ref; fieldRead = builder.create(callOp->getLoc(), fieldType, ref); if (narrowed) { @@ -1224,7 +1256,7 @@ namespace own_signature } } - use->set(fieldRead); + use->set(mlir::isa(use->get().getType()) ? refs[place] : fieldRead); } } } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 72597c2d3..6d806c11b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1926,12 +1926,13 @@ endforeach() # TSLANG_ISSUES 73: a private field nothing writes after the constructor, borrowed across a call that runs callbacks # TSLANG_ISSUES 75: a call through a constant global that holds a function, a call of that function # TSLANG_ISSUES 76: a constructor that keeps a parameter in a private field only it writes, and gives `this` to a call +# TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local 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_69 00tslang_issue_71 00tslang_issue_73 - 00tslang_issue_75 00tslang_issue_76) + 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77) 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() @@ -5511,7 +5512,8 @@ set(own_error_cases "own_err_kept_local_array_popped|:16:22: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" - "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved") + "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved" + "own_err_kept_array_filled_after_pop|:21:9: 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_kept_array_filled_after_pop.ts b/tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts new file mode 100644 index 000000000..c3b0c06e0 --- /dev/null +++ b/tslang/test/tester/own/own_err_kept_array_filled_after_pop.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `sets`, moved into a frame pushed on `this.frames`, is that frame's array; the +// frame popped, the push through `sets` would fill a freed array (TSLANG 77). +@final +class DSet { + constructor(public id: int) {} +} + +@final +class Frame { + constructor(public view: int, public sets: DSet[]) {} +} + +@final +class App { + frames: Frame[] = []; + + build() { + const sets: DSet[] = []; + this.frames.push(new Frame(0, sets)); + this.frames.pop(); + sets.push(new DSet(1)); + } +} + +function main() { + new App().build(); +} diff --git a/tslang/test/tester/tests/00tslang_issue_77.ts b/tslang/test/tester/tests/00tslang_issue_77.ts new file mode 100644 index 000000000..526f0c9d5 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_77.ts @@ -0,0 +1,26 @@ +// TSLANG 77: an array moved into a constructor that keeps it, the object pushed, then filled through +// the local: each push fills the object's array (`sets` is that array, by reference). +@final +class DSet { + constructor(public id: int) {} +} +@final +class Frame { + constructor(public view: int, public sets: DSet[]) {} +} +@final +class App { + frames: Frame[] = []; + build(n: int): int { + for (let i = 0; i < 2; i++) { + const sets: DSet[] = []; + this.frames.push(new Frame(i, sets)); + for (let t = 0; t < n; t++) { + const set = new DSet(t); + sets.push(set); + } + } + return this.frames[1].sets.length; + } +} +function main() { const a = new App(); assert(a.build(3) == 3, "sets"); print("done."); } From 917456c07ef3d995b904522e888f0c75090d3001 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:17:10 +0000 Subject: [PATCH 13/21] own: a value pushed on or stored into a field of a local object, then used, reads that field (TSLANG 78) Co-Authored-By: Claude Opus 5.5 --- .../OwnershipSignatureFieldLocals.cpp | 6 ++- tslang/test/tester/CMakeLists.txt | 6 ++- .../own/own_err_local_object_field_popped.ts | 23 +++++++++ tslang/test/tester/tests/00tslang_issue_78.ts | 49 +++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tslang/test/tester/own/own_err_local_object_field_popped.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_78.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index 2a514cb50..7d4be23ed 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -129,9 +129,11 @@ namespace own_signature return true; } + // ...or the object a value, not a read (`const frame = new Frame(); frame.allocators.push(a)`, + // TSLANG 78): the read right after uses that very value auto loadOp = field.getObjectRef().getDefiningOp(); - return loadOp && (loadOp.getReference().getDefiningOp() || - loadOp.getReference().getDefiningOp()); + return !loadOp || loadOp.getReference().getDefiningOp() || + loadOp.getReference().getDefiningOp(); } // The truthiness test of `value` a branch makes: `cond_br(DialectCast(Cast(value -> boolean)))`. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 6d806c11b..a8f5a4f8b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1927,12 +1927,13 @@ endforeach() # TSLANG_ISSUES 75: a call through a constant global that holds a function, a call of that function # TSLANG_ISSUES 76: a constructor that keeps a parameter in a private field only it writes, and gives `this` to a call # TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local +# TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used 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_69 00tslang_issue_71 00tslang_issue_73 - 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77) + 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78) 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() @@ -5513,7 +5514,8 @@ set(own_error_cases "own_err_private_field_written_callback|:33:24: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved" - "own_err_kept_array_filled_after_pop|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_kept_array_filled_after_pop|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_local_object_field_popped|:18:11: 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_local_object_field_popped.ts b/tslang/test/tester/own/own_err_local_object_field_popped.ts new file mode 100644 index 000000000..f5ce87ef5 --- /dev/null +++ b/tslang/test/tester/own/own_err_local_object_field_popped.ts @@ -0,0 +1,23 @@ +// -mm=own rejects: `a`, pushed on a field of the local object `frame`, is read back from it after the +// push (TSLANG 78); popped first, it would be read from a freed element. +@final +class Alloc { + constructor(public id: int) {} +} + +@final +class Frame { + allocs: Alloc[] = []; +} + +function build() { + const frame = new Frame(); + const a = new Alloc(1); + frame.allocs.push(a); + frame.allocs.pop(); + print(a.id); +} + +function main() { + build(); +} diff --git a/tslang/test/tester/tests/00tslang_issue_78.ts b/tslang/test/tester/tests/00tslang_issue_78.ts new file mode 100644 index 000000000..7a4148820 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_78.ts @@ -0,0 +1,49 @@ +// TSLANG 78: a value pushed on, or stored into, a field of a local object (`const frame = new Frame()`, +// a value, not a read), then used, is read back from that field. +@final +class Buf { constructor(public id: int) {} } +@final +class Alloc { + constructor(public id: int) {} + make(): Buf { return new Buf(this.id); } +} +@final +class Frame { + allocs: Alloc[] = []; + bufs: Buf[] = []; + constructor(public n: int) {} +} +@final +class App { + frames: Frame[] = []; + build(): int { + const frame = new Frame(1); + for (let f = 0; f < 3; f++) { + const a = new Alloc(f); + frame.allocs.push(a); + frame.bufs.push(a.make()); + } + this.frames.push(frame); + return this.frames[0].bufs.length; + } +} +@final +class Mesh { + constructor(public n: int) {} +} + +@final +class Obj { + mesh: Mesh | null = null; +} + +function fieldStore() { + const objects: Obj[] = []; + const o = new Obj(); + const mesh = new Mesh(5); + o.mesh = mesh; + assert(mesh.n == 5, "mesh"); + objects.push(o); +} + +function main() { assert(new App().build() == 3, "bufs"); fieldStore(); print("done."); } From 67574c7d41be16e472304d1c4ac41fa83ba8ade9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:18:03 +0000 Subject: [PATCH 14/21] 00tslang_issue_75: alias a function of its own (the tester compiles without the default lib) Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/tests/00tslang_issue_75.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tslang/test/tester/tests/00tslang_issue_75.ts b/tslang/test/tester/tests/00tslang_issue_75.ts index e534621c0..ee22265e5 100644 --- a/tslang/test/tester/tests/00tslang_issue_75.ts +++ b/tslang/test/tester/tests/00tslang_issue_75.ts @@ -1,8 +1,11 @@ // TSLANG 75: a call through a global nothing assigns, given a function by its initializer, is a -// call of that function: `const f = Math.fround` destroys nothing, so the cluster borrowed from a +// call of that function: `const f = half` (as `const f = Math.fround`) destroys nothing, so the cluster borrowed from a // parameter's mesh outlives it. namespace Clusters { - const f = Math.fround; + function half(x: number) { + return x / 2; + } + const f = half; function twice(x: number) { return x * 2; } @@ -35,6 +38,6 @@ namespace Clusters { function main() { const meshes = [new Clusters.Mesh()]; const c = Clusters.centroid(meshes); - assert(c == 26, "centroid"); + assert(c == 5, "centroid"); print("done."); } From e470e7cbce8e72701f856a6771e9a9a74845bbb9 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:18:26 +0000 Subject: [PATCH 15/21] TSLANG_ISSUES 76-78 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index c04e0af36..ac2b376be 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -577,6 +577,34 @@ function: `f(...)` is a call of `Math.fround`, which destroys nothing. A global unknown. In the sweep, `clustered_geometry.ts` goes from 44 errors to 16. Regression tests `tslang/test/tester/tests/00tslang_issue_75.ts` (every model) and `own/own_err_const_alias_drops.ts`. +76 (a parameter kept by a constructor that also gives `this` to a call: `constructor(private sample: NriSample) { +this.reset(); }`, then `sample.addExampleOption(...)` after `new App(sample)` in `examples/nri/simple_compute_nri.ts`, +refused "is used here after its value was moved") is fixed. TSLANG 53 reads the field a constructor keeps a +parameter in, for each use after the call. It took only a constructor that reads `this` for its fields and +nothing else, so that no call it makes can reach the field. A constructor that gives `this` to a call now keeps a +parameter too, when the field is private and no other function writes it: as in 73, no call can then overwrite +it. A field a method writes is still refused. In the sweep, the 4 examples with this shape lose their errors. +Regression tests `tslang/test/tester/tests/00tslang_issue_76.ts` (every model) and `own/own_err_kept_field_rebound.ts`. + +77 (an array kept by a constructor, then filled through the local: `const sceneSets: NriDescriptorSet[] = []; +this.queuedFrames.push(new QueuedFrame(sceneView, sceneSets)); for (...) sceneSets.push(sceneSet);` in +`examples/nri/msaa_nri.ts`, refused "is used here after its value was moved") is fixed. An array is a reference, +so after the move the local is the object's array, and each push fills it. TSLANG 72 took an array filled in +place before the move. One changed in place after it now changes the field, reached the way the other uses after +the move are: through the object, or the place it moved to last. The object popped before the push is still +refused. Regression tests `tslang/test/tester/tests/00tslang_issue_77.ts` (every model) and +`own/own_err_kept_array_filled_after_pop.ts`. + +78 (a value pushed on a field of a local object, then used: `const queuedFrame = new QueuedFrame(...); for (...) { +const faceAllocator = ...; queuedFrame.faceAllocators.push(faceAllocator); +queuedFrame.faceCommandBuffers.push(faceAllocator.createCommandBuffer()); }` in +`examples/nri/threaded_rendering_nri.ts`, refused "is used here after its value was moved") is fixed. TSLANG 65's +push step and the field store step read the value back through the object right after the move. They took an +object read out of a local or an element, but not a `const` holding the object itself, which is a value and +needs no read. That value now serves, as it is. The element popped before the use is still refused. +Regression tests `tslang/test/tester/tests/00tslang_issue_78.ts` (every model) and +`own/own_err_local_object_field_popped.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")` From c69fe37e32f57a4f97de5af10db5884ee6cdabe7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:22:10 +0000 Subject: [PATCH 16/21] own: a local pushed only where it is not null, then used, reads the element or null (TSLANG 79) `let pipeline = ...; if (pipeline) this.pipelines.push(pipeline); return pipeline;`: the reads after the paths join take the merge, and the local is released on the edge that did not push it. Co-Authored-By: Claude Opus 5.5 --- .../OwnershipSignatureFieldLocals.cpp | 84 ++++++++++++++----- tslang/test/tester/CMakeLists.txt | 6 +- .../tester/own/own_err_pushed_let_popped.ts | 28 +++++++ tslang/test/tester/tests/00tslang_issue_79.ts | 37 ++++++++ 4 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 tslang/test/tester/own/own_err_pushed_let_popped.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_79.ts diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index 7d4be23ed..74389db4a 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -147,32 +147,63 @@ namespace own_signature // `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) + // `value` that block dominates read it. `slot`: the value is a read of this local, which nothing + // assigns after the push (usesAfterMove), and the test and the uses are reads of it of their own + // (TSLANG 79: `let pipeline = ...; if (pipeline) this.pipelines.push(pipeline); return pipeline;`). + void mergePushedOrNull(mlir::Operation *pushOp, mlir::Value value, mlir::Value read, mlir::DominanceInfo &dominance, + mlir_ts::VariableOp slot = {}) { auto *pushBlock = pushOp->getBlock(); - mlir::cf::CondBranchOp test; - for (auto *user : value.getUsers()) + // the value, or each read of the local + llvm::SmallVector values{value}; + if (slot) { - auto cast = mlir::dyn_cast(user); - if (!cast) + values.clear(); + for (auto *user : slot.getResult().getUsers()) { - continue; + if (auto loadOp = mlir::dyn_cast(user)) + { + values.push_back(loadOp.getResult()); + } } + } - for (auto *castUser : cast->getUsers()) + mlir::cf::CondBranchOp test; + for (auto tested : values) + { + for (auto *user : tested.getUsers()) { - for (auto *dialectUser : castUser->getUsers()) + auto cast = mlir::dyn_cast(user); + if (!cast) + { + continue; + } + + for (auto *castUser : cast->getUsers()) { - auto branch = mlir::dyn_cast(dialectUser); - if (branch && testsTruthOf(branch, value) && dominance.dominates(branch.getTrueDest(), pushBlock)) + for (auto *dialectUser : castUser->getUsers()) { - test = branch; + auto branch = mlir::dyn_cast(dialectUser); + if (branch && testsTruthOf(branch, tested) && dominance.dominates(branch.getTrueDest(), pushBlock)) + { + test = branch; + } } } } } + // ...and nothing assigns the local from the test on: the false edge brings the null it held + if (test && slot && + llvm::any_of(slot.getResult().getUsers(), [&](mlir::Operation *user) { + auto storeOp = mlir::dyn_cast(user); + return storeOp && storeOp.getReference() == slot.getResult() && + dominance.dominates(test->getBlock(), storeOp->getBlock()); + })) + { + return; + } + if (!test || test.getFalseDest() == test.getTrueDest()) { return; @@ -194,13 +225,16 @@ namespace own_signature } llvm::SmallVector uses; - for (auto &use : value.getUses()) + for (auto each : values) { - auto *user = use.getOwner(); - if (!mlir::isa(user) && user->getBlock() != test->getBlock() && - dominance.dominates(merge, user->getBlock())) + for (auto &use : each.getUses()) { - uses.push_back(&use); + auto *user = use.getOwner(); + if (!mlir::isa(user) && user->getBlock() != test->getBlock() && + dominance.dominates(merge, user->getBlock())) + { + uses.push_back(&use); + } } } @@ -227,6 +261,16 @@ namespace own_signature { use->set(argument); } + + // the reads of the local the merge now stands for, left unused: the local is not read after + // the paths join, so its release goes on the edge that did not push it (TSLANG 79) + for (auto each : values) + { + if (slot && each.use_empty()) + { + each.getDefiningOp()->erase(); + } + } } // `value` seen through the views that keep its block: an optional made of it or opened, a @@ -1043,7 +1087,8 @@ namespace own_signature llvm::SmallVector uses; if (slotRead) { - if (!usesAfterMove(pushOp, root, uses) || uses.empty()) + // no use the push dominates: there may be some where the paths meet (below) + if (!usesAfterMove(pushOp, root, uses)) { continue; } @@ -1087,7 +1132,8 @@ namespace own_signature // ...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); + mergePushedOrNull(pushOp, root, read, dominance, + slotRead ? rootLoad.getReference().getDefiningOp() : mlir_ts::VariableOp()); } followFieldStores(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a8f5a4f8b..91f8095cc 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1928,12 +1928,13 @@ endforeach() # TSLANG_ISSUES 76: a constructor that keeps a parameter in a private field only it writes, and gives `this` to a call # TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used +# TSLANG_ISSUES 79: a local pushed only where it is not null, then used 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_69 00tslang_issue_71 00tslang_issue_73 - 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78) + 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79) 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() @@ -5515,7 +5516,8 @@ set(own_error_cases "own_err_const_alias_drops|:22:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved" "own_err_kept_array_filled_after_pop|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_local_object_field_popped|:18:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_local_object_field_popped|:18:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_pushed_let_popped|:17:9: 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_let_popped.ts b/tslang/test/tester/own/own_err_pushed_let_popped.ts new file mode 100644 index 000000000..6546ba471 --- /dev/null +++ b/tslang/test/tester/own/own_err_pushed_let_popped.ts @@ -0,0 +1,28 @@ +// -mm=own rejects: `pipe`, a local pushed where it is not null, is the array's element after the +// push (TSLANG 79); popped before it is returned, it would be a freed element. +@final +class Pipe { + constructor(public id: int) {} +} + +@final +class Cache { + private pipes: Pipe[] = []; + + get(ok: boolean): Pipe | null { + let pipe: Pipe | null = null; + if (ok) { + pipe = new Pipe(1); + } + if (pipe) { + this.pipes.push(pipe); + this.pipes.pop(); + } + return pipe; + } +} + +function main() { + const c = new Cache(); + print(c.get(true) != null); +} diff --git a/tslang/test/tester/tests/00tslang_issue_79.ts b/tslang/test/tester/tests/00tslang_issue_79.ts new file mode 100644 index 000000000..6a028deaf --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_79.ts @@ -0,0 +1,37 @@ +// TSLANG 79: a local pushed only where it is not null (`let pipeline = ...; if (pipeline) +// this.pipelines.push(pipeline); return pipeline;`) reads the array's element after the push, and null +// where the test failed: the method returns a borrow of `this` on every way. +@final +class Pipe { + constructor(public id: int) {} +} +@final +class Cache { + private keys: int[] = []; + private pipes: Pipe[] = []; + + get(key: int, ok: boolean): Pipe | null { + for (let i = 0; i < this.keys.length; i++) { + if (this.keys[i] == key) { + return this.pipes[i]; + } + } + let pipe: Pipe | null = null; + if (ok) { + pipe = new Pipe(key); + } + if (pipe) { + this.keys.push(key); + this.pipes.push(pipe); + } + return pipe; + } +} +function main() { + const c = new Cache(); + const a = c.get(3, true); + const b = c.get(3, true); + const n = c.get(4, false); + assert((a ? a.id : -1) == 3 && (b ? b.id : -1) == 3 && n == null, "cache"); + print("done."); +} From e3c25014595d3fde6059c1214740302108ecee68 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:22:21 +0000 Subject: [PATCH 17/21] TSLANG_ISSUES 79 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index ac2b376be..12f18153c 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -605,6 +605,17 @@ needs no read. That value now serves, as it is. The element popped before the us Regression tests `tslang/test/tester/tests/00tslang_issue_78.ts` (every model) and `own/own_err_local_object_field_popped.ts`. +79 (a local pushed only where it is not null, then used: `let pipeline: NriPipeline | null = null; if (loaded) +pipeline = device.createPipelineFromDesc(...); if (pipeline) { this.pipelineKeys.push(key); +this.pipelines.push(pipeline); } return pipeline;` in `examples/nri/msaa_nri.ts`, refused "is used here after its +value was moved") is fixed. TSLANG 67 did this for a value. For a local, the test and the uses are reads of +their own. Now, when nothing assigns the local from the test on, each read where the paths join takes the merge: +the element read back after the push, or the null the test found. The local is then not read after the join, so +its release goes on the edge that did not push it. The method returns a borrow of `this` on every way, so the +early `return this.pipelines[i]` is no longer refused either. `msaa_nri.ts` and +`dynamic_multisample_rasterization_nri.ts` compile. Regression tests +`tslang/test/tester/tests/00tslang_issue_79.ts` (every model) and `own/own_err_pushed_let_popped.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")` From 766e67c9c283cc429379ccf413e8ecdf4ce29109 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:30:55 +0000 Subject: [PATCH 18/21] own: an owned parameter stored into a field and read again is copied; a value moved on some paths is released on the others (TSLANG 80, 81) Co-Authored-By: Claude Opus 5.5 --- .../OwnershipInferenceEdgeReleases.cpp | 91 ++++++++++++++++++- .../TypeScript/OwnershipInferenceMoves.cpp | 5 +- .../lib/TypeScript/OwnershipInferencePass.cpp | 29 +++++- .../lib/TypeScript/OwnershipInferencePass.h | 3 + tslang/test/tester/CMakeLists.txt | 7 +- .../own/own_err_moved_on_some_paths_used.ts | 27 ++++++ tslang/test/tester/tests/00tslang_issue_80.ts | 28 ++++++ tslang/test/tester/tests/00tslang_issue_81.ts | 33 +++++++ 8 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 tslang/test/tester/own/own_err_moved_on_some_paths_used.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_80.ts create mode 100644 tslang/test/tester/tests/00tslang_issue_81.ts diff --git a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp index 835d739be..c5596b5f0 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp @@ -483,15 +483,102 @@ namespace own_inference return true; } + bool OwnershipInferencePass::valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, + mlir::Operation *release) + { + auto &body = getFunction().getFunctionBody(); + if (!definition || definition->getParentRegion() != &body || acquire->getParentRegion() != &body || + release->getParentRegion() != &body || !dominance->properlyDominates(definition, release)) + { + return false; + } + + // what follows the move, while this value is the one the definition made: the blocks the move + // reaches, not through the definition again (a loop's next turn makes another) + auto *moveBlock = acquire->getBlock(); + llvm::SmallPtrSet after; + llvm::SmallVector work(moveBlock->getSuccessors().begin(), moveBlock->getSuccessors().end()); + while (!work.empty()) + { + auto *block = work.pop_back_val(); + if (block == definition->getBlock() || !after.insert(block).second) + { + continue; + } + + work.append(block->getSuccessors().begin(), block->getSuccessors().end()); + } + + if (!after.contains(release->getBlock()) || after.contains(moveBlock)) + { + return false; + } + + // the edges into it from where the value is held and not moved + auto handler = handlerBlocks(); + llvm::SmallVector placed; + for (auto &block : body) + { + if (after.contains(&block) || &block == moveBlock || !dominance->dominates(definition, block.getTerminator())) + { + continue; + } + + for (auto index : llvm::seq(0, block.getNumSuccessors())) + { + if (!after.contains(block.getSuccessor(index))) + { + continue; + } + + auto *terminator = block.getTerminator(); + if (!mlir::isa(terminator) || handler.contains(&block)) + { + return false; + } + + // another release of the value placed it already + if (llvm::any_of(edgeReleases, [&](EdgeRelease &edge) { + return edge.slot == value && edge.from == &block && edge.successor == index; + })) + { + continue; + } + + placed.push_back({value, &block, static_cast(index), terminator->getLoc()}); + } + } + + if (placed.empty()) + { + return false; + } + + edgeReleases.append(placed.begin(), placed.end()); + return true; + } + void OwnershipInferencePass::placeEdgeReleases() { + // a slot's release, or a value's (TSLANG 81) + auto releaseOf = [](mlir::OpBuilder &builder, EdgeRelease &edge) { + if (edge.slot.getDefiningOp()) + { + builder.create(edge.loc, edge.slot); + } + else + { + builder.create(edge.loc, edge.slot); + } + }; + for (auto &edge : edgeReleases) { auto *terminator = edge.from->getTerminator(); mlir::OpBuilder builder(terminator); if (terminator->getNumSuccessors() == 1) { - builder.create(edge.loc, edge.slot); + releaseOf(builder, edge); continue; } @@ -501,7 +588,7 @@ namespace own_inference auto *split = new mlir::Block(); split->insertAfter(edge.from); builder.setInsertionPointToEnd(split); - builder.create(edge.loc, edge.slot); + releaseOf(builder, edge); builder.create(edge.loc, join, forwarded); if (!forwarded.empty()) { diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 8df5cb296..6fb45ebdc 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -1039,9 +1039,12 @@ namespace own_inference // 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) + // ...or, moved by this one taker, released on the edges that did not move it (TSLANG 81): + // `const c = new C(); if (c.read()) this.items.push(c);` if (!dominance->properlyDominates(acquire, release) && !emptyWhereNotMoved(value, definition, acquire, release) && - !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers))) + !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers)) && + !(takers.size() == 1 && valueUnmovedEdges(value, definition, acquire, release))) { reportMovedOnSomePaths(taker, release, name); return false; diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index a3edc5be9..3fa0caae2 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1076,9 +1076,36 @@ namespace own_inference auto storeOp = mlir::dyn_cast(taking->getOwner()); auto intoPlace = storeOp && storeOp.getValue() == taking->get() && mlir::isa_and_nonnull(storeOp.getReference().getDefiningOp()); + // ...unless the parameter is read again after the store (`constructor(public name: string)`, then + // `${name}` in a loop that calls): moved, the read would find it gone; a copy leaves it the + // parameter's own (TSLANG 80) + auto readAgain = [&]() { + auto loadOp = value.getDefiningOp(); + auto slot = loadOp ? loadOp.getReference() : mlir::Value(); + if (!intoPlace || !slot || !slot.getDefiningOp()) + { + return false; + } + + return llvm::any_of(slot.getUsers(), [&](mlir::Operation *user) { + if (user == loadOp.getOperation() || !mlir::isa(user)) + { + return false; + } + + if (user->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(user)) + { + return true; + } + + return llvm::any_of(storeOp->getBlock()->getSuccessors(), + [&](mlir::Block *successor) { return reachableFrom(successor, user, {}); }); + }); + }; if (borrowsKnownParam(value) && !(intoPlace && borrowedParam(value) == returnsBorrowOf && - !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf))) + !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf)) && + !readAgain()) { return false; } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 1292f29f8..8c5d1c6be 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1239,6 +1239,9 @@ class OwnershipInferencePass llvm::DenseMap before; }; + // A value moved on some paths only (TSLANG 81): its release, which the move reaches, goes on the + // edges into what follows the move from the paths that did not move it. + bool valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, mlir::Operation *release); bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, llvm::SetVector &toErase); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 91f8095cc..abb3c522c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1929,12 +1929,14 @@ endforeach() # TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used # TSLANG_ISSUES 79: a local pushed only where it is not null, then used +# TSLANG_ISSUES 80-81: an owned parameter stored into a field, then read; a value moved on some paths only 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_69 00tslang_issue_71 00tslang_issue_73 - 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79) + 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 + 00tslang_issue_80 00tslang_issue_81) 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() @@ -5517,7 +5519,8 @@ set(own_error_cases "own_err_kept_field_rebound|:29:5: error: '[^']*' is used here after its value was moved" "own_err_kept_array_filled_after_pop|:21:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_local_object_field_popped|:18:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" - "own_err_pushed_let_popped|:17:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten") + "own_err_pushed_let_popped|:17:9: error: '[^']*' borrows an element but is used here after it may be released or overwritten" + "own_err_moved_on_some_paths_used|:21:15: error: '[^']*' is used here after its value was moved") 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_some_paths_used.ts b/tslang/test/tester/own/own_err_moved_on_some_paths_used.ts new file mode 100644 index 000000000..3213148f0 --- /dev/null +++ b/tslang/test/tester/own/own_err_moved_on_some_paths_used.ts @@ -0,0 +1,27 @@ +// -mm=own rejects: `c`, pushed only where `read` succeeds, is released on the other way (TSLANG 81), +// so a use of it after the ways join may find it moved. +@final +class Ctl { + node: int = 0; + + read(ok: boolean): boolean { + return ok; + } +} + +@final +class Model { + lights: Ctl[] = []; + + load(n: int) { + const c = new Ctl(); + if (c.read(n != 2)) { + this.lights.push(c); + } + print(c.node); + } +} + +function main() { + new Model().load(1); +} diff --git a/tslang/test/tester/tests/00tslang_issue_80.ts b/tslang/test/tester/tests/00tslang_issue_80.ts new file mode 100644 index 000000000..4831cbc4a --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_80.ts @@ -0,0 +1,28 @@ +// TSLANG 80: a parameter the function owns, stored into a field, then read again (`constructor(public +// name: string)`, then `${name}` in a loop whose calls stop the field standing in for it): the store +// copies it, so the read finds the parameter. +@final +class Ctl { + node: int = 0; + read(ok: boolean): boolean { return ok; } +} +@final +class Model { + lights: Ctl[] = []; + constructor(public name: string, n: int) { + for (let i = 0; i < n; i++) { + if (i == 1) { + print(`skip ${name}`); + continue; + } + const c = new Ctl(); + c.node = i; + this.lights.push(c); + } + } +} +function main() { + const m = new Model("m", 4); + assert(m.lights.length == 3 && m.name == "m", "model"); + print("done."); +} diff --git a/tslang/test/tester/tests/00tslang_issue_81.ts b/tslang/test/tester/tests/00tslang_issue_81.ts new file mode 100644 index 000000000..aaf842dd8 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_81.ts @@ -0,0 +1,33 @@ +// TSLANG 81: a value moved on some paths only (`const c = new Ctl(); if (c.read()) this.lights.push(c);`) +// is released on the edges that did not move it. +@final +class Ctl { + node: int = 0; + read(ok: boolean): boolean { return ok; } +} +@final +class Model { + lights: Ctl[] = []; + load(n: int) { + for (let i = 0; i < n; i++) { + const c = new Ctl(); + if (c.read(i != 2)) { + this.lights.push(c); + } + } + } + load2(n: int) { + const c = new Ctl(); + if (c.read(n != 2)) { + this.lights.push(c); + } + } +} +function main() { + const m = new Model(); + m.load(4); + m.load2(1); + m.load2(2); + assert(m.lights.length == 4, "lights"); + print("done."); +} From 04cea3db558e73292011c5a830ba657cbe6f6090 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:31:14 +0000 Subject: [PATCH 19/21] TSLANG_ISSUES 80-81 Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 12f18153c..78584f345 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -616,6 +616,21 @@ early `return this.pipelines[i]` is no longer refused either. `msaa_nri.ts` and `dynamic_multisample_rasterization_nri.ts` compile. Regression tests `tslang/test/tester/tests/00tslang_issue_79.ts` (every model) and `own/own_err_pushed_let_popped.ts`. +80 (a parameter the function owns, stored into a field, then read again: `constructor(world: GameWorld, public +name: string, ...)`, then `${name}` in a loop that calls, in `core/rtxpt_game.ts`, refused "is used here after +its value was moved") is fixed. The local/field pairing reads the field for later reads of the parameter, but +stops at any call, as in that loop. A read of a parameter the function owns, stored into a field, was then +always moved, never copied. It is now copied when the parameter can be read again after the store, so that +read finds it. Regression test `tslang/test/tester/tests/00tslang_issue_80.ts` (every model). + +81 (a value moved on some paths only: `const controller = new LightController(); if (controller.read(...)) { +...; this.lights.push(controller); }` in `core/rtxpt_game.ts`, refused "is moved here on some paths only; -mm=own +cannot release it on the others yet") is fixed. Spec 30 placed a local's release on the edges that did not move +it; a value, like the `const` here, had no such rule. Now a value one taker moves gets its release on each edge +into what follows the move, from where the value is held and was not moved. The release after the move goes. A +use after the paths join is still refused. Regression tests `tslang/test/tester/tests/00tslang_issue_81.ts` +(every model) and `own/own_err_moved_on_some_paths_used.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")` From 9663502b7226486505d8ef2bb860646806d9cf5e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:44:13 +0000 Subject: [PATCH 20/21] own: back out TSLANG 80 (its copy leaked the owned parameter); 81 refuses a value read after the ways join; 78 takes only an object new made - 80 copied an owned parameter stored into a field when it was read again, but nothing released the caller's reference: 3M calls grew to 2.7 GB. own_err_string_kept_param_known says why; it is refused again. - 81 released a value on the edges that did not move it even where a read of a copyable value followed the join (own_string_copy_branch: a print after the release). - 78 followed field stores into closure capture records too. - Error tests now refused as a field borrow say so; three now accepted (a value moved on some ways, an aliased store) moved into the 78/81 tests. Co-Authored-By: Claude Opus 5.5 --- .../OwnershipInferenceEdgeReleases.cpp | 21 ++++++++- .../TypeScript/OwnershipInferenceMoves.cpp | 2 +- .../lib/TypeScript/OwnershipInferencePass.cpp | 29 +----------- .../lib/TypeScript/OwnershipInferencePass.h | 3 +- .../OwnershipSignatureFieldLocals.cpp | 13 ++++-- tslang/test/tester/CMakeLists.txt | 13 ++---- .../own/own_err_move_some_paths_field.ts | 11 ----- .../own/own_err_moved_when_tested_other.ts | 18 -------- tslang/test/tester/own/own_err_two_owners.ts | 15 ------ tslang/test/tester/tests/00tslang_issue_78.ts | 21 ++++++++- tslang/test/tester/tests/00tslang_issue_81.ts | 46 +++++++++++++++++++ 11 files changed, 105 insertions(+), 87 deletions(-) delete mode 100644 tslang/test/tester/own/own_err_move_some_paths_field.ts delete mode 100644 tslang/test/tester/own/own_err_moved_when_tested_other.ts delete mode 100644 tslang/test/tester/own/own_err_two_owners.ts diff --git a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp index c5596b5f0..07937e0a2 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceEdgeReleases.cpp @@ -484,7 +484,7 @@ namespace own_inference } bool OwnershipInferencePass::valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, - mlir::Operation *release) + mlir::Operation *taker, mlir::Operation *release) { auto &body = getFunction().getFunctionBody(); if (!definition || definition->getParentRegion() != &body || acquire->getParentRegion() != &body || @@ -514,6 +514,25 @@ namespace own_inference return false; } + // nothing reads the value where the ways join: released on an edge into there, it is gone + // (a copyable value's read after the move is not one the moves refuse) + auto usedAfter = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + auto *top = body.findAncestorOpInRegion(*user); + if (!top || user == release || user == taker || top == body.findAncestorOpInRegion(*taker) || + mlir::isa(user)) + { + return; + } + + usedAfter = usedAfter || after.contains(top->getBlock()) || + (top->getBlock() == moveBlock && acquire->isBeforeInBlock(top) && top != acquire); + }); + if (usedAfter) + { + return false; + } + // the edges into it from where the value is held and not moved auto handler = handlerBlocks(); llvm::SmallVector placed; diff --git a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp index 6fb45ebdc..9e490262a 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceMoves.cpp @@ -1044,7 +1044,7 @@ namespace own_inference if (!dominance->properlyDominates(acquire, release) && !emptyWhereNotMoved(value, definition, acquire, release) && !(definition && takers.size() > 1 && !reachableAfter(definition, release, takers)) && - !(takers.size() == 1 && valueUnmovedEdges(value, definition, acquire, release))) + !(takers.size() == 1 && valueUnmovedEdges(value, definition, acquire, taker, release))) { reportMovedOnSomePaths(taker, release, name); return false; diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 3fa0caae2..a3edc5be9 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1076,36 +1076,9 @@ namespace own_inference auto storeOp = mlir::dyn_cast(taking->getOwner()); auto intoPlace = storeOp && storeOp.getValue() == taking->get() && mlir::isa_and_nonnull(storeOp.getReference().getDefiningOp()); - // ...unless the parameter is read again after the store (`constructor(public name: string)`, then - // `${name}` in a loop that calls): moved, the read would find it gone; a copy leaves it the - // parameter's own (TSLANG 80) - auto readAgain = [&]() { - auto loadOp = value.getDefiningOp(); - auto slot = loadOp ? loadOp.getReference() : mlir::Value(); - if (!intoPlace || !slot || !slot.getDefiningOp()) - { - return false; - } - - return llvm::any_of(slot.getUsers(), [&](mlir::Operation *user) { - if (user == loadOp.getOperation() || !mlir::isa(user)) - { - return false; - } - - if (user->getBlock() == storeOp->getBlock() && storeOp->isBeforeInBlock(user)) - { - return true; - } - - return llvm::any_of(storeOp->getBlock()->getSuccessors(), - [&](mlir::Block *successor) { return reachableFrom(successor, user, {}); }); - }); - }; if (borrowsKnownParam(value) && !(intoPlace && borrowedParam(value) == returnsBorrowOf && - !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf)) && - !readAgain()) + !llvm::is_contained(ownedParams(getFunction()), returnsBorrowOf))) { return false; } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 8c5d1c6be..fa52f4b02 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -1241,7 +1241,8 @@ class OwnershipInferencePass // A value moved on some paths only (TSLANG 81): its release, which the move reaches, goes on the // edges into what follows the move from the paths that did not move it. - bool valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, mlir::Operation *release); + bool valueUnmovedEdges(mlir::Value value, mlir::Operation *definition, mlir::Operation *acquire, mlir::Operation *taker, + mlir::Operation *release); bool placeOnUnmovedEdges(mlir_ts::VariableOp varOp, mlir::Operation *acquire, mlir::Operation *retain, mlir::Operation *taker, llvm::ArrayRef assignments, llvm::SetVector &toErase); diff --git a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp index 74389db4a..f28aadfaf 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureFieldLocals.cpp @@ -131,9 +131,16 @@ namespace own_signature // ...or the object a value, not a read (`const frame = new Frame(); frame.allocators.push(a)`, // TSLANG 78): the read right after uses that very value - auto loadOp = field.getObjectRef().getDefiningOp(); - return !loadOp || loadOp.getReference().getDefiningOp() || - loadOp.getReference().getDefiningOp(); + // - an object of a class `new` made, not a closure's record of its captures + auto object = field.getObjectRef(); + if (object.getDefiningOp() && mlir::isa(object.getType())) + { + return true; + } + + auto loadOp = object.getDefiningOp(); + return loadOp && (loadOp.getReference().getDefiningOp() || + loadOp.getReference().getDefiningOp()); } // The truthiness test of `value` a branch makes: `cond_br(DialectCast(Cast(value -> boolean)))`. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index abb3c522c..ac1254bbf 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -5167,7 +5167,6 @@ set_tests_properties(test-own-err-names-the-variable set(own_error_cases "own_err_assign_after_move|:12:16: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_borrow_escape|:9:5: error: '[^']*' borrows '[^']*' and cannot be stored, returned or captured" - "own_err_move_some_paths_field|:8:9: error: '[^']*' is moved here on some paths only" "own_err_borrow_and_move|:8:5: error: '[^']*' is moved here while '[^']*' borrows it" "own_err_borrow_bound_method|:20:17: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_const_bound_method_reassigned|:20:11: error: '[^']*' takes a second reference" @@ -5176,7 +5175,7 @@ set(own_error_cases "own_err_borrow_derived_ref|:28:5: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten" "own_err_const_alias_move|:12:5: error: '[^']*' borrows '[^']*' and cannot be assigned" "own_err_const_alias_field|:15:5: error: '[^']*' is moved here while '[^']*' borrows it" - "own_err_field_alias|:9:11: error: '[^']*' is used here after its value was moved" + "own_err_field_alias|:9:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_call_in_try_drops|:30:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_closure_escape_used|:9:11: error: '[^']*' is captured by a closure that escapes, and is still used here" "own_err_closure_escape_twice|:12:25: error: 'n' is captured by a closure that escapes, and is still used here" @@ -5200,12 +5199,11 @@ set(own_error_cases "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" "own_err_generator_map_outlives|:7:11: error: '[^']*' borrows 'the generator's source' but is used here after 'the generator's source' is released or overwritten" "own_err_generator_place_overwritten|:15:16: error: '[^']*' borrows a field but is used here after it may be released or overwritten" - "own_err_two_owners|:8:13: error: '[^']*' is used here after its value was moved" "own_err_reassign_some_paths|:10:5: error: '[^']*' is moved here on some paths only" "own_err_push_let|:7:9: error: '[^']*' is used here after its value was moved" "own_err_push_loop|:6:33: error: '[^']*' is moved inside a loop but was made outside it" "own_err_field_loop|:10:9: error: '[^']*' is moved inside a loop but was made outside it" - "own_err_union_moved|:10:11: error: '[^']*' is used here after its value was moved" + "own_err_union_moved|:10:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_field_read_overwritten|:28:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_element_read_overwritten|:22:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" "own_err_element_read_popped|:26:11: error: '[^']*' borrows an element but is used here after it may be released or overwritten" @@ -5397,9 +5395,9 @@ set(own_error_cases "own_err_iface_field_view_overwritten|:26:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_call|:27:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_iface_field_result|borrows the result of 'dirOf' but is used here after it may be released or overwritten" - "own_err_read_after_move_overwritten|:10:11: error: '[^']*' is used here after its value was moved" - "own_err_read_after_move_view|:12:11: error: '[^']*' is used here after its value was moved" - "own_err_read_after_move_call|:11:11: error: '[^']*' is used here after its value was moved" + "own_err_read_after_move_overwritten|:10:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_read_after_move_view|:12:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" + "own_err_read_after_move_call|:11:11: error: '[^']*' borrows a field but is used here after it may be released or overwritten" "own_err_read_after_move_loop|:25:14: error: '[^']*' is used here after its value was moved" "own_err_read_after_move_after_loop|:14:11: error: '[^']*' is used here after its value was moved" "own_err_read_after_move_global|:42:15: error: '[^']*' borrows a field but is used here after it may be released or overwritten" @@ -5490,7 +5488,6 @@ set(own_error_cases "own_err_fill_in_loop|:23:11: error: '[^']*' borrows the result of 'Dev.getQ' but is used here after it may be released or overwritten" "own_err_fill_other_field|:20:11: error: '[^']*' borrows the result of 'Dev.getQ' but is used here after it may be released or overwritten" "own_err_fill_after_fill_call|:19:20: error: '[^']*' borrows the result of 'Dev.fill' but is used here after it may be released or overwritten" - "own_err_moved_when_tested_other|:13:13: error: '[^']*' is moved here on some paths only" "own_err_field_moved_call_between|:12:9: error: '[^']*' borrows a field and cannot be stored, returned or captured" "own_err_override_lends|:13:5: error: method 'read' returns a borrow .* but the method of 'Base' it overrides does not" "own_err_moved_field_overwritten|:15:25: error: '[^']*' borrows a field but is used here after it may be released or overwritten" diff --git a/tslang/test/tester/own/own_err_move_some_paths_field.ts b/tslang/test/tester/own/own_err_move_some_paths_field.ts deleted file mode 100644 index 62e2495c7..000000000 --- a/tslang/test/tester/own/own_err_move_some_paths_field.ts +++ /dev/null @@ -1,11 +0,0 @@ -// -mm=own rejects: `a` moves into the field on one path only, and a field store cannot borrow. -class C { x: number = 5; } -class H { c: C = new C(); } -function main(argc: number) { - const h = new H(); - const a = new C(); - if (argc > 5) { - h.c = a; - } - print("done."); -} diff --git a/tslang/test/tester/own/own_err_moved_when_tested_other.ts b/tslang/test/tester/own/own_err_moved_when_tested_other.ts deleted file mode 100644 index ab70c9726..000000000 --- a/tslang/test/tester/own/own_err_moved_when_tested_other.ts +++ /dev/null @@ -1,18 +0,0 @@ -// TSLANG_ISSUES 49: a fresh local moved into a field on some ways only is fine when on the others it -// was tested empty - not here, where `n == "y"` may be false while `l` holds an object. -class Level { - name: string = "a"; -} - -class App { - level: Level | null = null; - read(n: string): Level | null { return n == "" ? null : new Level(); } - start(n: string) { - const l = this.read(n); - if (l && n == "y") { - this.level = l; - } - } -} - -function main() { const a = new App(); a.start("x"); print(a.level != null); } diff --git a/tslang/test/tester/own/own_err_two_owners.ts b/tslang/test/tester/own/own_err_two_owners.ts deleted file mode 100644 index c844de441..000000000 --- a/tslang/test/tester/own/own_err_two_owners.ts +++ /dev/null @@ -1,15 +0,0 @@ -// -mm=own, phase 0 rejects: the array literal would have two owners, the field and `a`. With -// the count check loosened this compiles and double-frees; rc runs it correctly. -class N { v: number[] = [0]; } -function main() { - let sum = 0; - for (let i = 0; i < 100000; i++) { - const n = new N(); - let a = (n.v = [i, i + 1]); - const filler = [i, i, i, i]; - sum += a[1] - i + filler.length; - } - print(sum); - assert(sum == 500000); - print("done."); -} diff --git a/tslang/test/tester/tests/00tslang_issue_78.ts b/tslang/test/tester/tests/00tslang_issue_78.ts index 7a4148820..65deff4b2 100644 --- a/tslang/test/tester/tests/00tslang_issue_78.ts +++ b/tslang/test/tester/tests/00tslang_issue_78.ts @@ -46,4 +46,23 @@ function fieldStore() { objects.push(o); } -function main() { assert(new App().build() == 3, "bufs"); fieldStore(); print("done."); } +// an array stored into a field of a local object and read through a local declared from the store +// (was own_err_two_owners: the local now reads the field) +class N { + v: number[] = [0]; +} + +function aliasedStore(): number { + let sum = 0; + for (let i = 0; i < 1000; i++) { + const n = new N(); + let a = (n.v = [i, i + 1]); + const filler = [i, i, i, i]; + sum += a[1] - i + filler.length; + } + + return sum; +} + +function main() { assert(new App().build() == 3, "bufs"); fieldStore(); assert(aliasedStore() == 5000, "aliased"); + print("done."); } diff --git a/tslang/test/tester/tests/00tslang_issue_81.ts b/tslang/test/tester/tests/00tslang_issue_81.ts index aaf842dd8..587355fe8 100644 --- a/tslang/test/tester/tests/00tslang_issue_81.ts +++ b/tslang/test/tester/tests/00tslang_issue_81.ts @@ -23,11 +23,57 @@ class Model { } } } +// moved into a field on one way only (was own_err_move_some_paths_field) +class C { + x: number = 5; +} + +class H { + c: C = new C(); +} + +function intoField(argc: number): number { + const h = new H(); + const a = new C(); + a.x = 7; + if (argc > 5) { + h.c = a; + } + + return h.c.x; +} + +// moved where a second test passes, not only where the value is set (was own_err_moved_when_tested_other) +class Level { + name: string = "a"; +} + +class App { + level: Level | null = null; + + read(n: string): Level | null { + return n == "" ? null : new Level(); + } + + start(n: string) { + const l = this.read(n); + if (l && n == "y") { + this.level = l; + } + } +} + function main() { const m = new Model(); m.load(4); m.load2(1); m.load2(2); assert(m.lights.length == 4, "lights"); + assert(intoField(1) == 5 && intoField(9) == 7, "field"); + const app = new App(); + app.start("x"); + assert(app.level == null, "x"); + app.start("y"); + assert(app.level != null, "y"); print("done."); } From f1e23645b5a097ffeff415c061b64396ad9edb63 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 19:44:29 +0000 Subject: [PATCH 21/21] TSLANG 80 is open: its test goes; TSLANG_ISSUES says why Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 11 ++++---- tslang/test/tester/CMakeLists.txt | 4 +-- tslang/test/tester/tests/00tslang_issue_80.ts | 28 ------------------- 3 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 tslang/test/tester/tests/00tslang_issue_80.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 78584f345..a4a46cd80 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -618,17 +618,18 @@ early `return this.pipelines[i]` is no longer refused either. `msaa_nri.ts` and 80 (a parameter the function owns, stored into a field, then read again: `constructor(world: GameWorld, public name: string, ...)`, then `${name}` in a loop that calls, in `core/rtxpt_game.ts`, refused "is used here after -its value was moved") is fixed. The local/field pairing reads the field for later reads of the parameter, but -stops at any call, as in that loop. A read of a parameter the function owns, stored into a field, was then -always moved, never copied. It is now copied when the parameter can be read again after the store, so that -read finds it. Regression test `tslang/test/tester/tests/00tslang_issue_80.ts` (every model). +its value was moved") is open. The local/field pairing reads the field for later reads of the parameter, but stops +at any call, as in that loop. Copying the parameter at the store instead leaks it: the caller gave it up, and +nothing releases it then (`own/own_err_string_kept_param_known.ts`; a first fix that copied grew 3M calls to +2.7 GB, and was backed out). The fix is for the pairing to see past calls that cannot reach the object. Meanwhile +`this.name` reads the field. 81 (a value moved on some paths only: `const controller = new LightController(); if (controller.read(...)) { ...; this.lights.push(controller); }` in `core/rtxpt_game.ts`, refused "is moved here on some paths only; -mm=own cannot release it on the others yet") is fixed. Spec 30 placed a local's release on the edges that did not move it; a value, like the `const` here, had no such rule. Now a value one taker moves gets its release on each edge into what follows the move, from where the value is held and was not moved. The release after the move goes. A -use after the paths join is still refused. Regression tests `tslang/test/tester/tests/00tslang_issue_81.ts` +use after the paths join is still refused, a copyable value's read included (`own_string_copy_branch.ts`). Regression tests `tslang/test/tester/tests/00tslang_issue_81.ts` (every model) and `own/own_err_moved_on_some_paths_used.ts`. For native_ts_graphics, from the same sweep (its code, not the compiler): diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ac1254bbf..39a739234 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1929,14 +1929,14 @@ endforeach() # TSLANG_ISSUES 77: an array kept by a constructor, then filled through the local # TSLANG_ISSUES 78: a value pushed on or stored into a field of a local object, then used # TSLANG_ISSUES 79: a local pushed only where it is not null, then used -# TSLANG_ISSUES 80-81: an owned parameter stored into a field, then read; a value moved on some paths only +# TSLANG_ISSUES 81: a value moved on some paths only 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_69 00tslang_issue_71 00tslang_issue_73 00tslang_issue_75 00tslang_issue_76 00tslang_issue_77 00tslang_issue_78 00tslang_issue_79 - 00tslang_issue_80 00tslang_issue_81) + 00tslang_issue_81) 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() diff --git a/tslang/test/tester/tests/00tslang_issue_80.ts b/tslang/test/tester/tests/00tslang_issue_80.ts deleted file mode 100644 index 4831cbc4a..000000000 --- a/tslang/test/tester/tests/00tslang_issue_80.ts +++ /dev/null @@ -1,28 +0,0 @@ -// TSLANG 80: a parameter the function owns, stored into a field, then read again (`constructor(public -// name: string)`, then `${name}` in a loop whose calls stop the field standing in for it): the store -// copies it, so the read finds the parameter. -@final -class Ctl { - node: int = 0; - read(ok: boolean): boolean { return ok; } -} -@final -class Model { - lights: Ctl[] = []; - constructor(public name: string, n: int) { - for (let i = 0; i < n; i++) { - if (i == 1) { - print(`skip ${name}`); - continue; - } - const c = new Ctl(); - c.node = i; - this.lights.push(c); - } - } -} -function main() { - const m = new Model("m", 4); - assert(m.lights.length == 3 && m.name == "m", "model"); - print("done."); -}