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
Original file line number Diff line number Diff line change
Expand Up @@ -2754,3 +2754,34 @@ errors and the same pattern passes 267 of 267, the Windows build's count.
popped out of an array keeps its count (2 after the pop of a two-handle block, under rc and own
alike). The re-review's `arronly` (a `const` read out of a global array, then `arr = []`)
prints 2 under rc and 1 under own, as before this round.

## 24. Assignment after a move (#528), 2026-10-06

Phase 1 (§12.1) counted an assignment to a moved-from `let` as a use after the move: rc's
assignment releases what the slot holds before it stores (`ts.ReleaseSlot` right before
`ts.Store`), and after a move that is the receiver's value. So `let b = a; a = new C()` and
`h.c = a; a = new C()` were errors, although Rust accepts both. Now the moved-from slot owns the
new value:

- **An assignment is a new value.** A path from the move through an assignment of the slot
reaches a new value, as a path through the declaration does. Uses only such paths reach are
not uses after the move. The assignment's own release goes with the move, since what the slot
holds is the receiver's. A release only a path through an assignment reaches stays, and
destroys the new value.
- **Some paths.** A release that the move reaches both holding the moved value and holding a
newer one is `moved here on some paths only`. For example, `h.c = a; if (x) { a = new C(); }`
reaches the scope exit both ways (`own_err_reassign_some_paths`). A read on such a path is
still a use after the move (`own_err_assign_after_move`, now this shape).
- **Receivers per value.** Receivers of one slot that each read a value of their own are
decided one at a time: `h.c = a; a = new C(); g.c = a` moves twice. Two receivers read values
of their own when an assignment comes between their reads on every path between them.
Otherwise the receivers are decided together, as before (§13).
- **Loops.** `keep.push(a); a = new C()` in a loop body moves each iteration's value: the move's
read is met again only through the assignment. A path from the move back to a use of a read
that passes through the read again uses the value read again (`readUsedAfter`).

`own_err_borrow_bound_method`, `own_err_borrow_catch_local` and `own_err_borrow_finally_local`
used `let b = a; … a = new C()` to make `b` a borrow, but that is a move now. Each reads `a` once
after `let b = a`, which keeps `b` a borrow. `own_move_reassign` runs the shapes under own, rc
and gc: own holds 4.4 MB, as rc's 4.5 MB. On a quarantine heap it runs clean; with the
assignment's release kept, it is a double free. Corpus: 467 of 628 compile under own, unchanged.
140 changes: 118 additions & 22 deletions tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1817,31 +1817,71 @@ class OwnershipInferencePass
void decideSlot(mlir::Value slot, llvm::ArrayRef<SlotReceiver> receivers, llvm::SetVector<mlir::Operation *> &toErase)
{
auto owner = varName(slot.getDefiningOp<mlir_ts::VariableOp>());
if (receivers.size() == 1)
if (receivers.size() == 1 || takeValuesOfTheirOwn(slot, receivers))
{
auto &receiver = receivers.front();
if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); }))
for (auto &receiver : receivers)
{
// rc's retain goes with the move; a call that keeps the argument is the move itself
if (!isCall(receiver.retain))
{
toErase.insert(receiver.retain);
}

return;
decideSlotReceiver(slot, receiver, owner, toErase);
}

if (auto borrower = borrowerOf(receiver.retain))
return;
}

decideSlotReceivers(slot, receivers, owner, toErase);
}

void decideSlotReceiver(mlir::Value slot, const SlotReceiver &receiver, llvm::StringRef owner,
llvm::SetVector<mlir::Operation *> &toErase)
{
if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); }))
{
// rc's retain goes with the move; a call that keeps the argument is the move itself
if (!isCall(receiver.retain))
{
borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase);
return;
toErase.insert(receiver.retain);
}

checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); // reports
return;
}

decideSlotReceivers(slot, receivers, owner, toErase);
if (auto borrower = borrowerOf(receiver.retain))
{
borrowWith(borrower, receiver.retain, slotEnds(slot), owner, toErase);
return;
}

checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); // reports
}

// Receivers of one owning local that each read a value of its own (#528): `h.c = a; a = new C();
// g.c = a`. An assignment comes between every two of their reads on every path from one to the
// other, so each is decided alone, as the one receiver of the value it reads.
bool takeValuesOfTheirOwn(mlir::Value slot, llvm::ArrayRef<SlotReceiver> receivers)
{
auto assignments = slotAssignments(slot);
if (assignments.empty())
{
return false;
}

llvm::SmallVector<mlir::Operation *> kills(assignments.begin(), assignments.end());
if (auto varOp = slot.getDefiningOp<mlir_ts::VariableOp>())
{
kills.push_back(varOp);
}

for (auto &from : receivers)
{
for (auto &to : receivers)
{
if (&from != &to && reachableAfter(from.load, to.load, kills))
{
return false;
}
}
}

return true;
}

// A parameter this function keeps (`__own_params`): its caller moved the argument in, and this
Expand Down Expand Up @@ -4172,8 +4212,13 @@ class OwnershipInferencePass
// A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and
// retained what it read for the receiver; the move erases that retain and every release of
// the slot the move reaches, which must be dominated by it. Any other use of the slot the
// move reaches - a read, an assignment - is a use after the move. Reports and returns false
// when it is not a move.
// move reaches - a read, an assignment's own read of it - is a use after the move. Reports and
// returns false when it is not a move.
//
// An assignment the move reaches gives the slot a new value to own (#528): a path through it
// is a new value, as a path through the declaration is. Its release before the store goes
// with the move - what the slot held is the receiver's - and the releases only a path through
// an assignment reaches stay, to destroy the new value.
bool checkSlotMove(mlir::Operation *retain, mlir::Value value, mlir_ts::LoadOp load,
llvm::SetVector<mlir::Operation *> &toErase)
{
Expand All @@ -4188,8 +4233,15 @@ class OwnershipInferencePass
return false;
}

auto assignments = slotAssignments(slot);
llvm::SmallVector<mlir::Operation *> kills(assignments.begin(), assignments.end());
if (varOp)
{
kills.push_back(varOp);
}

// the move's own read, met again around a loop the slot was declared outside of
if (reachableAfter(taker, load, varOp))
if (reachableAfter(taker, load, kills))
{
reportMovedInLoop(taker, name);
return false;
Expand All @@ -4205,7 +4257,7 @@ class OwnershipInferencePass
continue;
}

if (use != load.getOperation() && reachableAfter(taker, use, varOp))
if (use != load.getOperation() && reachableAfter(taker, use, kills))
{
reportUseAfterMove(use, taker, name);
return false;
Expand All @@ -4225,7 +4277,7 @@ class OwnershipInferencePass
llvm::SmallVector<mlir::Operation *> moved;
for (auto *use : slot.getUsers())
{
if (!mlir::isa<mlir_ts::ReleaseSlotOp>(use) || !reachableAfter(acquire, use, varOp))
if (!mlir::isa<mlir_ts::ReleaseSlotOp>(use) || !reachableAfter(acquire, use, kills))
{
continue;
}
Expand All @@ -4236,15 +4288,53 @@ class OwnershipInferencePass
return false;
}

// a release that one path reaches holding the moved value and another holding a new
// one an assignment after the move stored: there is no single answer for it
for (auto *assignment : assignments)
{
llvm::SmallVector<mlir::Operation *, 2> movedAgain{load.getOperation()};
if (varOp)
{
movedAgain.push_back(varOp);
}

if (reachableAfter(acquire, assignment, varOp) && reachableAfter(assignment, use, movedAgain))
{
reportMovedOnSomePaths(taker, use, name);
return false;
}
}

moved.push_back(use);
}

toErase.insert(moved.begin(), moved.end());
return true;
}

// The stores of the assignments to an owning local: rc releases what the slot holds right
// before it stores the new value (an assignment, and a `var` declaration met again).
static llvm::SmallVector<mlir::Operation *> slotAssignments(mlir::Value slot)
{
llvm::SmallVector<mlir::Operation *> assignments;
for (auto *user : slot.getUsers())
{
auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(user);
auto releaseOp = storeOp ? mlir::dyn_cast_or_null<mlir_ts::ReleaseSlotOp>(storeOp->getPrevNode())
: mlir_ts::ReleaseSlotOp();
if (releaseOp && storeOp.getReference() == slot && releaseOp.getSlot() == slot)
{
assignments.push_back(storeOp);
}
}

return assignments;
}

// A use of what `read` returned - directly or through casts - that the move at `taker` can
// reach, other than the move's own retain and taker. None if there is none.
// reach, other than the move's own retain and taker. None if there is none. A path back
// through `read` itself reaches a use of a value read again: the read, met after the move,
// is the slot's own use to check (around a loop, of a value an assignment stored).
mlir::Operation *readUsedAfter(mlir_ts::LoadOp read, mlir::Operation *retain, mlir::Operation *taker,
mlir::Operation *kill)
{
Expand All @@ -4265,7 +4355,13 @@ class OwnershipInferencePass
continue;
}

if (reachableAfter(taker, user, kill))
llvm::SmallVector<mlir::Operation *, 2> kills{read.getOperation()};
if (kill)
{
kills.push_back(kill);
}

if (reachableAfter(taker, user, kills))
{
return user;
}
Expand Down
9 changes: 9 additions & 0 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2764,6 +2764,7 @@ set_tests_properties(test-rc-interface-call-owned-result-marked
# under JIT and ahead of time; each owns every block once.
foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local own_print_number own_literals
own_move_const_branch own_move_let own_move_field own_move_return own_move_loop_body
own_move_reassign
own_literal_reuse own_move_control_flow
own_borrow_inner_let own_borrow_loop own_borrow_branch
own_borrow_two_lets own_borrow_nullable
Expand Down Expand Up @@ -2797,6 +2798,13 @@ foreach(global_mm rc none gc)
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")
endforeach()

# A `let` given a new value after its value moved owns the new one (#528); the same program under
# rc and gc (not none: nothing is freed there, and the loops would hold about 800 MB).
foreach(reassign_mm rc gc)
tslang_add_test(NAME test-jit-${reassign_mm}-own_move_reassign COMMAND test-runner -jit -mm=${reassign_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_move_reassign.ts")
tslang_add_test(NAME test-compile-${reassign_mm}-own_move_reassign COMMAND test-runner -mm=${reassign_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_move_reassign.ts")
endforeach()

# Strings as values (spec 22): no model but own makes a copy, and each program means the same
# under every model.
foreach(strings_mm rc none gc)
Expand Down Expand Up @@ -2924,6 +2932,7 @@ set(own_error_cases
"own_err_generator_map_outlives|borrows .* but is used here after .* is released or overwritten"
"own_err_generator_place_overwritten|borrows a field but is used here after it may be released or overwritten"
"own_err_two_owners|is used here after its value was moved"
"own_err_reassign_some_paths|is moved here on some paths only"
"own_err_push_let|is used here after its value was moved"
"own_err_push_loop|is moved inside a loop"
"own_err_field_loop|is moved inside a loop"
Expand Down
10 changes: 7 additions & 3 deletions tslang/test/tester/own/own_err_assign_after_move.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// -mm=own, phase 1 rejects: `a` is assigned after its value moved into `b`. rc's assignment
// releases what the slot holds before storing - the moved value, which `b` owns.
// -mm=own rejects: `a` is read after its value moved into `b`. It is given a new value on one path
// only, and on the other it still holds the moved one. (Given a new value on every path, it owns
// that one: own_move_reassign.)
class C { x: number = 5; }
function main() {
let a = new C();
let b = a;
a = new C();
if (b.x > 3) {
a = new C();
}

print(a.x, b.x);
print("done.");
}
4 changes: 3 additions & 1 deletion tslang/test/tester/own/own_err_borrow_bound_method.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// -mm=own, phase 2 rejects: `f` is `b.m` bound to what `b` borrows, so calling it after `a` is
// overwritten reads the old `a`. What a borrower's reads produce is the borrower too.
// overwritten reads the old `a`. What a borrower's reads produce is the borrower too. (`a` is read
// after `let b = a`, so `b` cannot take its value over and borrows it.)
class C {
v: number[] = [];
constructor(public x: number) {}
Expand All @@ -18,6 +19,7 @@ function churn() {
function main() {
let a = new C(1);
let b = a;
print(a.x);
const f = b.m;
a = new C(2);
churn();
Expand Down
4 changes: 3 additions & 1 deletion tslang/test/tester/own/own_err_borrow_catch_local.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// -mm=own, phase 2 rejects: a catch clause's locals own nothing, so `x` is one more name for what
// `b` borrows - and it is read after `a` is overwritten.
// `b` borrows - and it is read after `a` is overwritten. (`a` is read after `let b = a`, so `b`
// cannot take its value over and borrows it.)
class C {
v: number[] = [];
constructor(public x: number) {}
Expand All @@ -19,6 +20,7 @@ function main() {
let a = new C(1);
a.v.push(1);
let b = a;
print(a.x);
try {
throw 1;
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion tslang/test/tester/own/own_err_borrow_finally_local.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// -mm=own, phase 2 rejects: the same through a finally clause's local.
// -mm=own, phase 2 rejects: the same through a finally clause's local. (`a` is read after
// `let b = a`, so `b` cannot take its value over and borrows it.)
class C {
v: number[] = [];
constructor(public x: number) {}
Expand All @@ -18,6 +19,7 @@ function main() {
let a = new C(1);
a.v.push(1);
let b = a;
print(a.x);
try {
print(b.x);
} finally {
Expand Down
15 changes: 15 additions & 0 deletions tslang/test/tester/own/own_err_reassign_some_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// -mm=own rejects: `a` is given a new value after its value moved on one path only, so its release
// at scope exit would destroy the new value on that path and the moved one on the other.
class C { x: number = 5; }
class H { c: C | null = null; }
function main() {
const h = new H();
let a = new C();
h.c = a;
if (h.c.x > 3) {
a = new C();
print(a.x);
}

print("done.");
}
Loading
Loading