Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion TSLANG_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down Expand Up @@ -791,3 +791,24 @@ 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). 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`.
113 changes: 111 additions & 2 deletions tslang/lib/TypeScript/OwnershipSignatureGlobals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,20 @@ 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;
forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) {
// 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 used) {
released = released || mlir::isa<mlir_ts::ReleaseOp, mlir_ts::ReleaseSlotOp>(user);
releasedOnce += mlir::isa<mlir_ts::ReleaseOp>(user);
retained += mlir::isa<mlir_ts::RetainOp>(user);
onlyConcatenated = onlyConcatenated &&
(mlir::isa<mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::ArithmeticBinaryOp, mlir_ts::StringConcatOp>(user) ||
(isCall(user) && llvm::is_contained(callArgs(user), used)));
});

if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value))
Expand All @@ -332,6 +342,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<mlir_ts::StringType>(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();
Expand Down Expand Up @@ -542,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<mlir::Value, 4> 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
Expand All @@ -574,6 +601,88 @@ namespace own_signature
});
}

bool OwnershipSignaturePass::takeOverTemporaryReceiver(mlir::Operation *call, const Callees &callees, const Facts &facts,
const llvm::SmallPtrSetImpl<mlir::Value> &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<size_t>(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<mlir_ts::ClassType>(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<mlir_ts::ClassType>(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<mlir::Operation *> releases;
std::function<bool(mlir::Operation *, int)> 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<mlir_ts::ReleaseOp>(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);
Expand Down
7 changes: 7 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,13 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
// is nobody's, and is given as it is.
static bool moveIntoCallee(mlir::Operation *call, mlir::Value value);

// A method call in a global's initializer whose result is the object it is called on, itself
// (spec 78; a function returning its parameter stays an error, #638), on an object the initializer made (`new Bag()`, or such a call's result in `ownedResults`)
// and gives to nothing after the call but its releases: the result is that object, the
// initializer's own. The object's releases go. `const B = new Bag().add(a).add(b)`.
static bool takeOverTemporaryReceiver(mlir::Operation *call, const Callees &callees, const Facts &facts,
const llvm::SmallPtrSetImpl<mlir::Value> &ownedResults);

// ---- Export ----

// A library says, beside its memory-model marker, which of its exported functions destroy
Expand Down
5 changes: 4 additions & 1 deletion tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1933,14 +1933,15 @@ 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
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_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()
Expand Down Expand Up @@ -5267,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"
Expand Down
17 changes: 17 additions & 0 deletions tslang/test/tester/own/own_err_global_chain_other_global.ts
Original file line number Diff line number Diff line change
@@ -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);
}
12 changes: 12 additions & 0 deletions tslang/test/tester/own/own_err_global_concat_borrowed.ts
Original file line number Diff line number Diff line change
@@ -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);
}
89 changes: 89 additions & 0 deletions tslang/test/tester/tests/00tslang_issue_85.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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"). 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";

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";

// 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");
}

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");
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.");
}
Loading