Skip to content

Commit 086d0ed

Browse files
-mm=own: calls in a global's initializer get their callees' facts
A global's region is no function: the signature pass pinned no facts on its calls and the inference never runs there, so a call was read with the defaults, an owned result and borrowed arguments. - `const c = child(n)` (child returns `n.c`), or `id(new V(1))`, made the global own a block its argument's owner keeps, or one the region freed: heap corruption, or a stale read under --opt. Now an error: a global cannot hold a borrow. - `const n = new N(new V(4))`, the constructor keeping `c`, released the `V` at the end of the region under the object that kept it. A kept argument made in the region and used by nothing after the call now moves into the callee (its release goes); a literal is given as it is; any other kept argument is an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 35ac7ff commit 086d0ed

6 files changed

Lines changed: 210 additions & 0 deletions

File tree

‎tslang/lib/TypeScript/OwnershipSignaturePass.cpp‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
251251
}
252252

253253
exportNoDrops();
254+
ownGlobalInitializerCalls(module);
254255

255256
module.walk([&](mlir_ts::CreateBoundFunctionOp boundOp) {
256257
if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME))
@@ -2353,6 +2354,107 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
23532354
llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); });
23542355
}
23552356

2357+
// ---- Calls in a global's initializer ----
2358+
2359+
// A global's region is no function: its calls get no facts pinned, and the inference never sees
2360+
// it, so its calls were read with the defaults - an owned result, borrowed arguments. A result
2361+
// that borrows an argument (`const c = child(n)`, `id(new V(1))`) then made the global a second
2362+
// owner of a block its argument's owner keeps, or kept one the region freed; and a fresh
2363+
// argument a callee keeps (`new N(new V(4))`, the constructor keeping `c`) was released at the
2364+
// end of the region, under the object that kept it. A kept argument made in the region and used
2365+
// by nothing after the call moves into the callee: its release goes. Anything else is an error.
2366+
void ownGlobalInitializerCalls(mlir::ModuleOp module)
2367+
{
2368+
module.walk([&](mlir_ts::GlobalOp globalOp) {
2369+
// collected first: a move erases a release the walk has not reached yet
2370+
llvm::SmallVector<mlir::Operation *> calls;
2371+
globalOp.walk([&](mlir::Operation *op) {
2372+
if (isCall(op))
2373+
{
2374+
calls.push_back(op);
2375+
}
2376+
});
2377+
2378+
for (auto *op : calls)
2379+
{
2380+
auto callees = resolve(op);
2381+
auto facts = callFacts(callees);
2382+
auto calleeName = callees.funcs.empty() ? llvm::StringRef("the callee") : callees.funcs.front().getSymName();
2383+
if (facts.borrows >= 0)
2384+
{
2385+
op->emitError("the initializer of global '")
2386+
<< globalOp.getSymName() << "' holds the result of '" << calleeName
2387+
<< "', which borrows its argument; a global cannot hold a borrow under -mm=own";
2388+
signalPassFailure();
2389+
continue;
2390+
}
2391+
2392+
auto args = callArgs(op);
2393+
llvm::SmallPtrSet<mlir::Value, 4> given;
2394+
for (auto index : facts.owned)
2395+
{
2396+
if (index < 0 || index >= static_cast<int32_t>(args.size()) ||
2397+
!given.insert(rootOf(args[index])).second || !moveIntoCallee(op, args[index]))
2398+
{
2399+
op->emitError("the initializer of global '")
2400+
<< globalOp.getSymName() << "' gives '" << calleeName
2401+
<< "' a value it keeps, but -mm=own cannot move it there";
2402+
signalPassFailure();
2403+
break;
2404+
}
2405+
}
2406+
}
2407+
});
2408+
}
2409+
2410+
// `value` was made in the call's block and nothing but its release, if it has one, comes after
2411+
// the call: the release goes, and the callee owns the block. Constant data (a string literal)
2412+
// is nobody's, and is given as it is.
2413+
static bool moveIntoCallee(mlir::Operation *call, mlir::Value value)
2414+
{
2415+
auto root = rootOf(value);
2416+
if (isConstantData(root))
2417+
{
2418+
return true;
2419+
}
2420+
2421+
if (!isFresh(root) || root.getDefiningOp()->getBlock() != call->getBlock())
2422+
{
2423+
return false;
2424+
}
2425+
2426+
mlir::Operation *release = nullptr;
2427+
auto usedAfter = false;
2428+
forEachUse(root, [&](mlir::Operation *user, mlir::Value) {
2429+
if (user == call)
2430+
{
2431+
return;
2432+
}
2433+
2434+
if (user->getBlock() != call->getBlock())
2435+
{
2436+
usedAfter = true;
2437+
}
2438+
else if (call->isBeforeInBlock(user))
2439+
{
2440+
usedAfter = usedAfter || release || !mlir::isa<mlir_ts::ReleaseOp>(user);
2441+
release = mlir::isa<mlir_ts::ReleaseOp>(user) ? user : release;
2442+
}
2443+
});
2444+
2445+
if (usedAfter)
2446+
{
2447+
return false;
2448+
}
2449+
2450+
if (release)
2451+
{
2452+
release->erase();
2453+
}
2454+
2455+
return true;
2456+
}
2457+
23562458
// ---- Export ----
23572459

23582460
// A library says, beside its memory-model marker, which of its exported functions destroy

‎tslang/test/tester/CMakeLists.txt‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3503,6 +3503,13 @@ foreach(global_mm rc none gc)
35033503
tslang_add_test(NAME test-compile-${global_mm}-own-global-script COMMAND test-runner -mm=${global_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_script.ts")
35043504
endforeach()
35053505

3506+
# A global made by a call that keeps an argument made for it owns that argument through what it was
3507+
# given; the same program under every model.
3508+
foreach(kept_mm own rc none gc)
3509+
tslang_add_test(NAME test-jit-${kept_mm}-own-global-kept-argument COMMAND test-runner -jit -mm=${kept_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_kept_argument.ts")
3510+
tslang_add_test(NAME test-compile-${kept_mm}-own-global-kept-argument COMMAND test-runner -mm=${kept_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_global_kept_argument.ts")
3511+
endforeach()
3512+
35063513
# A `let` given a new value after its value moved owns the new one (#528); the same program under
35073514
# rc and gc (not none: nothing is freed there, and the loops would hold about 800 MB).
35083515
foreach(reassign_mm rc gc)
@@ -3700,6 +3707,9 @@ set(own_error_cases
37003707
"own_err_param_assigned|is a parameter and cannot be assigned under -mm=own yet"
37013708
"own_err_local_owns_nothing|owns nothing under -mm=own .* so it cannot be given a new value here"
37023709
"own_err_merged_result|is merged from several branches"
3710+
"own_err_global_borrowed_result|the initializer of global 'c' holds the result of 'child', which borrows its argument"
3711+
"own_err_global_borrowed_temporary|the initializer of global 'c' holds the result of 'id', which borrows its argument"
3712+
"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"
37033713
"own_err_global_returned|borrows the global 'V.def' and cannot be stored, returned or captured"
37043714
"own_err_global_let_call|borrows the global 'names', but -mm=own cannot prove the global keeps its value"
37053715
"own_err_captured_returned|borrows a captured variable and cannot be stored, returned or captured"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// -mm=own rejects: `child` returns a borrow of its argument's field, so `c` would be a second owner
2+
// of the block `n.c` owns, and `n.c = new V(5)` frees what `c` still reads. A global cannot borrow.
3+
class V {
4+
constructor(public x: number) {}
5+
}
6+
7+
class N {
8+
constructor(public c: V) {}
9+
}
10+
11+
const n = new N(new V(4));
12+
13+
function child(m: N) {
14+
return m.c;
15+
}
16+
17+
const c = child(n);
18+
19+
function main() {
20+
n.c = new V(5);
21+
print(c.x, n.c.x);
22+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// -mm=own rejects: `id` returns a borrow of its argument, a temporary the initializer frees at its
2+
// end, so `c` would read a freed block. A global cannot borrow.
3+
class V {
4+
constructor(public x: number) {}
5+
}
6+
7+
function id(v: V) {
8+
return v;
9+
}
10+
11+
const c = id(new V(1));
12+
13+
function main() {
14+
print(c.x);
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// -mm=own rejects: the constructor keeps `c`, but what it is given is the global `v`, which keeps
2+
// its value, so `n` and `v` would both own one block. Only a value made for the call moves.
3+
class V {
4+
constructor(public x: number) {}
5+
}
6+
7+
class N {
8+
constructor(public c: V) {}
9+
}
10+
11+
const v = new V(1);
12+
const n = new N(v);
13+
14+
function main() {
15+
print(n.c.x, v.x);
16+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// A global made by a call that keeps an argument made for it (`new N(new V(4))`, the constructor
2+
// keeping `c`; a function that stores its parameter in what it returns) owns that argument through
3+
// what it was given. Under own the argument used to be released at the end of the global's
4+
// initializer, under the object that kept it. Each read follows allocations of the same size, so a
5+
// freed block is reused.
6+
class V {
7+
tag = "";
8+
constructor(public x: number) {
9+
this.tag = "v" + x;
10+
}
11+
}
12+
13+
class N {
14+
constructor(public c: V) {}
15+
}
16+
17+
function wrap(v: V) {
18+
const n = new N(v);
19+
return n;
20+
}
21+
22+
class Junk {
23+
a = -1;
24+
b = -2;
25+
tag = "junk";
26+
}
27+
28+
function churn() {
29+
let keep: Junk[] = [];
30+
for (let i = 0; i < 1000; i++) keep.push(new Junk());
31+
return keep.length;
32+
}
33+
34+
const n = new N(new V(4));
35+
let w = wrap(new V(5));
36+
37+
function main() {
38+
churn();
39+
assert(n.c.x == 4 && n.c.tag == "v4", "a constructor's kept argument");
40+
assert(w.c.x == 5 && w.c.tag == "v5", "a function's kept argument");
41+
w = wrap(new V(6));
42+
churn();
43+
assert(w.c.x == 6 && n.c.x == 4, "the global given a new value");
44+
print("done.");
45+
}

0 commit comments

Comments
 (0)