diff --git a/tslang/docs/superpowers/plans/2026-09-29-own-phase-2.md b/tslang/docs/superpowers/plans/2026-09-29-own-phase-2.md new file mode 100644 index 000000000..4e59a39ba --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-09-29-own-phase-2.md @@ -0,0 +1,767 @@ +# `-mm=own` Phase 2 Implementation Plan: borrows for locals + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development +> (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Under `-mm=own`, a `let` declared from a value it cannot take over *borrows* it +(spec §2.2, verdict 2). A borrow takes no reference and releases nothing. It is sound when no use +of the borrower can run after any release or assignment of the owner, and when the borrower never +escapes, meaning it is never stored, returned, retained, or assigned. These phase-1 errors become +legal programs: + +- `const a = new C(); { let b = a; ... } a.x`: a use of the owner after the "move"; +- `let b = a; let c = a;`: two receivers of one value; +- a `let` in a loop body declared from a value made outside the loop; +- a `let` declared on one branch only, which phase 1 rejected as "moved on some paths only". + +**Architecture:** Only `lib/TypeScript/OwnershipInferencePass.cpp` changes. The verdicts are tried +in the spec's order: move, then borrow, then error. The phase-1 move check runs *quietly*. If it +fails and the receiver can borrow (it is an owning `let` with its own `ts.RetainSlot`), `tryBorrow` +decides. If both fail, the error reported is the borrow's when the receiver could borrow, else the +move's. + +A borrow erases: +- the borrower's `ts.RetainSlot`; +- every `ts.ReleaseSlot` of the borrower's slot. + +The owner keeps all its releases. When a source has more than one receiver, every receiver must +be a borrow: a borrow pins its owner, so none of them may move it. + +**Tech Stack:** C++17, MLIR, CMake/ctest, `test/tester/test-runner.cpp`, +`test/tester/tools/measure.ps1`. + +**Spec:** `docs/superpowers/specs/2026-09-24-own-memory-model-design.md`. Read §2.2 (verdict 2), +§2.6, §4.2 step 3 (borrow), §5 (the borrow rows), and §12 (phase 1 as built, and §12.5, this +phase's input). + +## Global Constraints + +- Branch `own-phase-2` from `origin/main` (13b390e9, with #401 merged). +- `gc`, `rc` and `none` output must not change. Only `OwnershipInferencePass.cpp` changes, and it + runs only under `-mm=own`. +- Never branch on the memory model in MLIRGen (§4.3). +- `__tslang_inc_ref` and `__tslang_dec_ref` never appear in own output. +- Own tests run with `--no-default-lib`. Every positive test runs under JIT and AOT, and reads + through a `churn()` after the point where a wrong release would free. +- Build: `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --target tslang test-runner`. + Test: `ctest -j 16 -C Release` from that directory. Rebuild after editing `CMakeLists.txt` so + that ctest picks up the new regexes. +- Source files are CRLF. Edit them with the Edit tool or a Python byte-replace. Run `unix2dos` + on new files. On Windows, write multi-line Python to a file rather than a Bash heredoc. +- Commits are GPG-signed (never `--no-gpg-sign`), and each ends with the session's + `Co-Authored-By:` line. +- Debug rule: a Debug `tslang` idle for 30 s or more is probably on an assert dialog. Watch for + it. +- The IR the pass sees under `--opt`/JIT comes after CSE (spec §12.1). Check shapes with + `--emit=jit --mlir-print-ir-after-all --mlir-disable-threading`, not only with `--emit=mlir-affine`. + +## Review Focus + +1. **An owner assigned while a borrower is still used.** For + `let b = a; a = new C(); print(b.x)`, expect "'b' borrows 'a' but is used here after 'a' is + released or overwritten". It must never compile to a read of freed memory. Task 1 test: + `own_err_assign_after_move` (text changes). +2. **A borrower that escapes.** For `let b = a; h.c = b;` or `return b` or `arr.push(b)`, expect + "cannot be stored, returned or captured". Treating it as a borrow would give the field a value + whose owner frees it. Task 1 test: `own_err_borrow_escape`. +3. **A borrower re-declared in a loop whose owner is assigned in the same body.** For + `for (...) { let b = a; use(b); a = new C(); }`, each iteration's `b` borrows the new `a`. That + is sound, because the path back to `b`'s uses passes `b`'s declaration. Task 1 test: + `own_borrow_loop`. +4. **Mixed receivers.** For `let b = a; h.c = a;`, the second receiver has to move, and the first + pins `a`. Expect an error, not a borrow plus a move. Task 2 test: `own_err_borrow_and_move`. +5. **A borrow of a temporary whose release was consumed elsewhere.** With no temporary release, + the owner is another taker, so there is nothing to bound the borrow. Expect an error. Task 1 + keeps `own_err_two_owners` and `own_err_push_let` negative. + +--- + +## File structure + +- Modify: `lib/TypeScript/OwnershipInferencePass.cpp`. Add: + - the quiet mode (`quiet`, `quietly`); + - `borrowerOf`, `slotEnds`, `tryBorrow`; + - `decideSlot` and `struct SlotReceiver`; + - three borrow diagnostics. + + Also change the SSA path in `checkValueMoves` and the retain loop in `runOnFunction`. +- Delete from `test/tester/own/` the negatives that phase 2 accepts: `own_err_use_after_move.ts`, + `own_err_const_twice.ts`, `own_err_let_twice.ts`, `own_err_let_loop.ts`, `own_err_alias.ts`, + `own_err_move_some_paths.ts`. +- Create in `test/tester/own/`: + - positives: `own_borrow_inner_let.ts`, `own_borrow_two_lets.ts`, `own_borrow_loop.ts`, + `own_borrow_branch.ts`; + - negatives: `own_err_borrow_escape.ts`, `own_err_move_some_paths_field.ts`, + `own_err_borrow_and_move.ts`. +- Modify: `test/tester/CMakeLists.txt`, spec (new §13). + +--- + +### Task 1: borrow for a single `let` receiver + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp`, `test/tester/own/*`, `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes (phase 1): `checkSlotMove`, `reachableAfter`, `isBorrow`, `isOwningVariable`, + `varName`, `describe`, `slotLoadOf`, `takerAcquires`, and the `report*` functions. +- Produces, used by Task 2: + - `template bool quietly(F check)` + - `static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition)` + - `static llvm::SmallVector slotEnds(mlir::Value slot)` + - `bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef ownerName, llvm::SetVector &toErase)` + - `struct SlotReceiver { mlir::Operation *retain; mlir::Value value; mlir_ts::LoadOp load; };` + +- [ ] **Step 1: Failing positive tests.** Each defines the same `C`/`churn` as the phase-1 tests: + +```ts +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} +``` + +`own_borrow_inner_let.ts`, which a `const` and a `let` owner both take part in: + +```ts +// -mm=own, phase 2: a `let` in an inner scope borrows a value that is used again after that +// scope. The `let` takes no reference and releases nothing; the owner's release destroys it once. + + +function fromConst(i: number) { + const a = new C(i); + a.v.push(i); + { + let b = a; + b.x = b.x + 1; + } + + churn(); + return a.x + a.v.length; +} + +function fromLet(i: number) { + let a = new C(i); + a.v.push(i); + { + let b = a; + b.v.push(i); + } + + churn(); + return a.x + a.v.length; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += fromConst(i % 10) + fromLet(i % 10); + } + + assert(t == 1300000); + print("done."); +} +``` + +(`fromConst` gives `x + 1 + 1`: 65 per ten. `fromLet` gives `x + 2`: 65 per ten. That is 130 per +ten, 1300000 in all.) + +`own_borrow_loop.ts`: + +```ts +// -mm=own, phase 2: a `let` inside a loop borrows a value made before the loop (spec 2.6), and +// a `let` that borrows an owner reassigned later in the same body borrows the new value on the +// next iteration. + + +function fromOutside(n: number) { + const a = new C(n); + a.v.push(n); + let t: number = 0; + for (let i = 0; i < 3; i++) { + let b = a; + t += b.x + b.v.length; + } + + churn(); + return t + a.x; +} + +function reassigned(n: number) { + let a = new C(n); + let t: number = 0; + for (let i = 0; i < 3; i++) { + let b = a; + t += b.x; + a = new C(b.x + 1); + } + + churn(); + return t + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 10000; i++) { + t += fromOutside(i % 10) + reassigned(i % 10); + } + + assert(t == 450000); + print("done."); +} +``` + +(`fromOutside(n)` = `3(n+1) + n` = `4n + 3`. `reassigned(n)` = `n + (n+1) + (n+2) + (n+3)` = +`4n + 6`. Their sum is `8n + 9`, which over n = 0..9 is 450 per ten, so 450000 for 10000.) + +`own_borrow_branch.ts`: + +```ts +// -mm=own, phase 2: a `let` declared from a value on one branch only borrows it; phase 1 had to +// reject it as "moved on some paths only". + + +function pick(i: number) { + const a = new C(i); + a.v.push(i); + let t: number = 0; + if (i % 2 == 0) { + let b = a; + t = b.x + b.v.length; + } + + churn(); + return t + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += pick(i % 10); + } + + assert(t == 700000); + print("done."); +} +``` + +(Even `i` gives `(x + 1) + x`, and 0, 2, 4, 6, 8 sum to `2*20 + 5` = 45. Odd `i` gives `x`: 25. +That is 70 per ten, 700000 in all.) + +Before committing, compute every expected total by running the file under +`-mm=gc --emit=jit`. The arithmetic above is a check, not the source of truth. + +Register them by adding to the own positive `foreach`: +`own_borrow_inner_let own_borrow_loop own_borrow_branch`. + +- [ ] **Step 2: Negative tests, and the negatives phase 2 accepts** + +Delete `own_err_use_after_move.ts`, `own_err_const_twice.ts`, `own_err_alias.ts`, +`own_err_let_loop.ts` and `own_err_move_some_paths.ts`, along with their `own_error_cases` +entries. Their shapes are now covered by the positives above. `own_err_let_twice.ts` goes in +Task 2. + +`own_err_borrow_escape.ts`: + +```ts +// -mm=own, phase 2 rejects: `b` borrows `a` (which is used again), and is then stored into a +// field - the field would hold a value `a` goes on owning and will free. +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + let a = new C(); + let b = a; + h.c = b; + print(a.x); + print("done."); +} +``` + +`own_err_move_some_paths_field.ts`, to keep the "some paths" error covered: + +```ts +// -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 | null = null; } +function main(argc: number) { + const h = new H(); + const a = new C(); + if (argc > 5) { + h.c = a; + } + print("done."); +} +``` + +`own_error_cases` changes: + +```cmake + "own_err_borrow_escape|borrows .* and cannot be stored, returned or captured" + "own_err_move_some_paths_field|is moved here on some paths only" + "own_err_assign_after_move|borrows .* but is used here after .* is released or overwritten" + "own_err_const_alias_move|borrows .* and cannot be assigned" +``` + +`own_err_const_alias_move` is `const b = a; let c = a; c = new C(0);`. `c` cannot move, +because `b` is read after it, so it tries to borrow, and it is assigned. + +- [ ] **Step 3: Run to see them fail** + +Build, then `ctest -C Release -R "own_borrow|own_err_borrow|some_paths_field|assign_after_move|const_alias_move" --output-on-failure`. + +Expected: +- The positives fail with "is used here after its value was moved", or with "moved inside a + loop" / "on some paths only". +- `own_err_borrow_escape` fails on its message: it reports a use-after-move instead. +- `own_err_move_some_paths_field` passes already. That is fine: it is coverage, not RED. + +- [ ] **Step 4: Quiet mode and the borrow verdict** + +Add a member `unsigned quiet = 0;` and a helper: + +```cpp + // Runs a verdict without reporting: the next verdict gets its turn before anything is said. + template bool quietly(F check) + { + ++quiet; + auto result = check(); + --quiet; + return result; + } +``` + +Begin each of `reportSecondReference`, `reportUseAfterMove`, `reportMovedInLoop` and +`reportMovedOnSomePaths` with `if (quiet) return;`. + +Add the borrow diagnostics: + +```cpp + void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) return; + auto diag = use->emitError("'") << name << "' borrows '" << owner + << "' but is used here after '" << owner << "' is released or overwritten"; + diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; + signalPassFailure(); + } + + void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) return; + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + signalPassFailure(); + } + + void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) return; + op->emitError("'") << name << "' borrows '" << owner + << "' and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } +``` + +Add the borrow machinery: + +```cpp + // A `let` that could borrow: owning, declared from a value, holding a reference it took + // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || + varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + // Where an owning local stops holding its value: each release of its slot and each store + // into it. + static llvm::SmallVector slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its + // retain and every release of its slot go, and the owner keeps its own. Sound while no use of + // the borrower can run after any of `ends` - the owner's releases and assignments - and while + // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use + // through the borrower's own declaration is a new borrow, not this one. Reports and returns + // false otherwise. + bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto slot = borrower.getResult(); + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + if (mlir::isa(user)) + { + reportBorrowerAssigned(user, name, owner); + return false; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + reportBorrowEscapes(user, name, owner); + return false; + } + + uses.push_back(user); + llvm::SmallVector values{readOp.getResult()}; + while (!values.empty()) + { + auto current = values.pop_back_val(); + for (auto *valueUser : current.getUsers()) + { + uses.push_back(valueUser); + if (auto castOp = mlir::dyn_cast(valueUser)) + { + values.push_back(castOp.getResult()); + continue; + } + + if (mlir::isa(valueUser) || !isBorrow(valueUser, current)) + { + reportBorrowEscapes(valueUser, name, owner); + return false; + } + } + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (reachableAfter(end, use, borrower)) + { + reportBorrowOutlives(use, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } +``` + +- [ ] **Step 5: Wire the verdict order for a single receiver** + +SSA sources: in `checkValueMoves`, rename the current body after the early `return true` to a new +function `bool movesOf(mlir::Value value, llvm::SetVector &toErase)`. This is +the phase-1 move verdict, and it reports. `checkValueMoves` then becomes: + +```cpp + bool checkValueMoves(mlir::Value value, llvm::SetVector &toErase) + { + if (!ownsHeap(value) || isImmortalLiteral(value)) + { + return true; + } + + if (quietly([&] { return movesOf(value, toErase); })) + { + return true; + } + + // Not a move. Every taker a `let` that can borrow, and the temporary's own release the + // owner that bounds them - else the move's error stands. + llvm::SmallVector takers; + llvm::SmallVector releases; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (!isBorrow(user, value)) + { + takers.push_back(user); + } + } + + if (!releases.empty() && !takers.empty() && + llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) + { + auto owner = describe(value); + auto ok = true; + for (auto *taker : takers) + { + ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; + } + + return ok; + } + + return movesOf(value, toErase); + } +``` + +`describe(value)` returns the name of the first `ts.Variable` user, which is a borrower. For a +folded `const` that is the borrower's own name. **Ruling to make and ledger:** give +`describe` a parameter, or add an `ownerName(value)` that looks for a `ts.DebugVariable` user +(`--di` names a folded const through `ts.DebugVariable`), falling back to "this value". + +Slot sources: in `runOnFunction`, collect instead of deciding per retain: + +```cpp + llvm::MapVector> slotReceivers; + for (auto *op : retains) + { + auto value = retainedValue(op); + if (value && proven.contains(value)) + { + toErase.insert(op); + continue; + } + + if (auto load = value ? slotLoadOf(value) : mlir_ts::LoadOp()) + { + slotReceivers[load.getReference()].push_back({op, value, load}); + continue; + } + + if (!value || isFresh(value) == false) + { + reportSecondReference(op); + } + } + + for (auto &entry : slotReceivers) + { + decideSlot(entry.first, entry.second, toErase); + } +``` + +For a single receiver: + +```cpp + struct SlotReceiver + { + mlir::Operation *retain; + mlir::Value value; + mlir_ts::LoadOp load; + }; + + // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. + void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto owner = varName(slot.getDefiningOp()); + if (receivers.size() == 1) + { + auto &receiver = receivers.front(); + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); })) + { + toErase.insert(receiver.retain); + return; + } + + if (auto borrower = borrowerOf(receiver.retain)) + { + tryBorrow(borrower, slotEnds(slot), owner, toErase); + return; + } + + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); // reports + return; + } + + decideSlotReceivers(slot, receivers, owner, toErase); // Task 2 + } +``` + +For Task 1, make `decideSlotReceivers` run the single-receiver path on each receiver. That is the +phase-1 behavior, and Task 2 replaces it. + +- [ ] **Step 6: Build, run every own test, fix expected texts** + +Run `ctest -C Release -j 12 -R own --output-on-failure`. + +Expected: everything passes. A negative that changed text is fine only if its new error points +at a real problem. Check each one, update `own_error_cases`, and write a ruling for each change. + +- [ ] **Step 7: Commit** ("-mm=own phase 2: a let that cannot move borrows"). + +--- + +### Task 2: several receivers of one owner + +When a source has more than one receiver, a borrow pins the owner, so none of the receivers may +move it. Every receiver must be a `let` that borrows. Anything else reports: + +- for a receiver that is not a `let`, its phase-1 move error (another receiver's reads come after + it); +- or "takes a second reference", when that move check would pass. + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp`, `test/tester/own/*`, `test/tester/CMakeLists.txt` + +- [ ] **Step 1: Failing tests** + +`own_borrow_two_lets.ts`: + +```ts +// -mm=own, phase 2: two `let`s of one `let`, both borrowing it; neither may move it. + + +function twice(i: number) { + let a = new C(i); + a.v.push(i); + let b = a; + let c = a; + churn(); + return b.x + c.v.length + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += twice(i % 10); + } + + assert(t == 1000000); + print("done."); +} +``` + +(`2x + 1`: 100 per ten. Confirm the total under gc before committing.) + +`own_err_borrow_and_move.ts`: + +```ts +// -mm=own, phase 2 rejects: `b` borrows `a`, which pins it, so the field store cannot move it. +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + let a = new C(); + let b = a; + h.c = a; + print(b.x); + print("done."); +} +``` + +Delete `own_err_let_twice.ts` and its entry. Add: + +```cmake + "own_err_borrow_and_move|is used here after its value was moved|takes a second reference" +``` + +- [ ] **Step 2: Run to see them fail** + +Expected: `own_borrow_two_lets` fails with "is used here after its value was moved". + +- [ ] **Step 3: Implement** + +```cpp + // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, + // so none may move it. + void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + tryBorrow(borrower, ends, owner, toErase); + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportSecondReference(receiver.retain); + } + } + } +``` + +In `checkValueMoves`, the SSA path already requires every taker to borrow once the move fails. No +change is needed there. + +- [ ] **Step 4: Build, run own tests, commit** ("-mm=own phase 2: several lets of one owner all + borrow it"). + +--- + +### Task 3: teeth, measurement, suites, record + +- [ ] **Step 1: Teeth.** In `tryBorrow`, keep the borrower's `ts.ReleaseSlot`s by inserting only + the `ts.RetainSlot`s from `bookkeeping`. Rebuild and run `ctest -R "own-own_borrow"`. + + Expected: every borrow positive fails, because the borrower's release destroys what the owner + still holds. Strengthen any test that passes. Restore, and write the result in the ledger. +- [ ] **Step 2: Measure.** Run `measure.ps1` on `own_borrow_inner_let.ts` and + `own_borrow_loop.ts` with `-Models gc,rc,none,own`, at O3 and at O1 (a scratch copy with + `--opt_level=1`). + + Expected: own is flat like rc. +- [ ] **Step 3: Full Release suite.** Every test passes. +- [ ] **Step 4: Debug own subset.** + + ``` + ctest -C Debug -j 12 -R own -E owned-async + ``` + + Expected: only `test-ownership-verifier-4` fails, which is the known linkname assert. +- [ ] **Step 5: Spec §13 "Phase 2 results".** Cover: + - the borrow rule as built; + - the receivers rule; + - the numbers; + - the teeth result; + - the tests that changed; + - the limits left for phase 3: element and field borrows, borrow chains, borrowers that are + assigned, borrows of parameters, moves after borrows. + + Update the status line. +- [ ] **Step 6: Commit** ("-mm=own phase 2: results"). diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index c4c873f29..2006ba8d8 100644 --- a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md +++ b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md @@ -2,7 +2,8 @@ Date: 2026-09-24. Status: design approved in conversation; written review 2026-09-28 (§10), amendments folded in. Phase 0 merged as #399 (results §11); phase 1 -(moves by reachability) on branch `own-phase-1` (results §12). Plans in `docs/superpowers/plans/`. +(moves by reachability) merged as #401 (results §12); phase 2 (borrows for locals) on branch +`own-phase-2` (results §13). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -569,3 +570,88 @@ after the move. The loop cases report `is moved inside a loop`. - Everything in §11.4 not listed above as accepted: owned locals in a `try` body, closures, interface and `any` boxing, `delete`, callee-owned parameters, runtime helpers and `declare`d functions, the eight-element field literal. + +## 13. Phase 2 results, 2026-09-29 + +Plan: `docs/superpowers/plans/2026-09-29-own-phase-2.md`. Only `OwnershipInferencePass.cpp` +changed. + +### 13.1 What phase 2 accepts + +The verdicts run in §2.2's order. Phase 1's move check runs first, quietly. When it fails, a +receiver that is a `let` borrows (verdict 2). That means an owning `ts.Variable`, declared from +the value, that took its own reference (a `ts.RetainSlot`, not `__owned_consumed`) and is not +captured. Its retain and every `ts.ReleaseSlot` of its slot are erased, and the owner keeps all +of its releases. + +A borrow is sound while both of these hold: + +- **No use of the borrower can run after any end of the owner.** The ends are a local owner's + `ts.ReleaseSlot`s and assignments (`ts.Store` into its slot), or a temporary owner's + `ts.Release`s. The uses are every read of the borrower's slot and every use of anything derived + from a read that can still point into the block: casts that keep the block, field and element + references and values loaded through them, bound methods, and a catch or finally clause's + non-owning local. The walk stops at a number or a boolean. The final review found the first + version, which followed only casts, compiling `const f = b.m; a = new C(); f()`, a `for...of` + over `b.v`, and a catch local of `b` into reads of freed memory. A path back to a use through + the borrower's own declaration is a new borrow: `for (...) { let b = a; ...; a = new C(); }` is + fine. +- **Nothing the borrower holds is kept.** It must not be retained, taken by a non-borrowing use + (stored, pushed, returned, declared into another owning `let`), or assigned. + +Where an owner has **more than one receiver**, every one of them has to borrow: a borrow pins +its owner. A field store beside a borrowing `let` is `'a' is moved here while 'b' borrows it`. + +The errors follow §5. Under `--di` they name both variables, and a folded `const` owner is named +through its `ts.DebugVariable`: + +- `'b' borrows 'a' but is used here after 'a' is released or overwritten`, with a note at the end; +- `'b' borrows 'a' and cannot be stored, returned or captured`; +- `'b' borrows 'a' and cannot be assigned`. + +### 13.2 Measured + +1M iterations (100K calls), AOT, `measure.ps1`, at `-O3` and `-O1` (identical within 1.5%): + +| program | gc | rc | none | own | +| --- | --- | --- | --- | --- | +| `own_borrow_inner_let` | 6.6 | 4.8 | 1088.3 | 4.8 | +| `own_borrow_loop` | 6.4 | 4.7 | 113.4 | 4.7 | +| `own_borrow_two_lets` | 6.6 | 4.8 | 546.8 | 4.8 | + +### 13.3 Teeth + +With each borrower's releases kept (only its retain erased), 10 of 10 borrow checks fail +(`own_borrow_nullable` included). The borrower's release destroys what the owner still holds. + +### 13.4 Changed tests + +These phase-1 negatives are legal now, and positives replace them: + +- `own_err_use_after_move`, `own_err_const_twice` → `own_borrow_inner_let`; +- `own_err_alias`, `own_err_let_twice` → `own_borrow_two_lets`; +- `own_err_let_loop` → `own_borrow_loop`; +- `own_err_move_some_paths` → `own_borrow_branch`. + +Other negatives: + +- `own_err_move_some_paths_field` keeps "some paths" covered (a field store cannot borrow). +- `own_err_assign_after_move` and `own_err_const_alias_move` now report the borrow's error. +- New: `own_err_borrow_escape`, `own_err_borrow_and_move`, and from the final review + `own_err_borrow_bound_method`, `own_err_borrow_catch_local`, `own_err_borrow_finally_local`, + `own_err_borrow_derived_ref`, with `own_borrow_nullable` (`if (b)` on a nullable borrower was + a false escape). + +### 13.5 Known limits (the input to phase 3) + +- Reads out of containers as borrows (§2.3): `const e = arr[i]`, `const c = h.c`, bounded by the + container's tenure and its dropping mutations (`pop`, `shift`, `splice`, `length =`, an + overwrite). +- Borrow chains (`let c = b` where `b` borrows) and borrowers that are assigned. +- Borrows of parameters and of `this`, which need callee facts (phase 4). +- A move after the last use of every borrower. It is rejected today, since any borrower pins + its owner. +- A temporary taken by a `let` and also consumed elsewhere (`let b = h.c = new C()`): with no + temporary release there is nothing to bound a borrow, so it is still an error. +- From §12.5: moves on some paths only, and a `const` alias of a `let` held across an assignment + of the `let` (rc has the same bug). diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index aa38225e9..942908237 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -9,6 +9,7 @@ #include "TypeScript/Defines.h" #include "TypeScript/MLIRLogic/MLIRTypeHelper.h" +#include "llvm/ADT/MapVector.h" #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" @@ -108,6 +109,9 @@ class OwnershipInferencePass : public mlir::PassWrapper> slotReceivers; for (auto *op : retains) { auto value = retainedValue(op); @@ -119,11 +123,7 @@ class OwnershipInferencePass : public mlir::PassWrappererase(); @@ -142,6 +147,243 @@ class OwnershipInferencePass : public mlir::PassWrapper bool quietly(F check) + { + ++quiet; + auto result = check(); + --quiet; + return result; + } + + // Move, else borrow, else error (spec 2.2) for the receivers of one owning local. + void decideSlot(mlir::Value slot, llvm::ArrayRef receivers, llvm::SetVector &toErase) + { + auto owner = varName(slot.getDefiningOp()); + if (receivers.size() == 1) + { + auto &receiver = receivers.front(); + if (quietly([&] { return checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); })) + { + toErase.insert(receiver.retain); + return; + } + + if (auto borrower = borrowerOf(receiver.retain)) + { + tryBorrow(borrower, slotEnds(slot), owner, toErase); + return; + } + + checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase); // reports + return; + } + + decideSlotReceivers(slot, receivers, owner, toErase); + } + + // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so + // none may move it. A receiver that is not a `let` reports its move error - another + // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. + void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + mlir_ts::VariableOp someBorrower; + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + someBorrower = borrower; + tryBorrow(borrower, ends, owner, toErase); + } + } + + for (auto &receiver : receivers) + { + if (borrowerOf(receiver.retain)) + { + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + } + } + } + + // A `let` that could borrow: owning, declared from a value, holding a reference it took + // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || + varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + // Where an owning local stops holding its value: each release of its slot and each store + // into it. + static llvm::SmallVector slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its + // retain and every release of its slot go, and the owner keeps its own. Sound while no use of + // the borrower can run after any of `ends` - the owner's releases and assignments - and while + // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use + // through the borrower's own declaration is a new borrow, not this one. Reports and returns + // false otherwise. + bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto slot = borrower.getResult(); + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + if (mlir::isa(user)) + { + reportBorrowerAssigned(user, name, owner); + return false; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + reportBorrowEscapes(user, name, owner); + return false; + } + + uses.push_back(user); + + // The borrowed value itself - the read and the casts that keep it the same block - must + // not be kept anywhere. What it produces - a field reference, a value loaded through + // one, a bound method, a catch clause's non-owning local - still points into the + // borrowed block, so each of its uses is a use of the borrower too. A value that + // cannot point anywhere (a number, a boolean) ends the walk. + llvm::SmallVector> values{{readOp.getResult(), true}}; + llvm::SmallPtrSet seen; + while (!values.empty()) + { + auto [current, borrowed] = values.pop_back_val(); + for (auto *valueUser : current.getUsers()) + { + uses.push_back(valueUser); + if (borrowed) + { + if (auto castOp = mlir::dyn_cast(valueUser)) + { + if (mayPointInto(castOp.getResult()) && seen.insert(castOp.getResult()).second) + { + values.push_back({castOp.getResult(), true}); + } + + continue; + } + + if (mlir::isa(valueUser) || !isBorrow(valueUser, current)) + { + reportBorrowEscapes(valueUser, name, owner); + return false; + } + } + + for (auto result : valueUser->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) + { + values.push_back({result, false}); + } + } + } + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (reachableAfter(end, use, borrower)) + { + reportBorrowOutlives(use, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + // Can a value of this type point into a heap block - a reference into one, a block of its own, + // or a method bound to one? A number or a boolean read out of a borrowed block cannot. + bool mayPointInto(mlir::Value value) + { + auto type = value.getType(); + return mlir::isa(type) || + ownsHeap(value); + } + + // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it + // has a ts.DebugVariable, whose location carries the name. + static llvm::StringRef ownerName(mlir::Value value) + { + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + return nameAt(user->getLoc()); + } + } + + return "this value"; + } + // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer. // None for a variable with no initializer - its storage was hoisted in front of a try and its // value arrives by a store this phase does not follow. @@ -295,6 +537,51 @@ class OwnershipInferencePass : public mlir::PassWrapper takers; + llvm::SmallVector releases; + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user)) + { + releases.push_back(user); + } + else if (!isBorrow(user, value)) + { + takers.push_back(user); + } + } + + if (!releases.empty() && !takers.empty() && + llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) + { + auto owner = ownerName(value); + auto ok = true; + for (auto *taker : takers) + { + ok = tryBorrow(borrowerOf(taker), releases, owner, toErase) && ok; + } + + return ok; + } + + return movesOf(value, toErase); + } + + // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. + bool movesOf(mlir::Value value, llvm::SetVector &toErase) + { llvm::SmallVector takers; llvm::SmallVector releases; llvm::SmallVector reads; @@ -698,7 +985,11 @@ class OwnershipInferencePass : public mlir::PassWrapper>(location)) { if (auto name = fused.getMetadata().getName()) @@ -733,6 +1024,11 @@ class OwnershipInferencePass : public mlir::PassWrapper(op)) { @@ -752,6 +1048,11 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' is used here after its value was moved"; diag.attachNote(move->getLoc()) << "value moved here"; signalPassFailure(); @@ -759,14 +1060,71 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' is moved inside a loop but was made outside it; only a borrow could do " "that, and -mm=own cannot prove one yet"; signalPassFailure(); } + void reportBorrowOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows '" << owner << "' but is used here after '" << owner + << "' is released or overwritten"; + diag.attachNote(end->getLoc()) << "'" << owner << "' is released or overwritten here"; + signalPassFailure(); + } + + void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) + { + if (quiet) + { + return; + } + + op->emitError("'") << owner << "' is moved here while '" << borrower + << "' borrows it; a borrowed value cannot be moved"; + signalPassFailure(); + } + + void reportBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) + { + return; + } + + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + signalPassFailure(); + } + + void reportBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef owner) + { + if (quiet) + { + return; + } + + op->emitError("'") << name << "' borrows '" << owner << "' and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name) { + if (quiet) + { + return; + } + auto diag = move->emitError("'") << name << "' is moved here on some paths only; -mm=own cannot release it " "on the others yet"; diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index fb1c35991..a3e603ca3 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2402,7 +2402,9 @@ set_tests_properties(test-own-inference-erases-birth-takes # 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_literal_reuse own_move_control_flow) + own_literal_reuse own_move_control_flow + own_borrow_inner_let own_borrow_loop own_borrow_branch + own_borrow_two_lets own_borrow_nullable) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -2424,7 +2426,7 @@ set_tests_properties(test-own-verify-ownership PROPERTIES FAIL_REGULAR_EXPRESSIO # scope-fused location, and the error reads it. add_test(NAME test-own-err-names-the-variable COMMAND $ --emit=obj --no-default-lib -mm=own --di - "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_let_twice.ts" + "${PROJECT_SOURCE_DIR}/test/tester/own/own_err_const_alias_field.ts" -o "${CMAKE_CURRENT_BINARY_DIR}/own-err-names-the-variable.obj") set_tests_properties(test-own-err-names-the-variable PROPERTIES PASS_REGULAR_EXPRESSION "'a' is used here after its value was moved" @@ -2432,22 +2434,23 @@ set_tests_properties(test-own-err-names-the-variable # Each negative program has one shape phase 0 cannot prove, and must be rejected with this text. set(own_error_cases - "own_err_alias|is used here after its value was moved" - "own_err_assign_after_move|is used here after its value was moved" - "own_err_let_twice|is used here after its value was moved" - "own_err_let_loop|is moved inside a loop" - "own_err_const_alias_move|is used here after its value was moved" + "own_err_assign_after_move|borrows .* but is used here after .* is released or overwritten" + "own_err_borrow_escape|borrows .* and cannot be stored, returned or captured" + "own_err_move_some_paths_field|is moved here on some paths only" + "own_err_borrow_and_move|is moved here while .* borrows it" + "own_err_borrow_bound_method|borrows .* but is used here after .* is released or overwritten" + "own_err_borrow_catch_local|borrows .* but is used here after .* is released or overwritten" + "own_err_borrow_finally_local|borrows .* but is used here after .* is released or overwritten" + "own_err_borrow_derived_ref|borrows .* but is used here after .* is released or overwritten" + "own_err_const_alias_move|borrows .* and cannot be assigned" "own_err_const_alias_field|is used here after its value was moved" "own_err_field_alias|is used here after its value was moved" "own_err_closure|closures that capture a variable are not supported" "own_err_unknown_producer|takes a second reference" "own_err_two_owners|is used here after its value was moved" - "own_err_const_twice|is used here after its value was moved" "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" - "own_err_move_some_paths|is moved here on some paths only" - "own_err_use_after_move|is used here after its value was moved" "own_err_interface|takes a second reference" "own_err_delete|'delete' is not supported by -mm=own yet") foreach(own_error_case ${own_error_cases}) diff --git a/tslang/test/tester/own/own_borrow_branch.ts b/tslang/test/tester/own/own_borrow_branch.ts new file mode 100644 index 000000000..5767de80d --- /dev/null +++ b/tslang/test/tester/own/own_borrow_branch.ts @@ -0,0 +1,38 @@ +// -mm=own, phase 2: a `let` declared from a value on one branch only borrows it; phase 1 had to +// reject it as "moved on some paths only". +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function pick(i: number) { + const a = new C(i); + a.v.push(i); + let t: number = 0; + if (i % 2 == 0) { + let b = a; + t = b.x + b.v.length; + } + + churn(); + return t + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += pick(i % 10); + } + + assert(t == 700000); + print("done."); +} diff --git a/tslang/test/tester/own/own_borrow_inner_let.ts b/tslang/test/tester/own/own_borrow_inner_let.ts new file mode 100644 index 000000000..5d1fe82e5 --- /dev/null +++ b/tslang/test/tester/own/own_borrow_inner_let.ts @@ -0,0 +1,49 @@ +// -mm=own, phase 2: a `let` in an inner scope borrows a value that is used again after that +// scope. The `let` takes no reference and releases nothing; the owner's release destroys it once. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function fromConst(i: number) { + const a = new C(i); + a.v.push(i); + { + let b = a; + b.x = b.x + 1; + } + + churn(); + return a.x + a.v.length; +} + +function fromLet(i: number) { + let a = new C(i); + a.v.push(i); + { + let b = a; + b.v.push(i); + } + + churn(); + return a.x + a.v.length; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += fromConst(i % 10) + fromLet(i % 10); + } + + assert(t == 1300000); + print("done."); +} diff --git a/tslang/test/tester/own/own_borrow_loop.ts b/tslang/test/tester/own/own_borrow_loop.ts new file mode 100644 index 000000000..b21d28fbe --- /dev/null +++ b/tslang/test/tester/own/own_borrow_loop.ts @@ -0,0 +1,52 @@ +// -mm=own, phase 2: a `let` inside a loop borrows a value made before the loop (spec 2.6), and +// a `let` that borrows an owner reassigned later in the same body borrows the new value on the +// next iteration. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function fromOutside(n: number) { + const a = new C(n); + a.v.push(n); + let t: number = 0; + for (let i = 0; i < 3; i++) { + let b = a; + t += b.x + b.v.length; + } + + churn(); + return t + a.x; +} + +function reassigned(n: number) { + let a = new C(n); + let t: number = 0; + for (let i = 0; i < 3; i++) { + let b = a; + t += b.x; + a = new C(b.x + 1); + } + + churn(); + return t + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 10000; i++) { + t += fromOutside(i % 10) + reassigned(i % 10); + } + + assert(t == 450000); + print("done."); +} diff --git a/tslang/test/tester/own/own_borrow_nullable.ts b/tslang/test/tester/own/own_borrow_nullable.ts new file mode 100644 index 000000000..4e3e121b6 --- /dev/null +++ b/tslang/test/tester/own/own_borrow_nullable.ts @@ -0,0 +1,36 @@ +// -mm=own, phase 2: a nullable `let` borrowing a class value, tested for null. The test reads a +// boolean out of the borrower, which keeps nothing. +class C { + v: number[] = []; + constructor(public x: number) {} + m() { return this.x; } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + let a = new C(i % 10); + a.v.push(3); + { + let b: C | null = a; + if (b) { + t += b.x + b.v.length; + } + } + + churn(); + t += a.x; + } + + assert(t == 1000000); + print("done."); +} diff --git a/tslang/test/tester/own/own_borrow_two_lets.ts b/tslang/test/tester/own/own_borrow_two_lets.ts new file mode 100644 index 000000000..9e9636a1d --- /dev/null +++ b/tslang/test/tester/own/own_borrow_two_lets.ts @@ -0,0 +1,33 @@ +// -mm=own, phase 2: two `let`s of one `let`, both borrowing it; neither may move it. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function twice(i: number) { + let a = new C(i); + a.v.push(i); + let b = a; + let c = a; + churn(); + return b.x + c.v.length + a.x; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += twice(i % 10); + } + + assert(t == 1000000); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_alias.ts b/tslang/test/tester/own/own_err_alias.ts deleted file mode 100644 index 80f2b2fa5..000000000 --- a/tslang/test/tester/own/own_err_alias.ts +++ /dev/null @@ -1,7 +0,0 @@ -// -mm=own, phase 1 rejects: `a`'s array moves into `b`, and `a` is read after that. -function main() { - let a: number[] = [1]; - let b = a; - print(b.length, a.length); - print("done."); -} diff --git a/tslang/test/tester/own/own_err_borrow_and_move.ts b/tslang/test/tester/own/own_err_borrow_and_move.ts new file mode 100644 index 000000000..77e39a356 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_and_move.ts @@ -0,0 +1,11 @@ +// -mm=own, phase 2 rejects: `b` borrows `a`, which pins it, so the field store cannot move it. +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + let a = new C(); + let b = a; + h.c = a; + print(b.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_borrow_bound_method.ts b/tslang/test/tester/own/own_err_borrow_bound_method.ts new file mode 100644 index 000000000..4511a2359 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_bound_method.ts @@ -0,0 +1,25 @@ +// -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. +class C { + v: number[] = []; + constructor(public x: number) {} + m() { return this.x; } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function main() { + let a = new C(1); + let b = a; + const f = b.m; + a = new C(2); + churn(); + print(f()); +} diff --git a/tslang/test/tester/own/own_err_borrow_catch_local.ts b/tslang/test/tester/own/own_err_borrow_catch_local.ts new file mode 100644 index 000000000..86969b530 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_catch_local.ts @@ -0,0 +1,30 @@ +// -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. +class C { + v: number[] = []; + constructor(public x: number) {} + m() { return this.x; } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function main() { + let a = new C(1); + a.v.push(1); + let b = a; + try { + throw 1; + } catch (e) { + let x = b; + a = new C(2); + churn(); + print(x.x, x.v.length); + } +} diff --git a/tslang/test/tester/own/own_err_borrow_derived_ref.ts b/tslang/test/tester/own/own_err_borrow_derived_ref.ts new file mode 100644 index 000000000..1921f6f28 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_derived_ref.ts @@ -0,0 +1,32 @@ +// -mm=own, phase 2 rejects: the loop iterates `b.v`, a field of what `b` borrows, through one +// reference into it that every iteration reads - after the body has overwritten `a`. +class C { + v: number[] = []; + constructor(public x: number) {} + m() { return this.x; } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function main() { + let a = new C(1); + a.v.push(1); + a.v.push(2); + a.v.push(3); + let b = a; + let t: number = 0; + for (const x of b.v) { + a = new C(2); + churn(); + t += x; + } + + print(t); +} diff --git a/tslang/test/tester/own/own_err_borrow_escape.ts b/tslang/test/tester/own/own_err_borrow_escape.ts new file mode 100644 index 000000000..9cf909322 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_escape.ts @@ -0,0 +1,12 @@ +// -mm=own, phase 2 rejects: `b` borrows `a` (which is used again), and is then stored into a +// field - the field would hold a value `a` goes on owning and will free. +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + let a = new C(); + let b = a; + h.c = b; + print(a.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_borrow_finally_local.ts b/tslang/test/tester/own/own_err_borrow_finally_local.ts new file mode 100644 index 000000000..c1189ea1e --- /dev/null +++ b/tslang/test/tester/own/own_err_borrow_finally_local.ts @@ -0,0 +1,29 @@ +// -mm=own, phase 2 rejects: the same through a finally clause's local. +class C { + v: number[] = []; + constructor(public x: number) {} + m() { return this.x; } +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function main() { + let a = new C(1); + a.v.push(1); + let b = a; + try { + print(b.x); + } finally { + let x = b; + a = new C(2); + churn(); + print(x.x, x.v.length); + } +} diff --git a/tslang/test/tester/own/own_err_const_twice.ts b/tslang/test/tester/own/own_err_const_twice.ts deleted file mode 100644 index 1bb5a1c0a..000000000 --- a/tslang/test/tester/own/own_err_const_twice.ts +++ /dev/null @@ -1,9 +0,0 @@ -// -mm=own, phase 0 rejects: two locals would each own the one instance `a` names. -class C { v: number[] = [1,2,3]; } -function main() { - const a = new C(); - let b = a; - let c = a; - print(b.v.length + c.v.length); - print("done."); -} diff --git a/tslang/test/tester/own/own_err_let_loop.ts b/tslang/test/tester/own/own_err_let_loop.ts deleted file mode 100644 index 7a4598f91..000000000 --- a/tslang/test/tester/own/own_err_let_loop.ts +++ /dev/null @@ -1,11 +0,0 @@ -// -mm=own, phase 1 rejects: `a` is declared outside the loop and moved inside it, so the -// second iteration would move a value that is already gone (spec 2.6). -class C { x: number = 5; } -function main() { - let a = new C(); - for (let i = 0; i < 3; i++) { - let b = a; - print(b.x); - } - print("done."); -} diff --git a/tslang/test/tester/own/own_err_let_twice.ts b/tslang/test/tester/own/own_err_let_twice.ts deleted file mode 100644 index 4f2e9a503..000000000 --- a/tslang/test/tester/own/own_err_let_twice.ts +++ /dev/null @@ -1,9 +0,0 @@ -// -mm=own, phase 1 rejects: `a` moves into `b`, then is read again for `c`. -class C { x: number = 5; } -function main() { - let a = new C(); - let b = a; - let c = a; - print(b.x, c.x); - print("done."); -} diff --git a/tslang/test/tester/own/own_err_move_some_paths.ts b/tslang/test/tester/own/own_err_move_some_paths.ts deleted file mode 100644 index 00491fd5d..000000000 --- a/tslang/test/tester/own/own_err_move_some_paths.ts +++ /dev/null @@ -1,12 +0,0 @@ -// -mm=own, phase 1 rejects: `a` moves into `b` on one path only, and the release after the `if` -// is reached from both. Keeping it double-frees on the path that moved; erasing it leaks on the -// other. Splitting it (drop elaboration) is not in this phase. -class C { x: number = 5; } -function main(argc: number) { - const a = new C(); - if (argc > 5) { - let b = a; - print(b.x); - } - print("done."); -} 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 new file mode 100644 index 000000000..62e2495c7 --- /dev/null +++ b/tslang/test/tester/own/own_err_move_some_paths_field.ts @@ -0,0 +1,11 @@ +// -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_use_after_move.ts b/tslang/test/tester/own/own_err_use_after_move.ts deleted file mode 100644 index d41b232c8..000000000 --- a/tslang/test/tester/own/own_err_use_after_move.ts +++ /dev/null @@ -1,9 +0,0 @@ -// -mm=own, phase 0 rejects: `b` takes `a`'s instance and gives it up at the end of its block; `a` is used after. -class C { v: number[] = [1,2,3]; } -function main() { - const a = new C(); - { let b = a; print(b.v.length); } - const f = [9,9,9,9,9,9,9]; - print(a.v.length); - print("done."); -}