From cd443c7e5e6938e7e124200e5fd4f564fce0c5b5 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 15 Nov 2026 00:24:38 +0000 Subject: [PATCH 1/2] own: a global of three or more concatenated strings (TSLANG 85) The inner sum of `"a" + "b" + "c"` in a global's initializer is a temporary the initializer makes, reads and gives back itself. moveIntoGlobals took its retain for a second reference the global takes. A fresh string retained once and released once, read only by a concatenation, now keeps its release and loses the retain, as in a function body. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 13 +++++- .../TypeScript/OwnershipSignatureGlobals.cpp | 16 +++++++ tslang/test/tester/CMakeLists.txt | 3 +- tslang/test/tester/tests/00tslang_issue_85.ts | 45 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tslang/test/tester/tests/00tslang_issue_85.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index f37f79eed..441e0788f 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -763,7 +763,7 @@ old compiler, its exe hangs at exit). native_ts_graphics: with #321 and #322 (2026-11-14, on 5aa1cdb0 with workarounds) every source compiles under `-mm=own` and its ctest passes 525/525, as under gc and rc. -85 (open, found 2026-11-14 on pre-alpha128 (0c8439cb); native_ts_graphics `examples/sakura_forest_ser.ts` +85 (fixed; found 2026-11-14 on pre-alpha128 (0c8439cb); native_ts_graphics `examples/sakura_forest_ser.ts` `LIBRARY_EXPORTS`): under `-mm=own`, a module-level `const` whose initializer concatenates three or more string literals is refused at the initializer. Run with `-mm=own --emit=jit` (gc prints `Raygen,Miss,Hit,Group`): @@ -791,3 +791,14 @@ something else holds. The whole initializer is a constant (literals only), so it literal, as 70 made a literal-typed constant global its literal. Suspect (not checked in the source yet): the global-initializer check in the own pass treating the nested `+`'s left operand as a borrow. Workaround in native_ts_graphics: a function returning the concatenation (`libraryExports()`). + +Fixed: the inner sum is a temporary the initializer makes, reads and gives back itself. rc's retain on it is +the reference it arrives with and its release gives that back, as in a function body, but the check that +moves a value into a global took every retain in the initializer for the global's: one retain, a release, +and no move into the result read as a second reference. A fresh string retained once and released once, +that only a concatenation reads, now keeps its release and loses the retain. Four literals, a number in +the chain, a template literal, and other globals, a field or an element read into the chain work too +(a call that returns a borrow of its argument is still refused). Still open: a string +built this way and given to a constructor in a global's initializer (`const H = new Holder("x" + "y")`, +two literals are enough) is refused the same way, as before. Regression test +`tslang/test/tester/tests/00tslang_issue_85.ts` (every model). diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp index 12c9664e3..09036e190 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -320,10 +320,16 @@ namespace own_signature // nothing in the region gives the block back, or takes a second reference to it: // the global is its one owner auto released = false; + auto releasedOnce = 0; auto retained = 0; + // read only by a concatenation, which copies what it reads into a string of its own + auto onlyConcatenated = true; forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { released = released || mlir::isa(user); + releasedOnce += mlir::isa(user); retained += mlir::isa(user); + onlyConcatenated = onlyConcatenated && + mlir::isa(user); }); if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) @@ -332,6 +338,16 @@ namespace own_signature continue; } + // a temporary the initializer makes, reads and gives back itself (TSLANG 85: the inner + // sum of `"a" + "b" + "c"`): rc's retain is the reference the value arrives with, which + // the release then gives back, as in a function body + if (retained == 1 && releasedOnce == 1 && onlyConcatenated && isFresh(value) && + mlir::isa(value.getType())) + { + retainOp.erase(); + continue; + } + retainOp.emitError("the initializer of global '") << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; signalPassFailure(); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 9ebb8571d..341622748 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -1933,6 +1933,7 @@ endforeach() # TSLANG_ISSUES 82: a closure calling a function value it captured # TSLANG_ISSUES 83: a row of a global array of constant arrays bound to a local # TSLANG_ISSUES 84: a borrowed call result across a release of a type that cannot hold it +# TSLANG_ISSUES 85: a global whose initializer concatenates three or more strings 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 @@ -1940,7 +1941,7 @@ foreach(issues_mm gc rc none own) 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_82 - 00tslang_issue_83 00tslang_issue_84) + 00tslang_issue_83 00tslang_issue_84 00tslang_issue_85) 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_85.ts b/tslang/test/tester/tests/00tslang_issue_85.ts new file mode 100644 index 000000000..7665a25c2 --- /dev/null +++ b/tslang/test/tester/tests/00tslang_issue_85.ts @@ -0,0 +1,45 @@ +// TSLANG 85: a global whose initializer concatenates three or more strings. The inner sum is a +// temporary the initializer reads and gives back itself; -mm=own took its reference for a second +// one the global takes ("the initializer of global 'M.EXPORTS' takes a second reference"). +namespace M { + const EXPORTS = "Raygen,Miss," + + "Hit," + "Group"; + + function show(names: string) { + print(names); + return names.length; + } + + export function run() { + return show(EXPORTS); + } +} + +const FOUR = "a" + "b" + "c" + "d"; +const MIXED = "n=" + 1 + "," + 2; +const FROM_GLOBALS = FOUR + "-" + MIXED + "-" + FOUR; + +const TEMPLATE = `${FOUR}!` + "?" + MIXED; + +class H { + text: string = "t" + "u"; +} + +const G = new H(); +const FROM_FIELD = G.text + "x" + "y"; +const ROWS = ["p" + "q"]; +const FROM_ELEMENT = ROWS[0] + "x" + "y"; + +function main() { + for (let i = 0; i < 1000; i++) { + assert(FOUR == "abcd", "four"); + } + + assert(M.run() == 21, "namespace"); + assert(MIXED == "n=1,2", "mixed"); + assert(FROM_GLOBALS == "abcd-n=1,2-abcd", "from globals"); + assert(TEMPLATE == "abcd!?n=1,2", "template"); + assert(FROM_FIELD == "tuxy" && G.text == "tu", "field"); + assert(FROM_ELEMENT == "pqxy" && ROWS[0] == "pq", "element"); + print("done."); +} From c58bd8a80f422bc05b654e6649e977201051b622 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 15 Nov 2026 00:37:11 +0000 Subject: [PATCH 2/2] own: a concatenation given to a call, and a this-returning chain, in a global's initializer (TSLANG 85) `const H = new Holder("x" + "y")` was refused ("takes a second reference"): the sum is a call argument, which moveIntoGlobals did not accept. ownGlobalInitializerCalls, after the facts, already moves it into a callee that keeps it, or leaves its release. `const B = new Bag().add("m").add("n" + "o")`, with `add(): this`, was refused as a borrow. A method call whose result is its own `this` (spec 78), on an object the initializer made and gives to nothing after but its releases, now returns that object, the global's own, as transferTemporaryReceivers does in function bodies. A chain on another global's object, and a function returning its parameter (#638), stay errors. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 18 +++- .../TypeScript/OwnershipSignatureGlobals.cpp | 101 +++++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.h | 7 ++ tslang/test/tester/CMakeLists.txt | 2 + .../own/own_err_global_chain_other_global.ts | 17 +++ .../own/own_err_global_concat_borrowed.ts | 12 +++ tslang/test/tester/tests/00tslang_issue_85.ts | 46 +++++++- 7 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 tslang/test/tester/own/own_err_global_chain_other_global.ts create mode 100644 tslang/test/tester/own/own_err_global_concat_borrowed.ts diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 441e0788f..95bf0bb05 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -798,7 +798,17 @@ moves a value into a global took every retain in the initializer for the global' and no move into the result read as a second reference. A fresh string retained once and released once, that only a concatenation reads, now keeps its release and loses the retain. Four literals, a number in the chain, a template literal, and other globals, a field or an element read into the chain work too -(a call that returns a borrow of its argument is still refused). Still open: a string -built this way and given to a constructor in a global's initializer (`const H = new Holder("x" + "y")`, -two literals are enough) is refused the same way, as before. Regression test -`tslang/test/tester/tests/00tslang_issue_85.ts` (every model). +(a call that returns a borrow of its argument is still refused). So does a sum given to a call, which +was refused even of two strings (`const H = new Holder("x" + "y")`): the global-initializer step after the +facts moves it into a callee that keeps it, so its release goes, and leaves the release after a callee +that only reads it. A callee whose result borrows it is refused, "holds the result of 'id', which borrows +its argument". A chain returning `this` on a new object (`const B = new Bag().add("m").add("n" + "o")`, +with `add(s: string): this` or a `return this` the body infers) was refused too, "holds the result of +'Bag.add', which borrows its argument", with a plain literal as well. A function body takes the object +over (39, spec 78), but only for calls in functions. The global step now does the same: a call whose result +is its `this` itself, on an object the initializer made (a `new`, or the call before it in the chain) +and gives to nothing after but its releases, returns that object, the global's own, and its releases go. +A chain on another global's object (`const A = SHARED.add("q")`) is still refused: `A` would be a second +owner. So is a function returning its parameter (`const c = id(new V(1))`, #638): only a method's +`this` is taken over. Regression tests `tslang/test/tester/tests/00tslang_issue_85.ts` (every model), +`own/own_err_global_concat_borrowed.ts` and `own/own_err_global_chain_other_global.ts`. diff --git a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp index 09036e190..7c57880b8 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp @@ -322,14 +322,18 @@ namespace own_signature auto released = false; auto releasedOnce = 0; auto retained = 0; - // read only by a concatenation, which copies what it reads into a string of its own + // read only by a concatenation, which copies what it reads into a string of its own, or + // given to a call as an argument: ownGlobalInitializerCalls, run after the facts, moves it + // into a callee that keeps it (the release goes), and refuses a callee whose result + // borrows it auto onlyConcatenated = true; - forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { + forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value used) { released = released || mlir::isa(user); releasedOnce += mlir::isa(user); retained += mlir::isa(user); onlyConcatenated = onlyConcatenated && - mlir::isa(user); + (mlir::isa(user) || + (isCall(user) && llvm::is_contained(callArgs(user), used))); }); if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) @@ -558,12 +562,19 @@ namespace own_signature } }); + // the results that are the initializer's own: a chain's calls, in order, each given the one + // before it (`new Bag().add(a).add(b)`) + llvm::SmallPtrSet ownedResults; for (auto *op : calls) { auto callees = resolve(op); auto facts = callFacts(callees); auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName(); - if (!facts.borrows.empty()) + if (!facts.borrows.empty() && takeOverTemporaryReceiver(op, callees, facts, ownedResults)) + { + ownedResults.insert(op->getResult(0)); + } + else if (!facts.borrows.empty()) { op->emitError("the initializer of global '") << globalOp.getSymName() << "' holds the result of '" << calleeName @@ -590,6 +601,88 @@ namespace own_signature }); } + bool OwnershipSignaturePass::takeOverTemporaryReceiver(mlir::Operation *call, const Callees &callees, const Facts &facts, + const llvm::SmallPtrSetImpl &ownedResults) + { + // a result that is the one argument it borrows, itself (spec 78), as transferTemporaryReceivers + // takes it in a function body: `this` returned by `add(s: string): this` + auto args = callArgs(call); + if (!facts.itself || facts.record || facts.borrows.size() != 1 || call->getNumResults() != 1) + { + return false; + } + + auto index = facts.borrows.front(); + if (index < 0 || static_cast(index) >= args.size() || llvm::is_contained(facts.owned, index)) + { + return false; + } + + // a method called through the object's vtable or by its symbol, returning the object it is bound + // to. A function returning its parameter (`id(new V(1))`) stays an error here (#638): only a + // chain's `this` is taken over. + // Called directly (`ts.SymbolCallInternal`, by now), the object is the first argument, and the + // callee a method of its class (`Bag.add`). + auto object = boundThis(args[index]); + if (!object && index == 0) + { + auto classType = mlir::dyn_cast(args[index].getType()); + auto prefix = classType ? (classType.getName().getValue() + ".").str() : std::string(); + auto isMethod = classType && !callees.funcs.empty() && llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp funcOp) { + return funcOp.getSymName().starts_with(prefix); + }); + object = isMethod ? args[index] : mlir::Value(); + } + + if (!object || !mlir::isa(object.getType())) + { + return false; + } + + auto receiver = rootOf(object); + + // the initializer's own: made here (`new Bag()`), or the result of a call taken over before + auto *producer = receiver.getDefiningOp(); + if (!producer || producer->getBlock() != call->getBlock() || !(isFresh(receiver) || ownedResults.contains(receiver))) + { + return false; + } + + // given to nothing after the call but its releases; a use before it has its own uses before it + // too, or the call itself (rc's header of a new object, the method bound to it) + llvm::SmallVector releases; + std::function before = [&](mlir::Operation *other, int depth) { + return depth < 4 && other->getBlock() == call->getBlock() && other->isBeforeInBlock(call) && + llvm::all_of(other->getUsers(), [&](mlir::Operation *use) { return use == call || before(use, depth + 1); }); + }; + auto temporary = llvm::all_of(receiver.getUsers(), [&](mlir::Operation *user) { + if (user == call) + { + return true; + } + + if (mlir::isa(user) && user->getBlock() == call->getBlock() && call->isBeforeInBlock(user)) + { + releases.push_back(user); + return true; + } + + return before(user, 0); + }); + if (!temporary) + { + return false; + } + + // the result is the object itself, the initializer's: its releases are the result's to make + for (auto *release : releases) + { + release->erase(); + } + + return true; + } + bool OwnershipSignaturePass::moveIntoCallee(mlir::Operation *call, mlir::Value value) { auto root = rootOf(value); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 0ae951c1d..d9d5ba92d 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -1190,6 +1190,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper &ownedResults); + // ---- Export ---- // A library says, beside its memory-model marker, which of its exported functions destroy diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 341622748..b6070fd6b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -5268,6 +5268,8 @@ set(own_error_cases "own_err_merged_result|:7:5: error: '[^']*' is merged from several branches [(]`[?]:`, `&&`, `[|][|]`, `[?][?]`, a loop[)]" "own_err_global_borrowed_result|the initializer of global 'c' holds the result of 'child', which borrows its argument" "own_err_global_borrowed_temporary|:11:11: error: the initializer of global 'c' holds the result of 'id', which borrows its argument" + "own_err_global_concat_borrowed|:8:11: error: the initializer of global 'B' holds the result of 'id', which borrows its argument" + "own_err_global_chain_other_global|:13:15: error: the initializer of global 'ALIAS' holds the result of 'Bag.add', which borrows its argument" "own_err_global_kept_not_fresh|the initializer of global 'n' gives 'N.constructor' a value it keeps, but -mm=own cannot move it there" "own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured" "own_err_immortal_borrowed_init|:9:5: error: '[^']*' borrows 'K[.]list\\[\\]' and cannot be stored, returned or captured" diff --git a/tslang/test/tester/own/own_err_global_chain_other_global.ts b/tslang/test/tester/own/own_err_global_chain_other_global.ts new file mode 100644 index 000000000..cb204e3a1 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_chain_other_global.ts @@ -0,0 +1,17 @@ +// -mm=own rejects: `add` returns its `this`, here the object `SHARED` owns, so `ALIAS` would be a +// second owner of it. A chain on a new object (`new Bag().add("q")`) is the global's own (TSLANG 85). +class Bag { + items: string[] = []; + + add(s: string): this { + this.items.push(s); + return this; + } +} + +const SHARED = new Bag(); +const ALIAS = SHARED.add("q"); + +function main() { + print(ALIAS.items.length); +} diff --git a/tslang/test/tester/own/own_err_global_concat_borrowed.ts b/tslang/test/tester/own/own_err_global_concat_borrowed.ts new file mode 100644 index 000000000..2d6470cf4 --- /dev/null +++ b/tslang/test/tester/own/own_err_global_concat_borrowed.ts @@ -0,0 +1,12 @@ +// -mm=own rejects: `id` returns a borrow of its argument, the sum `"a" + "b"` the initializer +// frees once the call is made, so `B` would hold a freed string (TSLANG 85: a concatenation given +// to a call in a global's initializer is moved into a callee that keeps it, or freed after it). +function id(s: string): string { + return s; +} + +const B = id("a" + "b") + "c"; + +function main() { + print(B); +} diff --git a/tslang/test/tester/tests/00tslang_issue_85.ts b/tslang/test/tester/tests/00tslang_issue_85.ts index 7665a25c2..31d13f59b 100644 --- a/tslang/test/tester/tests/00tslang_issue_85.ts +++ b/tslang/test/tester/tests/00tslang_issue_85.ts @@ -1,6 +1,8 @@ // TSLANG 85: a global whose initializer concatenates three or more strings. The inner sum is a // temporary the initializer reads and gives back itself; -mm=own took its reference for a second -// one the global takes ("the initializer of global 'M.EXPORTS' takes a second reference"). +// one the global takes ("the initializer of global 'M.EXPORTS' takes a second reference"). So it +// did for a sum given to a call, even of two strings: `new Holder("x" + "y")`, and a chain returning +// `this` on a new object was refused as a borrow: `new Bag().add("m")`. namespace M { const EXPORTS = "Raygen,Miss," + "Hit," + "Group"; @@ -30,6 +32,44 @@ const FROM_FIELD = G.text + "x" + "y"; const ROWS = ["p" + "q"]; const FROM_ELEMENT = ROWS[0] + "x" + "y"; +// given to a constructor that keeps it, and to a function that only reads it +class Holder { + constructor(public text: string) {} +} + +function len(s: string) { + return s.length; +} + +const HELD = new Holder("x" + "y" + "z"); +const HELD_TWO = new Holder("p" + "q"); +const LENGTH = len("a" + "b" + "c"); +const BOTH = new Holder("u" + "v").text + "w" + len("r" + "s"); + +// a chain of calls returning `this` on a new object: the result is that object, the global's own +class Bag { + items: string[] = []; + + add(s: string): this { + this.items.push(s); + return this; + } +} + +class Counter { + n = 0; + + bump() { + this.n++; + return this; + } +} + +const BAG = new Bag().add("m").add("n" + "o"); +const ONE = new Bag().add("p"); +const COUNTER = new Counter().bump().bump(); +const COUNT = new Bag().add("a").add("b" + "c").items.length; + function main() { for (let i = 0; i < 1000; i++) { assert(FOUR == "abcd", "four"); @@ -41,5 +81,9 @@ function main() { assert(TEMPLATE == "abcd!?n=1,2", "template"); assert(FROM_FIELD == "tuxy" && G.text == "tu", "field"); assert(FROM_ELEMENT == "pqxy" && ROWS[0] == "pq", "element"); + assert(HELD.text == "xyz" && HELD_TWO.text == "pq", "held"); + assert(LENGTH == 3 && BOTH == "uvw2", "call"); + assert(BAG.items.length == 2 && BAG.items[0] == "m" && BAG.items[1] == "no", "chain"); + assert(ONE.items[0] == "p" && COUNTER.n == 2 && COUNT == 2, "chains"); print("done."); }