From 98e8b2cab0956aa172e4b664e3550b434b29c384 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 15:16:03 +0100 Subject: [PATCH 1/6] -mm=own phase 3 plan Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-09-29-own-phase-3.md | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 tslang/docs/superpowers/plans/2026-09-29-own-phase-3.md diff --git a/tslang/docs/superpowers/plans/2026-09-29-own-phase-3.md b/tslang/docs/superpowers/plans/2026-09-29-own-phase-3.md new file mode 100644 index 000000000..7e72f858c --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-09-29-own-phase-3.md @@ -0,0 +1,492 @@ +# `-mm=own` Phase 3 Implementation Plan: containers and unions + +> **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 value read out of a field or an element is a *borrow* bounded by its +container (spec §2.3), and a union or optional local is a view of the block it holds. Today: + +- `const c = h.c; h.c = new C(5); c.x` compiles and reads freed memory. So do an element store, + an assignment of `h`, and a call given `h` between the read and the use. rc has the same bug. +- `let c = h.c` and `let e = arr[i]` are rejected ("takes a second reference"). +- `let u: C | null = new C(1)`, `let u: C | string = new C(1)` and `let u: C | undefined = ...` + are rejected, the most common shape in the probes. +- `h2.c = h.c` is rejected with the unhelpful "takes a second reference". + +After this phase, the first becomes an error, the second and third compile, and the fourth says +why. + +**Architecture:** Only `lib/TypeScript/OwnershipInferencePass.cpp` changes. + +1. **Views.** An op whose result is the same block as its operand - a widening or narrowing + `ts.Cast` between class, union and optional types, `ts.CreateUnionInstance`, + `ts.GetValueFromUnionOp`, `ts.OptionalValue`, `ts.Value` - is transparent. Every place the + pass identifies a value strips views up to the root (`rootOf`), and every place it lists a + value's uses looks through views down to the real users (`usesThroughViews`). +2. **Place reads.** A `ts.Load` of a heap-owning value through a `ts.PropertyRef` or + `ts.ElementRef` is a borrow of that place. It never owns: it may not be retained, stored, + pushed or returned. No use of it, or of anything derived from it, may be reachable from a + *drop* of its chain without passing back through the read: + - an overwrite (`ts.ReleaseSlot`) of any place on the chain, matched by position and type; + - an `ArrayPop`/`ArrayShift`/`ArraySplice`/`SetLengthOf` of an array whose elements are on the + chain; + - an end of the chain's root: its releases, assignments, and moves; + - a call that may reach the root. +3. **`let` receivers of a place read** borrow it: their `ts.RetainSlot` and `ts.ReleaseSlot`s go, + exactly as phase 2's borrows, and their reads join the read's uses. + +**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 (dropping +mutations), §2.3, §4.2 step 5, §5 (borrow rows), and §13 (phase 2 as built; §13.5 is this +phase's input). + +## Global Constraints + +- Branch `own-phase-3` from `origin/main` (306b011a, with #402 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. Expected values are + checked under `gc` first (all below were). +- Build: `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --target tslang test-runner` + from `I:/TypeScriptCompiler`. Test: `ctest -j 16 -C Release` from the build directory. Rebuild + after editing `CMakeLists.txt` so ctest picks up new regexes. +- Source files are CRLF. Edit with the Edit tool or a Python byte-replace; new files get CRLF. + On Windows, write multi-line Python to a file rather than a Bash heredoc. +- Commits are GPG-signed (never `--no-gpg-sign`), each ending 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` as well as `--emit=mlir-affine`. + +## Rulings made while planning + +- **`any` boxing moves to phase 4.** §7 lists it under phase 3, but unboxing is a call to a + generated `___unbox` that retains the payload inside and returns it +1, and `___cast` + allocates on one branch and returns the payload on the other. Making either a borrow is phase + 4's borrowed-from-argument result, a callee fact; special-casing the helpers by name here would + be thrown away there. A found rc bug belongs with it: `const a: any = new C(i)` never releases + the box (`measure.ps1`: rc 66.2 MB, gc 6.1 MB over 1M iterations) - the boxing cast is +0 and a + folded `const` takes no reference. Cost if wrong: phase 3 ships without `any`. +- **Calls and roots this function does not own.** A place read whose root is a parameter, `this`, + a global or any value the function did not make is dropped by *every* call: the caller's owner + may be a global the callee assigns. Sound, and restrictive for methods + (`const c = this.c; this.log(); c.x` becomes an error) until phase 4's callee facts. A root the + function owns is dropped only by a call given something derived from the root and not from + the read itself: `f(h)` drops `h.c`, `f(c)` does not. Cost if wrong: methods that compile today + (unsoundly) are rejected. +- **Aliasing.** An overwrite hits a place on the chain when its `ts.PropertyRef` has the same + position and result type, or its `ts.ElementRef` the same result type. Two references to one + field of one object always agree on both, and a derived class keeps its base's positions, so + the match is sound; it may also match an unrelated class's field of the same type at the same + position, which can only add an error. Cost if wrong: extra errors. +- **Merges.** A value merged from branches (`u ?? d`, a conditional) arrives as a block argument + and stays unsupported. §14 lists it. + +## Review Focus + +1. `for (const e of arr) { out.push(e.x); arr.push(new C()) }`: pushing onto the array being + iterated must stay accepted (a push drops nothing). Covered by `own_element_borrow`. +2. A field read inside a loop that overwrites the field at the end of the body must stay + accepted: the next iteration's read is a new borrow. Covered by `own_container_loop`. +3. Temporaries: `h.c.x`, `arr[i].v.length` and `this.items.length` are place reads too. Every + existing own test and the corpus must not regress through them. The corpus report before and + after (Task 4) shows it. +4. A borrow of an element must end at a `pop`, including one in a nested block and one in a + `for...of` body. Covered by `own_err_element_read_popped` and `own_err_element_read_loop_pop`. +5. Views must be stripped at *every* site that identifies a value: candidates, `retainedValue`, + `movesOf`, `takerOf`, `takerAcquires`, `acquirePoint`, `isFresh`, `isImmortalLiteral`, + `slotLoadOf`, `readUsedAfter`, and `tryBorrow`'s walk. One missed site is a spurious error or, + worse, a release that is not erased when the value moved. Each union positive test is also a + teeth test. + +--- + +### Task 1: views + +**Files:** +- Modify: `lib/TypeScript/OwnershipInferencePass.cpp` +- Create: `test/tester/own/own_union_nullable.ts`, `own_union_tagged.ts`, `own_union_optional.ts`, + `own_err_union_moved.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Produces: `static bool isView(mlir::Operation *op)`, `static mlir::Value rootOf(mlir::Value)`, + `static void forEachUse(mlir::Value root, llvm::function_ref)` + (each real user and the value, root or view, it uses). Task 2 relies on all three. + +- [ ] **Step 1: Write the failing tests** + +Copy the four programs from the scratchpad generator (`p3tests.py`), which ran under gc with +these results: `own_union_nullable` asserts `t == 1000000`, `own_union_tagged` `t == 1100000`, +`own_union_optional` `t == 900000`; `own_err_union_moved` prints `5` then `done.`. + +`own_union_nullable.ts` (the other two positives follow the same shape - see the files): + +```ts +function one(i: number) { + let u: C | null = new C(i); + u!.v.push(i); + churn(); + let t: number = 0; + if (u) { + t += u.x + u.v.length; + } + + t += u!.x; + u = null; + return t; +} +``` + +`own_err_union_moved.ts`: + +```ts +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + const c = new C(); + h.c = c; + print(c.x); + print("done."); +} +``` + +CMake: add `own_union_nullable own_union_tagged own_union_optional` to the positive `foreach`, +and `"own_err_union_moved|is used here after its value was moved"` to `own_error_cases`. + +- [ ] **Step 2: Run them, watch the positives fail** + +Run: `ctest -C Release -R "own_union|own-err-own_err_union"` +Expected: the nine positive tests (jit, compile, no-counting × 3) FAIL with "takes a second +reference". `own_err_union_moved` already passes: it is a guard that the view does not hide the +move. + +- [ ] **Step 3: Implement the views** + +```cpp + // An op whose result is the very block its operand holds: a class widened to a union or an + // optional or narrowed back, a union made from its payload or read back from it. rc's retain + // or release of either is one of the block, so the pass looks through them. A cast that + // converts (a number printed into a string) or boxes (into `any`) makes a new block and is + // not one. + static bool isView(mlir::Operation *op) + { + if (mlir::isa_and_nonnull(op)) + { + return true; + } + + auto castOp = mlir::dyn_cast_or_null(op); + if (!castOp) + { + return false; + } + + auto keeps = [](mlir::Type type) { + return mlir::isa(type); + }; + return keeps(castOp.getIn().getType()) && keeps(castOp.getType()); + } + + // The block a value is a view of. + static mlir::Value rootOf(mlir::Value value) + { + while (auto *def = value.getDefiningOp()) + { + if (!isView(def) || def->getNumOperands() == 0) + { + break; + } + + value = def->getOperand(0); + } + + return value; + } + + // Every user of `root` and of the views of it, with the value it uses. A view itself is not a + // use. + static void forEachUse(mlir::Value root, llvm::function_ref each) + { + llvm::SmallVector values{root}; + while (!values.empty()) + { + auto value = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isView(user) && use.getOperandNumber() == 0) + { + values.push_back(user->getResult(0)); + continue; + } + + each(user, value); + } + } + } +``` + +Then, at each site: + +- `runOnFunction` candidates: insert `rootOf(...)` of a Retain's and a Release's operand and of a + Variable's initializer; skip a fresh result that is a view (`isView(op)`), whose root is its + candidate. +- `retainedValue`: return `rootOf(...)`. +- `checkValueMoves` and `movesOf`: collect users with `forEachUse(value, ...)`, calling + `isBorrow(user, used)` with the used value. +- `takerOf`: the same over `forEachUse`. +- `takerAcquires` and `acquirePoint`: a `ts.Retain` of `value` or of any view of it counts. + Collect the retains with `forEachUse`. +- `isImmortalLiteral`: apply to `rootOf(value)`. `isFresh` is only ever asked about roots. +- `slotLoadOf`: step through `isView` ops, not only class/union/optional casts. It keeps the + current behaviour for those casts. +- `readUsedAfter` and `tryBorrow`'s walk: follow a view's result as the same borrowed value, + where they follow a `ts.CastOp` now. In `tryBorrow`, a cast that is *not* a view (to `boolean` + in `if (b)`) keeps its current treatment: a non-borrowed derived value. + +- [ ] **Step 4: Run the tests, watch them pass** + +Run: `ctest -C Release -R "own"` then the whole suite. +Expected: all `own` tests pass, including the new nine positives and `own_err_union_moved`. Whole +suite green. + +- [ ] **Step 5: Teeth** + +Temporarily skip erasing a move's releases (`toErase.insert(moved...)` in `movesOf`), rebuild, +and run the three union positives. Expected: each fails (a double free or a wrong value under +`churn()`). Restore. + +- [ ] **Step 6: Commit** + +`-mm=own phase 3: a union or optional is a view of the block it holds` + +--- + +### Task 2: reads out of containers are borrows + +**Files:** +- Modify: `lib/TypeScript/OwnershipInferencePass.cpp` +- Create: `test/tester/own/own_field_borrow.ts`, `own_element_borrow.ts`, `own_container_loop.ts`, + `own_err_field_read_overwritten.ts`, `own_err_element_read_overwritten.ts`, + `own_err_element_read_popped.ts`, `own_err_element_read_loop_pop.ts`, + `own_err_field_read_owner_assigned.ts`, `own_err_field_read_owner_moved.ts`, + `own_err_field_read_call.ts`, `own_err_param_read_call.ts`, `own_err_container_read_stored.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `isView`, `rootOf`, `forEachUse` (Task 1); `reachableAfter`, `slotEnds`, + `mayPointInto`, `isBorrow`, `ownerName`, `varName`, `reportBorrowEscapes` (phase 2). +- Produces: `static mlir_ts::LoadOp placeReadOf(mlir::Value)` (the `ts.Load` through a + PropertyRef/ElementRef that the root of `value` is, or none); + `bool checkPlaceRead(mlir_ts::LoadOp read, llvm::SetVector &toErase)`. + Task 3 extends `checkPlaceRead` with `let` receivers. + +- [ ] **Step 1: Write the failing tests** + +From `p3tests.py`. Positives, checked under gc: `own_field_borrow` `t == 1100000`, +`own_element_borrow` `t == 80000`, `own_container_loop` `t == 449991`. All three compile under own +*today* (the reads are unchecked); they are the guard against false errors. + +Negatives. Each compiles today, and five of them print `999` under rc and own, reading freed +memory: + +| test | shape | expected message | +| --- | --- | --- | +| `own_err_field_read_overwritten` | `const c = h.c; h.c = new C(5); c.x` | `borrows .* but is used here after it may be released or overwritten` | +| `own_err_element_read_overwritten` | `const e = arr[0]; arr[0] = new C(2); e.x` | same | +| `own_err_element_read_popped` | `const e = arr[0]; { arr.pop(); } e.x` | same | +| `own_err_element_read_loop_pop` | `for (const f of arr) { { arr.pop(); } f.x }` | same | +| `own_err_field_read_owner_assigned` | `let h; const c = h.c; h = new H(); c.x` | same | +| `own_err_field_read_owner_moved` | `const c = h.c; keep.push(h); { keep.pop(); } c.x` | same | +| `own_err_field_read_call` | `const c = h.c; reset(h); c.x` | same | +| `own_err_param_read_call` | `function read(h: H) { const c = h.c; reset(); c.x }` | same | +| `own_err_container_read_stored` | `h2.c = h.c` | `borrows .* and cannot be stored, returned or captured` | + +CMake: the three positives into the `foreach`, the nine negatives into `own_error_cases`. + +- [ ] **Step 2: Run them, watch the negatives fail** + +Run: `ctest -C Release -R "own_field|own_element|own_container|own_err_field|own_err_element|own_err_param|own_err_container"` +Expected: the positives pass. The negatives fail: eight compile with no error, and +`own_err_container_read_stored` reports "takes a second reference". + +- [ ] **Step 3: Implement** + +```cpp + // A read of a heap value out of a field or an element: `h.c`, `arr[i]`. It does not own what + // it reads - the container does - so it can only be a borrow (spec 2.3). + mlir_ts::LoadOp placeReadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp || !ownsHeap(loadOp.getResult())) + { + return {}; + } + + auto *ref = loadOp.getReference().getDefiningOp(); + return mlir::isa_and_nonnull(ref) ? loadOp + : mlir_ts::LoadOp(); + } +``` + +The chain of a read is the list of places from the read up to its root. From the read's reference, +take its base (`PropertyRefOp::getObjectRef`, `ElementRefOp::getArray`) and `rootOf` it. If that +is another place read, add its place and continue; otherwise it is the root. A root is one of: + +- **a slot**: a `ts.Load` of an owning, uncaptured `ts.Variable`, or a `ts.Variable`'s ref + itself (a value-type local such as an object literal). Its ends are `slotEnds(slot)`, plus every + `ts.Retain` of a read of the slot and every owning `ts.Variable` declared from one: the root + may move there. +- **owned**: a fresh value (`isFresh`). Its ends are its `ts.Release`s, and every user that is + neither a borrow nor a view (`forEachUse`): a move. +- **not owned**: anything else - a block argument (a parameter, `this`), a global's load, a + non-fresh call result, a merge. It has no ends here, but every call drops it. + +The drops of a read, from the function's ops (collected once per function): + +- a `ts.ReleaseSlotOp` whose operand is a `PropertyRefOp` with the position and result type of a + chain place, or an `ElementRefOp` with the result type of a chain element place; +- an `ArrayPopOp`/`ArrayShiftOp`/`ArraySpliceOp`/`SetLengthOfOp` whose array's element type is + the value type of a chain element place; +- the root's ends; +- a call (`SymbolCallInternalOp`, `CallOp`, `CallIndirectOp`, `CallInternalOp`, + `CallHybridInternalOp`): every call for a root the function does not own; otherwise one with an + operand derived from the root and not derived from the read. *Derived from x* is the forward + closure over users' results with `mayPointInto`, stopping at call results: a call hands back a + value it owns. + +The read's uses are the `tryBorrow` walk from the read's result: views keep it borrowed, a +`ts.Retain` or a non-borrow use of the borrowed value is an escape (`reportBorrowEscapes` with the +place's description), and derived values' users are uses. Then, for each drop and each use, if +`reachableAfter(drop, use, read)`, report: + +```cpp + void reportPlaceBorrowOutlives(mlir::Operation *use, mlir::Operation *drop, llvm::StringRef name, + llvm::StringRef place) + { + if (quiet) + { + return; + } + + auto diag = use->emitError("'") << name << "' borrows " << place + << " but is used here after it may be released or overwritten"; + diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; + signalPassFailure(); + } +``` + +`name` is `ownerName(read)` - the folded `const`'s name under `--di`. `place` is `'h.c'` when the +chain's first base has a name, with the field's name from the class storage (`getId(position)`), +or `'arr[]'` for an element; otherwise `a field` or `an element`. + +Wiring: + +- `checkValueMoves`: return `true` for a place read. It owns nothing, so there is no move to + check. +- The retain loop: a retain whose value is a place read is skipped. `checkPlaceRead` reports it + (Task 2: as an escape; Task 3: as a borrowing `let`). +- `runOnFunction`: after the candidates, call `checkPlaceRead` on every place read in the + function. Short-circuit when the function has no drops at all. + +- [ ] **Step 4: Run the tests, watch them pass** + +Run: the Step 2 command, then `ctest -C Release -R own`, then the whole suite. +Expected: all pass. If an existing own test fails, read its IR before changing anything: the +failure is either a real hole the old pass missed (document it and fix the test) or a drop matched +too broadly. + +- [ ] **Step 5: Commit** + +`-mm=own phase 3: a read out of a field or an element borrows it until the container drops it` + +--- + +### Task 3: `let` receivers of a place read borrow it + +**Files:** +- Modify: `lib/TypeScript/OwnershipInferencePass.cpp` +- Create: `test/tester/own/own_field_let_borrow.ts`, `own_element_let_borrow.ts`, + `own_err_field_let_overwritten.ts`, `own_err_field_let_assigned.ts` +- Modify: `test/tester/CMakeLists.txt` + +**Interfaces:** +- Consumes: `checkPlaceRead` (Task 2); `borrowerOf`, `tryBorrow` (phase 2). +- Produces: `bool collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::StringRef owner, + llvm::SmallVectorImpl &uses, llvm::SmallVectorImpl &bookkeeping)`, + split out of `tryBorrow`, which now calls it. + +- [ ] **Step 1: Write the failing tests** + +From `p3tests.py`, checked under gc: `own_field_let_borrow` `t == 650000`, +`own_element_let_borrow` `t == 45000`. Negatives: `own_err_field_let_overwritten` +(`borrows .* but is used here after it may be released or overwritten`) and +`own_err_field_let_assigned` (`borrows .* and cannot be assigned`). + +- [ ] **Step 2: Run them, watch them fail** + +Expected: all four report "takes a second reference" (the negatives fail their regex). + +- [ ] **Step 3: Implement** + +Split `tryBorrow`'s first half (the walk over the borrower's slot) into `collectBorrowerUses`. In +`checkPlaceRead`, a user of the read (through views) that is `borrowerOf` a `let` is not an +escape. Its uses join the read's, its bookkeeping (`RetainSlot`, `ReleaseSlot`s) is erased when +the check passes, and its assignment reports `reportBorrowerAssigned` with the place's +description. The kill stays the read, which comes before the `let`. + +- [ ] **Step 4: Run the tests, watch them pass**; then `ctest -C Release -R own` and the whole +suite. + +- [ ] **Step 5: Teeth** + +Temporarily keep a place-read borrower's `ReleaseSlot`s (erase only its `RetainSlot`), rebuild, and +run the two let positives. Expected: both fail. Restore. + +- [ ] **Step 6: Commit** + +`-mm=own phase 3: a let declared from a field or an element borrows it` + +--- + +### Task 4: results + +**Files:** +- Modify: `docs/superpowers/specs/2026-09-24-own-memory-model-design.md` (status line; §7's + phase 3 line gets a pointer to §14; new §14) +- Modify: `test/tester/CMakeLists.txt` (only if a message changed) + +- [ ] **Step 1: Measure** + +`measure.ps1` (and the O1 copy) on `own_field_borrow`, `own_field_let_borrow`, +`own_union_nullable`, `own_union_tagged` under all four models. Expected: own equal to rc (about +4.7-4.8 MB), and none far above both. + +- [ ] **Step 2: Corpus report** + +Compile every `test/tester/tests/*.ts` under `-mm=own --no-default-lib`, as done before this phase +(252 of 564 compiled). Record the count after, and list the files that stopped compiling, with +their first error. Each one is either a hole this phase closed (say which) or a false error to +fix before the PR. + +- [ ] **Step 3: Debug suite** + +Build Debug and run `ctest -C Debug -R own`, watching for assert dialogs. The count changes from +240: new tests are added and none is removed. + +- [ ] **Step 4: Spec §14** + +Write §14 the way §12 and §13 were written: what phase 3 accepts, measured numbers, teeth, changed +tests, the `any` ruling and the rc leak it found, and known limits (merges, `any`, borrows of +parameters' fields across calls, getters). Commit: +`-mm=own phase 3: results`. From 110ef8c3988702a1efb0cab88999e7cbc27c0d58 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 15:23:37 +0100 Subject: [PATCH 2/6] -mm=own phase 3: a union or optional is a view of the block it holds A widening or narrowing cast between class, union and optional types, CreateUnionInstance, GetValueFromUnionOp, OptionalValue and Value hand back the block their operand holds. The pass strips them to the root wherever it names a value and looks through them wherever it lists a value's uses, so `let u: C | null = new C(i)` is one owner, not a second reference. An empty optional (`u = undefined`) holds no block, like `null` widened. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 215 +++++++++++------- tslang/test/tester/CMakeLists.txt | 4 +- tslang/test/tester/own/own_err_union_moved.ts | 11 + tslang/test/tester/own/own_union_nullable.ts | 39 ++++ tslang/test/tester/own/own_union_optional.ts | 38 ++++ tslang/test/tester/own/own_union_tagged.ts | 47 ++++ 6 files changed, 271 insertions(+), 83 deletions(-) create mode 100644 tslang/test/tester/own/own_err_union_moved.ts create mode 100644 tslang/test/tester/own/own_union_nullable.ts create mode 100644 tslang/test/tester/own/own_union_optional.ts create mode 100644 tslang/test/tester/own/own_union_tagged.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 942908237..11b937375 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -13,6 +13,7 @@ #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/STLFunctionalExtras.h" #define DEBUG_TYPE "own" @@ -52,11 +53,12 @@ class OwnershipInferencePass : public mlir::PassWrapper candidates; llvm::SmallVector retains; + // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { if (auto retainOp = mlir::dyn_cast(op)) { retains.push_back(op); - candidates.insert(retainOp.getReference()); + candidates.insert(rootOf(retainOp.getReference())); } else if (auto retainSlotOp = mlir::dyn_cast(op)) { @@ -64,13 +66,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(op)) { - candidates.insert(releaseOp.getReference()); + candidates.insert(rootOf(releaseOp.getReference())); } else if (auto varOp = mlir::dyn_cast(op)) { if (varOp.getInitializer() && isOwningVariable(varOp)) { - candidates.insert(varOp.getInitializer()); + candidates.insert(rootOf(varOp.getInitializer())); } } else if (mlir::isa(op)) @@ -86,7 +88,7 @@ class OwnershipInferencePass : public mlir::PassWrappergetResults()) { - if (isFresh(result) && ownsHeap(result)) + if (!isView(op) && isFresh(result) && ownsHeap(result)) { candidates.insert(result); } @@ -315,11 +317,12 @@ class OwnershipInferencePass : public mlir::PassWrapper(valueUser)) + if (mlir::isa(valueUser) || isView(valueUser)) { - if (mayPointInto(castOp.getResult()) && seen.insert(castOp.getResult()).second) + auto result = valueUser->getResult(0); + if (mayPointInto(result) && seen.insert(result).second) { - values.push_back({castOp.getResult(), true}); + values.push_back({result, true}); } continue; @@ -373,29 +376,92 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) { - return nameAt(user->getLoc()); + name = nameAt(user->getLoc()); } + }); + + return name; + } + + // An op whose result is the very block its operand holds: a class widened to a union or an + // optional or narrowed back, a union made from its payload or read back from it. rc's retain + // or release of either is one of the block, so the pass looks through them. A cast that + // converts (a number printed into a string) or boxes (into `any`) makes a new block and is + // not one. + static bool isView(mlir::Operation *op) + { + if (mlir::isa_and_nonnull(op)) + { + return true; } - return "this value"; + auto castOp = mlir::dyn_cast_or_null(op); + if (!castOp) + { + return false; + } + + auto keeps = [](mlir::Type type) { + return mlir::isa(type); + }; + return keeps(castOp.getIn().getType()) && keeps(castOp.getType()); + } + + // The block a value is a view of. + static mlir::Value rootOf(mlir::Value value) + { + while (auto *def = value.getDefiningOp()) + { + if (!isView(def)) + { + break; + } + + value = def->getOperand(0); + } + + return 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. + // Every user of `root` and of the views of it, with the value it uses. A view itself is not a + // use. + static void forEachUse(mlir::Value root, llvm::function_ref each) + { + llvm::SmallVector values{root}; + while (!values.empty()) + { + auto value = values.pop_back_val(); + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isView(user) && use.getOperandNumber() == 0) + { + values.push_back(user->getResult(0)); + continue; + } + + each(user, value); + } + } + } + + // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer, + // seen through its views. 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. static mlir::Value retainedValue(mlir::Operation *op) { if (auto retainOp = mlir::dyn_cast(op)) { - return retainOp.getReference(); + return rootOf(retainOp.getReference()); } auto varOp = mlir::cast(op).getSlot().getDefiningOp(); - return varOp ? varOp.getInitializer() : mlir::Value(); + return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value(); } static bool isOwningVariable(mlir_ts::VariableOp varOp) @@ -446,7 +512,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); + auto castOp = rootOf(value).getDefiningOp(); return castOp && mlir::isa(castOp.getType()) && castOp.getIn().getDefiningOp(); } @@ -454,9 +520,15 @@ class OwnershipInferencePass : public mlir::PassWrapper(def)) + { + return true; + } + auto castOp = mlir::dyn_cast(def); if (!castOp) { @@ -546,22 +618,21 @@ class OwnershipInferencePass : public mlir::PassWrapper takers; llvm::SmallVector releases; - for (auto *user : value.getUsers()) - { + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { if (mlir::isa(user)) { - continue; + return; } if (mlir::isa(user)) { releases.push_back(user); } - else if (!isBorrow(user, value)) + else if (!isBorrow(user, used)) { takers.push_back(user); } - } + }); if (!releases.empty() && !takers.empty() && llvm::all_of(takers, [](mlir::Operation *taker) { return !!borrowerOf(taker); })) @@ -585,18 +656,17 @@ class OwnershipInferencePass : public mlir::PassWrapper takers; llvm::SmallVector releases; llvm::SmallVector reads; - for (auto *user : value.getUsers()) - { + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { if (mlir::isa(user)) { - continue; + return; } if (mlir::isa(user)) { releases.push_back(user); } - else if (isBorrow(user, value)) + else if (isBorrow(user, used)) { reads.push_back(user); } @@ -604,7 +674,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); + if (!loadOp) { - auto *def = value.getDefiningOp(); - if (auto castOp = mlir::dyn_cast_or_null(def)) - { - if (!mlir::isa(castOp.getType())) - { - return {}; - } - - value = castOp.getIn(); - continue; - } - - auto loadOp = mlir::dyn_cast_or_null(def); - if (!loadOp) - { - return {}; - } - - auto varOp = loadOp.getReference().getDefiningOp(); - if (!varOp || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - return {}; - } + return {}; + } - return loadOp; + auto varOp = loadOp.getReference().getDefiningOp(); + if (!varOp || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return {}; } - return {}; + return loadOp; } // The use that takes `value` for this retain: the declaration a `ts.RetainSlot` belongs to, - // or the one use of the value after a `ts.Retain` that is not a read. + // or the one use of the value, or of a view of it, after a `ts.Retain` that is not a read. static mlir::Operation *takerOf(mlir::Operation *retain, mlir::Value value) { if (auto retainSlotOp = mlir::dyn_cast(retain)) @@ -817,22 +870,18 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) || isBorrow(user, value)) + auto several = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user) || isBorrow(user, used)) { - continue; - } - - if (taker) - { - return nullptr; // two takers of one read: not a move this phase proves + return; } + several = several || taker; taker = user; - } + }); - return taker; + return several ? nullptr : taker; // two takers of one read: not a move this phase proves } // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and @@ -925,9 +974,9 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + if (mlir::isa(user) || isView(user)) { - values.push_back(castOp.getResult()); + values.push_back(user->getResult(0)); continue; } @@ -948,14 +997,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) && user->getBlock() == taker->getBlock() && user->isBeforeInBlock(first)) { first = user; } - } + }); return first; } @@ -973,10 +1021,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(user); }); } - return llvm::any_of(value.getUsers(), [&](mlir::Operation *user) { - return mlir::isa(user) && user->getBlock() == taker->getBlock() && - user->isBeforeInBlock(taker); + auto acquires = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + acquires = acquires || (mlir::isa(user) && user->getBlock() == taker->getBlock() && + user->isBeforeInBlock(taker)); }); + + return acquires; } // The declaration's name survives only as debug metadata (`--di`). At this level MLIRGen's @@ -1011,15 +1062,15 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (auto varOp = mlir::dyn_cast(user); varOp && name == "this value") { - return varName(varOp); + name = varName(varOp); } - } + }); - return "this value"; + return name; } void reportSecondReference(mlir::Operation *op) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index a3e603ca3..dce5c8bfc 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2404,7 +2404,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o 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_borrow_inner_let own_borrow_loop own_borrow_branch - own_borrow_two_lets own_borrow_nullable) + own_borrow_two_lets own_borrow_nullable + own_union_nullable own_union_tagged own_union_optional) 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. @@ -2452,6 +2453,7 @@ set(own_error_cases "own_err_push_loop|is moved inside a loop" "own_err_field_loop|is moved inside a loop" "own_err_interface|takes a second reference" + "own_err_union_moved|is used here after its value was moved" "own_err_delete|'delete' is not supported by -mm=own yet") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) diff --git a/tslang/test/tester/own/own_err_union_moved.ts b/tslang/test/tester/own/own_err_union_moved.ts new file mode 100644 index 000000000..55ad90a33 --- /dev/null +++ b/tslang/test/tester/own/own_err_union_moved.ts @@ -0,0 +1,11 @@ +// -mm=own, phase 3 rejects: `c` is moved into the nullable field through the widening view, then +// read. +class C { x: number = 5; } +class H { c: C | null = null; } +function main() { + const h = new H(); + const c = new C(); + h.c = c; + print(c.x); + print("done."); +} diff --git a/tslang/test/tester/own/own_union_nullable.ts b/tslang/test/tester/own/own_union_nullable.ts new file mode 100644 index 000000000..b7fd598c6 --- /dev/null +++ b/tslang/test/tester/own/own_union_nullable.ts @@ -0,0 +1,39 @@ +// -mm=own, phase 3: a nullable local owns its class value. Widening it to `C | null`, testing it +// and narrowing it back with `!` are views of the one block, not second owners. +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 one(i: number) { + let u: C | null = new C(i); + u!.v.push(i); + churn(); + let t: number = 0; + if (u) { + t += u.x + u.v.length; + } + + t += u!.x; + u = null; + return t; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += one(i % 10); + } + + assert(t == 1000000); + print("done."); +} diff --git a/tslang/test/tester/own/own_union_optional.ts b/tslang/test/tester/own/own_union_optional.ts new file mode 100644 index 000000000..160d780ce --- /dev/null +++ b/tslang/test/tester/own/own_union_optional.ts @@ -0,0 +1,38 @@ +// -mm=own, phase 3: an optional local owns its class value; testing it and narrowing it with `!` +// read it through the view. +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 one(i: number) { + let u: C | undefined = new C(i); + churn(); + let t: number = 0; + if (u !== undefined) { + t += u.x; + } + + t += u!.x; + u = undefined; + return t; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += one(i % 10); + } + + assert(t == 900000); + print("done."); +} diff --git a/tslang/test/tester/own/own_union_tagged.ts b/tslang/test/tester/own/own_union_tagged.ts new file mode 100644 index 000000000..43586ffae --- /dev/null +++ b/tslang/test/tester/own/own_union_tagged.ts @@ -0,0 +1,47 @@ +// -mm=own, phase 3: a tagged union owns its class payload. Making the union, reading the payload +// back after `typeof` and overwriting it with a string literal are views of that payload. +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 one(i: number) { + let t: number = 0; + let u: C | string = new C(i); + if (typeof u !== "string") { + t += u.x; + } + + const c = new C(i + 1); + let w: C | string = c; + churn(); + if (typeof w !== "string") { + t += w.x; + } + + u = "s"; + if (typeof u === "string") { + t += 1; + } + + return t; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += one(i % 10); + } + + assert(t == 1100000); + print("done."); +} From 385c42a07c7db72cdf5fc770c8f7a6afe6a6aa2a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 15:40:23 +0100 Subject: [PATCH 3/6] -mm=own phase 3: a read out of a field or an element borrows it until the container drops it `const c = h.c` and `arr[i]` read a value the container owns, and until now nothing bounded them: an overwrite of the field or element, a pop, an assignment or move of the owner, or a call given it could free the value while it was still read (rc has the same hole). Such a read is now a borrow. It may not be kept (a store or a retain is an error), and no use of it - nor of anything it produces, passed on through a merge or a local that owns nothing - may be reachable from: - an overwrite of a place on the way to its root (matched by position and type); - a pop, shift, splice or length assignment of an array of its element type; - an end of the root: its releases, assignments and moves; - a call given something derived from the root, or any call where the root is a parameter, `this` or a global, whose owner the callee may reach. A path back to a use through the read is a new borrow. Testing an optional (`if (d)`, `d?.x`) and `typeof` are reads, not takers. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 685 ++++++++++++++++-- tslang/test/tester/CMakeLists.txt | 16 +- tslang/test/tester/own/own_borrow_merge.ts | 57 ++ tslang/test/tester/own/own_container_loop.ts | 35 + tslang/test/tester/own/own_element_borrow.ts | 42 ++ .../tester/own/own_err_alias_read_popped.ts | 31 + .../own/own_err_container_read_stored.ts | 28 + .../own/own_err_element_read_loop_pop.ts | 27 + .../own/own_err_element_read_overwritten.ts | 23 + .../tester/own/own_err_element_read_popped.ts | 27 + .../tester/own/own_err_field_read_call.ts | 32 + .../own/own_err_field_read_overwritten.ts | 29 + .../own/own_err_field_read_owner_assigned.ts | 28 + .../own/own_err_field_read_owner_moved.ts | 34 + .../own/own_err_global_read_assigned.ts | 30 + .../own/own_err_merge_read_overwritten.ts | 31 + .../tester/own/own_err_param_read_call.ts | 38 + tslang/test/tester/own/own_field_borrow.ts | 41 ++ tslang/test/tester/own/own_optional_access.ts | 49 ++ 19 files changed, 1241 insertions(+), 42 deletions(-) create mode 100644 tslang/test/tester/own/own_borrow_merge.ts create mode 100644 tslang/test/tester/own/own_container_loop.ts create mode 100644 tslang/test/tester/own/own_element_borrow.ts create mode 100644 tslang/test/tester/own/own_err_alias_read_popped.ts create mode 100644 tslang/test/tester/own/own_err_container_read_stored.ts create mode 100644 tslang/test/tester/own/own_err_element_read_loop_pop.ts create mode 100644 tslang/test/tester/own/own_err_element_read_overwritten.ts create mode 100644 tslang/test/tester/own/own_err_element_read_popped.ts create mode 100644 tslang/test/tester/own/own_err_field_read_call.ts create mode 100644 tslang/test/tester/own/own_err_field_read_overwritten.ts create mode 100644 tslang/test/tester/own/own_err_field_read_owner_assigned.ts create mode 100644 tslang/test/tester/own/own_err_field_read_owner_moved.ts create mode 100644 tslang/test/tester/own/own_err_global_read_assigned.ts create mode 100644 tslang/test/tester/own/own_err_merge_read_overwritten.ts create mode 100644 tslang/test/tester/own/own_err_param_read_call.ts create mode 100644 tslang/test/tester/own/own_field_borrow.ts create mode 100644 tslang/test/tester/own/own_optional_access.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 11b937375..9bc4c66a2 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1,6 +1,7 @@ #include "mlir/Pass/Pass.h" #include "mlir/IR/Dominance.h" #include "mlir/Dialect/LLVMIR/LLVMAttrs.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" #include "TypeScript/TypeScriptDialect.h" #include "TypeScript/TypeScriptOps.h" @@ -52,6 +53,10 @@ class OwnershipInferencePass : public mlir::PassWrapper candidates; llvm::SmallVector retains; + // reads out of fields and elements, and what may destroy what they read (spec 2.3) + llvm::SmallVector placeReads; + drops.clear(); + derivedCache.clear(); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { @@ -85,6 +90,27 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'delete' is not supported by -mm=own yet"); signalPassFailure(); } + else if (auto loadOp = mlir::dyn_cast(op)) + { + if (placeReadOf(loadOp.getResult())) + { + placeReads.push_back(loadOp); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + if (mlir::isa_and_nonnull( + releaseSlotOp.getSlot().getDefiningOp())) + { + drops.push_back(op); + } + } + else if (mlir::isa(op) || + isCall(op)) + { + drops.push_back(op); + } for (auto result : op->getResults()) { @@ -108,6 +134,11 @@ class OwnershipInferencePass : public mlir::PassWrapper drops; + + // derivedFrom's answer for each value it was asked about in this function; held by pointer, so + // an answer stays put while the next one is added + llvm::DenseMap>> derivedCache; + // While non-zero, verdicts are tried without reporting: the next verdict gets its turn first. unsigned quiet = 0; @@ -301,75 +345,596 @@ class OwnershipInferencePass : public mlir::PassWrapper &values) + { + auto *user = use.getOwner(); + if (mlir::isa(user) || isView(user)) + { + values.push_back(user->getResult(0)); + return true; + } + + if (auto branchOp = mlir::dyn_cast(user)) + { + if (auto argument = branchOp.getSuccessorBlockArgument(use.getOperandNumber())) + { + values.push_back(*argument); + return true; + } + + return false; + } + + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == use.get()) + { + if (auto varOp = storeOp.getReference().getDefiningOp(); + varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) + { + for (auto *varUser : varOp.getResult().getUsers()) + { + if (auto loadOp = mlir::dyn_cast(varUser)) + { + values.push_back(loadOp.getResult()); + } + } - // 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()) + return true; + } + } + + return false; + } + + // Where a borrowed value goes. Every use of it, and of what it produces that may still point + // into the block, joins `uses`. The value itself - through what passesOn hands it on to - 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 borrow too. A value that cannot point anywhere (a number, a + // boolean) ends the walk. `isBorrower` accepts a use of the value itself that the caller + // follows on its own (a borrowing `let`). Answers the first use that would keep the value. + mlir::Operation *walkBorrowed(mlir::Value start, llvm::SmallVectorImpl &uses, + llvm::function_ref isBorrower = nullptr) + { + llvm::SmallVector> values{{start, true}}; + llvm::SmallPtrSet seen{start}; + while (!values.empty()) + { + auto [current, borrowed] = values.pop_back_val(); + for (auto &use : current.getUses()) + { + auto *user = use.getOwner(); + uses.push_back(user); + if (borrowed) { - uses.push_back(valueUser); - if (borrowed) + llvm::SmallVector same; + if (passesOn(use, same)) { - if (mlir::isa(valueUser) || isView(valueUser)) + for (auto value : same) { - auto result = valueUser->getResult(0); - if (mayPointInto(result) && seen.insert(result).second) + if (mayPointInto(value) && seen.insert(value).second) { - values.push_back({result, true}); + values.push_back({value, true}); } - - continue; } - if (mlir::isa(valueUser) || !isBorrow(valueUser, current)) - { - reportBorrowEscapes(valueUser, name, owner); - return false; - } + continue; + } + + if (isBorrower && isBorrower(user)) + { + continue; + } + + if (mlir::isa(user) || !isBorrow(user, current)) + { + return user; } + } - for (auto result : valueUser->getResults()) + for (auto result : user->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) { - if (mayPointInto(result) && seen.insert(result).second) + values.push_back({result, false}); + } + } + } + } + + return nullptr; + } + + // 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); + } + + // ---- Reads out of containers (spec 2.3, phase 3) ---- + // + // `h.c` and `arr[i]` read a value the container owns. Such a read is a borrow: it may not be + // kept, and none of its uses - nor any use of what it produces - may run after something that + // may destroy it (spec 2.2's dropping mutations): an overwrite of a place on the way from the + // read up to its root, an array op that removes elements, an end of the root itself, or a call + // that may reach the root. A path back to a use through the read itself is a new borrow. + + static bool isCall(mlir::Operation *op) + { + return mlir::isa(op); + } + + static bool isPlace(mlir::Value ref) + { + return mlir::isa_and_nonnull(ref.getDefiningOp()); + } + + // A read of a heap value out of a field or an element, seen through its views. None for + // anything else. + mlir_ts::LoadOp placeReadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp || !isPlace(loadOp.getReference()) || !ownsHeap(loadOp.getResult())) + { + return {}; + } + + return loadOp; + } + + // The places from a read up to its root, and the root: the value the function reached the + // first container through. + struct Chain + { + enum Kind + { + Slot, // an owning local, or a value-type local reached by reference + Owned, // a value this function made + NotOwned // a parameter, `this`, a global, anything else + }; + + // a field's position and reference type; an element is position -1 + llvm::SmallVector> places; + mlir::Value root; + Kind kind = NotOwned; + }; + + Chain chainOf(mlir_ts::LoadOp read) + { + Chain chain; + auto ref = read.getReference(); + while (true) + { + mlir::Value base; + if (auto propertyRefOp = ref.getDefiningOp()) + { + chain.places.push_back({propertyRefOp.getPosition(), propertyRefOp.getType()}); + base = propertyRefOp.getObjectRef(); + } + else + { + auto elementRefOp = ref.getDefiningOp(); + chain.places.push_back({-1, elementRefOp.getType()}); + base = elementRefOp.getArray(); + } + + base = rootOf(base); + if (isPlace(base)) + { + ref = base; // a field of a value-type field: the same object + continue; + } + + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + ref = loadOp.getReference(); + continue; + } + + chain.root = base; + if (base.getDefiningOp()) + { + chain.kind = Chain::Slot; + } + else if (auto slotLoad = slotLoadOf(base)) + { + chain.root = slotLoad.getReference(); + chain.kind = Chain::Slot; + } + else if (isFresh(base)) + { + chain.kind = Chain::Owned; + } + + return chain; + } + } + + // Where the root stops holding what the chain reads: a local's releases and assignments and + // every declaration or retain that may move it away; a made value's releases and every use + // that takes it; a global's assignments. + llvm::SmallVector rootEnds(const Chain &chain) + { + llvm::SmallVector ends; + if (chain.kind == Chain::Slot) + { + for (auto *user : chain.root.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + else if (auto loadOp = mlir::dyn_cast(user)) + { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use) || (varOp && isOwningVariable(varOp))) { - values.push_back({result, false}); + ends.push_back(use); } + }); + } + } + } + else if (chain.kind == Chain::Owned) + { + forEachUse(chain.root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) + { + ends.push_back(use); + } + }); + } + else if (auto loadOp = chain.root.getDefiningOp()) + { + if (auto global = loadOp.getReference().getDefiningOp()) + { + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global.getGlobalName()) + { + ends.push_back(op); + } + }); + } + } + + return ends; + } + + // What a call has to be given to reach the root: the root, and where the root is a local + // declared from another (a borrow, or a move that left the other dead), that one too. + static llvm::SmallVector rootSources(const Chain &chain) + { + llvm::SmallVector sources{chain.root}; + auto value = chain.root; + while (auto varOp = value.getDefiningOp()) + { + if (!varOp.getInitializer() || sources.size() > 8) + { + break; + } + + auto initializer = rootOf(varOp.getInitializer()); + auto slotLoad = slotLoadOf(initializer); + value = slotLoad ? slotLoad.getReference() : initializer; + sources.push_back(value); + } + + return sources; + } + + // Everything that may point into what `start` points to: the forward closure over users' + // results that may point anywhere. A call's result is not followed: the call hands back a + // value it owns. + const llvm::DenseSet &derivedFrom(mlir::Value start) + { + if (auto found = derivedCache.find(start); found != derivedCache.end()) + { + return *found->second; + } + + auto &slot = derivedCache[start]; + slot = std::make_shared>(); + auto &derived = *slot; + llvm::SmallVector work{start}; + while (!work.empty()) + { + auto value = work.pop_back_val(); + if (!derived.insert(value).second) + { + continue; + } + + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (isCall(user)) + { + continue; + } + + llvm::SmallVector next(user->getResults().begin(), user->getResults().end()); + passesOn(use, next); + for (auto result : next) + { + if (mayPointInto(result)) + { + work.push_back(result); } } } } - for (auto *end : ends) + return derived; + } + + // May this op of the function's drops destroy what the chain reads? An overwrite of a place + // on it, matched by position and type (two references to one field of one object always + // agree on both; an unrelated field that happens to agree only adds an error); an array op + // that removes elements of the type of an element on it; a call - any call, for a root this + // function does not own, since the owner may be a global the callee assigns; else one given + // something derived from the root that the read did not produce. + bool dropsChain(mlir::Operation *drop, const Chain &chain, mlir_ts::LoadOp read) + { + if (auto releaseSlotOp = mlir::dyn_cast(drop)) + { + auto slot = releaseSlotOp.getSlot(); + int64_t position = -1; + if (auto propertyRefOp = slot.getDefiningOp()) + { + position = propertyRefOp.getPosition(); + } + + return llvm::any_of(chain.places, [&](auto &place) { + return place.first == position && place.second == slot.getType(); + }); + } + + if (isCall(drop)) + { + if (chain.kind == Chain::NotOwned) + { + return true; + } + + auto &fromRead = derivedFrom(read.getResult()); + auto sources = rootSources(chain); + return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { + return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { + return derivedFrom(source).contains(operand); + }); + }); + } + + // pop, shift, splice, `length =` + auto refType = mlir::dyn_cast(drop->getOperand(0).getType()); + auto arrayType = refType ? mlir::dyn_cast(refType.getElementType()) : mlir_ts::ArrayType(); + if (!arrayType) + { + return true; + } + + return llvm::any_of(chain.places, [&](auto &place) { + auto placeType = mlir::dyn_cast(place.second); + return place.first == -1 && placeType && placeType.getElementType() == arrayType.getElementType(); + }); + } + + // The uses of what `read` returned and of everything it produces that may still point into + // the block (walkBorrowed). A retain or a taking use is reported and answers false. + bool collectReadUses(mlir_ts::LoadOp read, llvm::StringRef name, llvm::StringRef place, + llvm::SmallVectorImpl &uses) + { + if (auto *kept = walkBorrowed(read.getResult(), uses)) { + reportPlaceBorrowEscapes(kept, name, place); + return false; + } + + return true; + } + + // A read out of a container is a borrow of it (spec 2.3). Reports and returns false when it + // is kept, or used after something that may destroy it. + bool checkPlaceRead(mlir_ts::LoadOp read, llvm::SetVector &toErase) + { + auto name = ownerName(read.getResult()); + auto place = describePlace(read.getReference()); + llvm::SmallVector uses; + if (!collectReadUses(read, name, place, uses)) + { + return false; + } + + auto chain = chainOf(read); + auto outlives = [&](mlir::Operation *drop) { for (auto *use : uses) { - if (reachableAfter(end, use, borrower)) + // a call's own arguments are read before it runs + if (use != drop && reachableAfter(drop, use, read)) { - reportBorrowOutlives(use, end, name, owner); - return false; + reportPlaceBorrowOutlives(use, drop, name, place); + return true; } } + + return false; + }; + + for (auto *end : rootEnds(chain)) + { + if (outlives(end)) + { + return false; + } + } + + for (auto *drop : drops) + { + if (dropsChain(drop, chain, read) && outlives(drop)) + { + 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) + // `'h.c'`, `'arr[]'`, `'h.c.v'` - or `a field`, `an element` when the root has no name (a + // temporary, or no --di). + std::string describePlace(mlir::Value ref) { - auto type = value.getType(); - return mlir::isa(type) || - ownsHeap(value); + auto path = pathOf(ref); + if (path.empty()) + { + return ref.getDefiningOp() ? "an element" : "a field"; + } + + return "'" + path + "'"; + } + + std::string pathOf(mlir::Value ref) + { + mlir::Value base; + std::string step; + if (auto propertyRefOp = ref.getDefiningOp()) + { + auto field = fieldName(propertyRefOp); + if (field.empty()) + { + return {}; + } + + base = propertyRefOp.getObjectRef(); + step = "." + field; + } + else if (auto elementRefOp = ref.getDefiningOp()) + { + base = elementRefOp.getArray(); + step = "[]"; + } + else + { + return {}; + } + + base = rootOf(base); + std::string prefix; + if (isPlace(base)) + { + prefix = pathOf(base); + } + else if (auto loadOp = base.getDefiningOp()) + { + auto from = loadOp.getReference(); + if (isPlace(from)) + { + prefix = pathOf(from); + } + else if (auto varOp = from.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else if (auto global = from.getDefiningOp()) + { + prefix = global.getGlobalName().str(); + } + } + else if (auto varOp = base.getDefiningOp()) + { + prefix = varName(varOp).str(); + } + else + { + prefix = ownerName(base).str(); + } + + // a name MLIRGen made up (`.a`, the array a `for...of` walks) means nothing to the reader + if (prefix.empty() || prefix == "this value" || prefix.front() == '.') + { + return {}; + } + + return prefix + step; + } + + static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) + { + auto type = propertyRefOp.getObjectRef().getType(); + if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + + if (auto classType = mlir::dyn_cast(type)) + { + type = classType.getStorageType(); + } + + auto position = propertyRefOp.getPosition(); + mlir::Attribute id; + if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) + { + id = storageType.getId(position); + } + else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) + { + id = tupleType.getId(position); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.str(); + } + + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.getValue().str(); + } + + return {}; } // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it @@ -557,6 +1122,18 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + { + return true; + } + + if (auto castOp = mlir::dyn_cast(user)) + { + return mlir::isa(castOp.getType()) || castOp.getType().isInteger(1); + } + // arguments are borrowed; a callee that keeps one retains it, and that retain is its own // error if (mlir::isaemitError("'") << name << "' borrows " << place + << " but is used here after it may be released or overwritten"; + diag.attachNote(drop->getLoc()) << "it may be released or overwritten here"; + signalPassFailure(); + } + + void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (quiet) + { + return; + } + + op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + signalPassFailure(); + } + void reportMovedWhileBorrowed(mlir::Operation *op, llvm::StringRef owner, llvm::StringRef borrower) { if (quiet) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index dce5c8bfc..e803eedac 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2405,7 +2405,9 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_literal_reuse own_move_control_flow own_borrow_inner_let own_borrow_loop own_borrow_branch own_borrow_two_lets own_borrow_nullable - own_union_nullable own_union_tagged own_union_optional) + own_union_nullable own_union_tagged own_union_optional + own_field_borrow own_element_borrow own_container_loop + own_optional_access own_borrow_merge) 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. @@ -2454,6 +2456,18 @@ set(own_error_cases "own_err_field_loop|is moved inside a loop" "own_err_interface|takes a second reference" "own_err_union_moved|is used here after its value was moved" + "own_err_field_read_overwritten|borrows .* but is used here after it may be released or overwritten" + "own_err_element_read_overwritten|borrows .* but is used here after it may be released or overwritten" + "own_err_element_read_popped|borrows .* but is used here after it may be released or overwritten" + "own_err_element_read_loop_pop|borrows .* but is used here after it may be released or overwritten" + "own_err_field_read_owner_assigned|borrows .* but is used here after it may be released or overwritten" + "own_err_field_read_owner_moved|borrows .* but is used here after it may be released or overwritten" + "own_err_field_read_call|borrows .* but is used here after it may be released or overwritten" + "own_err_param_read_call|borrows .* but is used here after it may be released or overwritten" + "own_err_global_read_assigned|borrows .* but is used here after it may be released or overwritten" + "own_err_merge_read_overwritten|borrows .* but is used here after it may be released or overwritten" + "own_err_alias_read_popped|borrows .* but is used here after it may be released or overwritten" + "own_err_container_read_stored|borrows .* and cannot be stored, returned or captured" "own_err_delete|'delete' is not supported by -mm=own yet") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) diff --git a/tslang/test/tester/own/own_borrow_merge.ts b/tslang/test/tester/own/own_borrow_merge.ts new file mode 100644 index 000000000..dbfb4ac7b --- /dev/null +++ b/tslang/test/tester/own/own_borrow_merge.ts @@ -0,0 +1,57 @@ +// -mm=own, phase 3: a borrow passed on through a merge (`??`) or stored into a local that owns +// nothing (`for (x of a)` into an outer `let`) is still that borrow, bounded by its container. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + + +class N { + c: C | null = null; +} + +function one(n: N, m: N) { + const c = n.c ?? m.c; + let t: number = 0; + if (c) { + t += c.x; + } + + return t; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 10000; i++) { + const n = new N(); + const m = new N(); + m.c = new C(i % 10); + t += one(n, m); + const arr: C[] = [new C(1), new C(2)]; + let x: C | number; + for (x of arr) { + churn(); + if (typeof x !== "number") { + t += x.x; + } + } + } + + assert(t == 75000); + print("done."); +} diff --git a/tslang/test/tester/own/own_container_loop.ts b/tslang/test/tester/own/own_container_loop.ts new file mode 100644 index 000000000..21d7ec583 --- /dev/null +++ b/tslang/test/tester/own/own_container_loop.ts @@ -0,0 +1,35 @@ +// -mm=own, phase 3: each iteration borrows `h.c` afresh and overwrites it after the last use; the +// path back to the next iteration's use passes through the next read. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const h = new H(); + let t: number = 0; + for (let i = 0; i < 100000; i++) { + const c = h.c; + churn(); + t += c.x; + h.c = new C(i % 10); + } + + assert(t == 449991); + print("done."); +} diff --git a/tslang/test/tester/own/own_element_borrow.ts b/tslang/test/tester/own/own_element_borrow.ts new file mode 100644 index 000000000..12dbeb935 --- /dev/null +++ b/tslang/test/tester/own/own_element_borrow.ts @@ -0,0 +1,42 @@ +// -mm=own, phase 3: `const e = arr[3]` and `for (const f of arr)` borrow elements. Pushing onto +// the same array or another one cannot destroy an element, and neither can a call that is not +// given `arr`; the `pop` comes after the last use. +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 main() { + let t: number = 0; + for (let i = 0; i < 1000; i++) { + const arr: C[] = []; + for (let j = 0; j < 10; j++) { + arr.push(new C(j)); + } + + const e = arr[3]; + const out: C[] = []; + out.push(new C(e.x)); + arr.push(new C(10)); + churn(); + t += e.x + out.length + arr.length; + for (const f of arr) { + t += f.x; + } + + arr.pop(); + t += arr.length; + } + + assert(t == 80000); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_alias_read_popped.ts b/tslang/test/tester/own/own_err_alias_read_popped.ts new file mode 100644 index 000000000..c891c2571 --- /dev/null +++ b/tslang/test/tester/own/own_err_alias_read_popped.ts @@ -0,0 +1,31 @@ +// -mm=own, phase 3 rejects: `x` owns nothing, so the loop stores a borrow of each element into it, +// and the `pop` destroys the element `x` still names. +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 main() { + const arr: C[] = [new C(1)]; + let x: C | number; + for (x of arr) { + } + + { + arr.pop(); + } + + churn(); + if (typeof x !== "number") { + print(x.x); + } +} diff --git a/tslang/test/tester/own/own_err_container_read_stored.ts b/tslang/test/tester/own/own_err_container_read_stored.ts new file mode 100644 index 000000000..2bd4b4b37 --- /dev/null +++ b/tslang/test/tester/own/own_err_container_read_stored.ts @@ -0,0 +1,28 @@ +// -mm=own, phase 3 rejects: a value read out of a field is borrowed; storing it into another +// field would give it a second owner. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const h = new H(); + const h2 = new H(); + h2.c = h.c; + print(h2.c.x); +} diff --git a/tslang/test/tester/own/own_err_element_read_loop_pop.ts b/tslang/test/tester/own/own_err_element_read_loop_pop.ts new file mode 100644 index 000000000..addeb8bb3 --- /dev/null +++ b/tslang/test/tester/own/own_err_element_read_loop_pop.ts @@ -0,0 +1,27 @@ +// -mm=own, phase 3 rejects: the loop's `f` borrows an element that the body pops. +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 main() { + const arr: C[] = []; + arr.push(new C(1)); + for (const f of arr) { + { + arr.pop(); + } + + churn(); + print(f.x); + } +} diff --git a/tslang/test/tester/own/own_err_element_read_overwritten.ts b/tslang/test/tester/own/own_err_element_read_overwritten.ts new file mode 100644 index 000000000..2ad40b65a --- /dev/null +++ b/tslang/test/tester/own/own_err_element_read_overwritten.ts @@ -0,0 +1,23 @@ +// -mm=own, phase 3 rejects: `e` borrows `arr[0]`, and the element store destroys 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 main() { + const arr: C[] = []; + arr.push(new C(1)); + const e = arr[0]; + arr[0] = new C(2); + churn(); + print(e.x); +} diff --git a/tslang/test/tester/own/own_err_element_read_popped.ts b/tslang/test/tester/own/own_err_element_read_popped.ts new file mode 100644 index 000000000..f7f3e97a8 --- /dev/null +++ b/tslang/test/tester/own/own_err_element_read_popped.ts @@ -0,0 +1,27 @@ +// -mm=own, phase 3 rejects: `e` borrows `arr[0]`, and the `pop` - whose result is given back at +// the end of its block - destroys 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 main() { + const arr: C[] = []; + arr.push(new C(1)); + const e = arr[0]; + { + arr.pop(); + } + + churn(); + print(e.x); +} diff --git a/tslang/test/tester/own/own_err_field_read_call.ts b/tslang/test/tester/own/own_err_field_read_call.ts new file mode 100644 index 000000000..a79f135d8 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_read_call.ts @@ -0,0 +1,32 @@ +// -mm=own, phase 3 rejects: `c` borrows `h.c`, and a call given `h` may overwrite 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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function reset(h: H) { + h.c = new C(9); +} + +function main() { + const h = new H(); + const c = h.c; + reset(h); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_field_read_overwritten.ts b/tslang/test/tester/own/own_err_field_read_overwritten.ts new file mode 100644 index 000000000..2b09c5f87 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_read_overwritten.ts @@ -0,0 +1,29 @@ +// -mm=own, phase 3 rejects: `c` borrows `h.c`, and the overwrite of `h.c` destroys it before the +// read. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const h = new H(); + const c = h.c; + h.c = new C(5); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_field_read_owner_assigned.ts b/tslang/test/tester/own/own_err_field_read_owner_assigned.ts new file mode 100644 index 000000000..b90cbc48b --- /dev/null +++ b/tslang/test/tester/own/own_err_field_read_owner_assigned.ts @@ -0,0 +1,28 @@ +// -mm=own, phase 3 rejects: `c` borrows `h.c`, and assigning `h` destroys the old `h` with 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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + let h = new H(); + const c = h.c; + h = new H(); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_field_read_owner_moved.ts b/tslang/test/tester/own/own_err_field_read_owner_moved.ts new file mode 100644 index 000000000..46bc1dc72 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_read_owner_moved.ts @@ -0,0 +1,34 @@ +// -mm=own, phase 3 rejects: `c` borrows `h.c`; `h` then moves into `keep`, whose `pop` destroys +// it - a borrow ends where its root moves. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const keep: H[] = []; + const h = new H(); + const c = h.c; + keep.push(h); + { + keep.pop(); + } + + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_global_read_assigned.ts b/tslang/test/tester/own/own_err_global_read_assigned.ts new file mode 100644 index 000000000..159a82ebf --- /dev/null +++ b/tslang/test/tester/own/own_err_global_read_assigned.ts @@ -0,0 +1,30 @@ +// -mm=own, phase 3 rejects: `c` borrows a field of the global `g`, and assigning `g` destroys the +// old value with 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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +let g = new H(); + +function main() { + const c = g.c; + g = new H(); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_merge_read_overwritten.ts b/tslang/test/tester/own/own_err_merge_read_overwritten.ts new file mode 100644 index 000000000..f763bfabe --- /dev/null +++ b/tslang/test/tester/own/own_err_merge_read_overwritten.ts @@ -0,0 +1,31 @@ +// -mm=own, phase 3 rejects: `c` is `n.c` or `m.c` through a merge, and `n.c` is overwritten before +// the read. +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); + } +} + +class N { + c: C | null = null; +} + +function main() { + const n = new N(); + const m = new N(); + n.c = new C(1); + const c = n.c ?? m.c; + n.c = new C(2); + churn(); + if (c) { + print(c.x); + } +} diff --git a/tslang/test/tester/own/own_err_param_read_call.ts b/tslang/test/tester/own/own_err_param_read_call.ts new file mode 100644 index 000000000..5cb338024 --- /dev/null +++ b/tslang/test/tester/own/own_err_param_read_call.ts @@ -0,0 +1,38 @@ +// -mm=own, phase 3 rejects: `c` borrows a field of a parameter, which this function does not own; +// any call may reach its owner - here through a global - and overwrite the field. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +let g = new H(); + +function reset() { + g.c = new C(9); +} + +function read(h: H) { + const c = h.c; + reset(); + churn(); + print(c.x); +} + +function main() { + read(g); +} diff --git a/tslang/test/tester/own/own_field_borrow.ts b/tslang/test/tester/own/own_field_borrow.ts new file mode 100644 index 000000000..4b81f52ff --- /dev/null +++ b/tslang/test/tester/own/own_field_borrow.ts @@ -0,0 +1,41 @@ +// -mm=own, phase 3: `const c = h.c` borrows the field. Stores into other fields - a number, a +// string - cannot destroy it, and neither can a call that is not given `h`; the overwrite of `h.c` +// comes after its last use. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + const h = new H(); + h.c = new C(i % 10); + h.c.v.push(i % 10); + const c = h.c; + h.n = h.n + c.x; + h.s = "b" + i; + churn(); + t += c.x + c.v.length + h.n; + h.c = new C(1); + t += h.c.x; + } + + assert(t == 1100000); + print("done."); +} diff --git a/tslang/test/tester/own/own_optional_access.ts b/tslang/test/tester/own/own_optional_access.ts new file mode 100644 index 000000000..394e3348e --- /dev/null +++ b/tslang/test/tester/own/own_optional_access.ts @@ -0,0 +1,49 @@ +// -mm=own, phase 3: testing an optional (`d?.x`, `if (d)`) reads it; it does not take 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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + + +function one(i: number) { + const c = new C(i); + const d: C | undefined = c; + churn(); + let t: number = 0; + if (d) { + t += d.x; + } + + d?.x; + d?.v; + if (d !== undefined) { + t += d.x + d.v.length; + } + + return t; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + t += one(i % 10); + } + + assert(t == 900000); + print("done."); +} From 6064d713fd1b1691fc0efafc7b68450a7d4ea257 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 15:44:28 +0100 Subject: [PATCH 4/6] -mm=own phase 3: a let declared from a field or an element borrows it `let c = h.c` and `let e = arr[j]` take no reference and give none back: the let's RetainSlot and ReleaseSlots go, and its reads join the read's uses, bounded by what may destroy the container's value. An assignment of such a let is an error, as for phase 2's borrowers. tryBorrow's walk over a borrower's slot is collectBorrowerUses, shared by both. Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 130 ++++++++++++------ tslang/test/tester/CMakeLists.txt | 5 +- .../test/tester/own/own_element_let_borrow.ts | 35 +++++ .../tester/own/own_err_field_let_assigned.ts | 28 ++++ .../own/own_err_field_let_overwritten.ts | 29 ++++ .../test/tester/own/own_field_let_borrow.ts | 38 +++++ 6 files changed, 224 insertions(+), 41 deletions(-) create mode 100644 tslang/test/tester/own/own_element_let_borrow.ts create mode 100644 tslang/test/tester/own/own_err_field_let_assigned.ts create mode 100644 tslang/test/tester/own/own_err_field_let_overwritten.ts create mode 100644 tslang/test/tester/own/own_field_let_borrow.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 9bc4c66a2..ea88fc1f9 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -319,37 +319,21 @@ class OwnershipInferencePass : public mlir::PassWrapper 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 (auto *kept = collectBorrowerUses(borrower, uses, bookkeeping)) { - if (mlir::isa(user)) + if (isAssignmentOf(kept, borrower)) { - bookkeeping.push_back(user); - continue; + reportBorrowerAssigned(kept, name, owner); } - - 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); - if (auto *kept = walkBorrowed(readOp.getResult(), uses)) + else { reportBorrowEscapes(kept, name, owner); - return false; } + + return false; } for (auto *end : ends) @@ -368,6 +352,43 @@ class OwnershipInferencePass : public mlir::PassWrapper &uses, + llvm::SmallVectorImpl &bookkeeping) + { + for (auto *user : borrower.getResult().getUsers()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + return user; + } + + uses.push_back(user); + if (auto *kept = walkBorrowed(readOp.getResult(), uses)) + { + return kept; + } + } + + return nullptr; + } + + static bool isAssignmentOf(mlir::Operation *op, mlir_ts::VariableOp varOp) + { + auto storeOp = mlir::dyn_cast(op); + return storeOp && storeOp.getReference() == varOp.getResult(); + } + // What `use` hands the very value it uses on to, as that value: a cast or a view of it, the // argument of the block a branch passes it to (a merge: `a ?? b`), or the reads of a local // that owns nothing and is assigned it (`for (x of arr)` into an outer `let x`). Answers @@ -762,29 +783,46 @@ class OwnershipInferencePass : public mlir::PassWrapper &uses) - { - if (auto *kept = walkBorrowed(read.getResult(), uses)) - { - reportPlaceBorrowEscapes(kept, name, place); - return false; - } - - return true; - } - - // A read out of a container is a borrow of it (spec 2.3). Reports and returns false when it - // is kept, or used after something that may destroy it. + // A read out of a container is a borrow of it (spec 2.3), and so is a `let` declared from one: + // its retain and releases go when the check passes, as for any borrowing `let` (phase 2). + // Reports and returns false when the read is kept, or used after something that may destroy + // it. bool checkPlaceRead(mlir_ts::LoadOp read, llvm::SetVector &toErase) { - auto name = ownerName(read.getResult()); auto place = describePlace(read.getReference()); + llvm::StringRef name = ownerName(read.getResult()); llvm::SmallVector uses; - if (!collectReadUses(read, name, place, uses)) + llvm::SmallVector bookkeeping; + mlir_ts::VariableOp letKeeps; + mlir::Operation *letKept = nullptr; + auto *kept = walkBorrowed(read.getResult(), uses, [&](mlir::Operation *user) { + auto borrower = borrowerOf(user); + if (!borrower || borrower.getOperation() != user) + { + return false; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, uses, bookkeeping); keeps && !letKept) + { + letKept = keeps; + letKeeps = borrower; + } + + return true; + }); + + if (kept || letKept) { + if (!kept && isAssignmentOf(letKept, letKeeps)) + { + reportPlaceBorrowerAssigned(letKept, varName(letKeeps), place); + } + else + { + reportPlaceBorrowEscapes(kept ? kept : letKept, name, place); + } + return false; } @@ -819,6 +857,7 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' borrows " << place << " and cannot be assigned; -mm=own cannot prove that yet"; + signalPassFailure(); + } + void reportPlaceBorrowEscapes(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) { if (quiet) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e803eedac..82a4e21f7 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2407,7 +2407,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_borrow_two_lets own_borrow_nullable own_union_nullable own_union_tagged own_union_optional own_field_borrow own_element_borrow own_container_loop - own_optional_access own_borrow_merge) + own_optional_access own_borrow_merge + own_field_let_borrow own_element_let_borrow) 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. @@ -2468,6 +2469,8 @@ set(own_error_cases "own_err_merge_read_overwritten|borrows .* but is used here after it may be released or overwritten" "own_err_alias_read_popped|borrows .* but is used here after it may be released or overwritten" "own_err_container_read_stored|borrows .* and cannot be stored, returned or captured" + "own_err_field_let_overwritten|borrows .* but is used here after it may be released or overwritten" + "own_err_field_let_assigned|borrows .* and cannot be assigned" "own_err_delete|'delete' is not supported by -mm=own yet") foreach(own_error_case ${own_error_cases}) string(FIND "${own_error_case}" "|" own_error_sep) diff --git a/tslang/test/tester/own/own_element_let_borrow.ts b/tslang/test/tester/own/own_element_let_borrow.ts new file mode 100644 index 000000000..e4d549f85 --- /dev/null +++ b/tslang/test/tester/own/own_element_let_borrow.ts @@ -0,0 +1,35 @@ +// -mm=own, phase 3: `let e = arr[j]` in a loop borrows each element in turn. +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 main() { + let t: number = 0; + for (let i = 0; i < 1000; i++) { + const arr: C[] = []; + for (let j = 0; j < 10; j++) { + arr.push(new C(j)); + } + + for (let j = 0; j < 10; j++) { + let e = arr[j]; + churn(); + t += e.x; + } + + arr.pop(); + } + + assert(t == 45000); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_field_let_assigned.ts b/tslang/test/tester/own/own_err_field_let_assigned.ts new file mode 100644 index 000000000..36755120d --- /dev/null +++ b/tslang/test/tester/own/own_err_field_let_assigned.ts @@ -0,0 +1,28 @@ +// -mm=own, phase 3 rejects: `c` borrows `h.c`, so it cannot be assigned. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const h = new H(); + let c = h.c; + print(c.x); + c = new C(5); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_field_let_overwritten.ts b/tslang/test/tester/own/own_err_field_let_overwritten.ts new file mode 100644 index 000000000..a1cb3f024 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_let_overwritten.ts @@ -0,0 +1,29 @@ +// -mm=own, phase 3 rejects: `let c = h.c` borrows the field, which is overwritten before `c` is +// read. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + const h = new H(); + let c = h.c; + h.c = new C(5); + churn(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_field_let_borrow.ts b/tslang/test/tester/own/own_field_let_borrow.ts new file mode 100644 index 000000000..487bdc3a6 --- /dev/null +++ b/tslang/test/tester/own/own_field_let_borrow.ts @@ -0,0 +1,38 @@ +// -mm=own, phase 3: `let c = h.c` borrows the field - no reference taken, none given back - and +// its uses end before the overwrite. +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); + } +} + +class H { + c: C = new C(0); + n: number = 0; + s: string = "a"; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + const h = new H(); + h.c = new C(i % 10); + h.c.v.push(i % 10); + let c = h.c; + churn(); + t += c.x + c.v.length; + h.c = new C(1); + t += h.c.x; + } + + assert(t == 650000); + print("done."); +} From 17871933ab293a36af11c580741e09aa77c9d18d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 15:46:51 +0100 Subject: [PATCH 5/6] -mm=own phase 3: results Spec section 14: views, reads out of containers as borrows, let receivers of them; measured flat like rc; teeth; corpus 252 -> 272 of 564; any boxing moved to phase 4 with the rc box leak it found; known limits for phase 4. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 124 +++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) 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 2006ba8d8..3f20727d5 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,8 +2,9 @@ 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) merged as #401 (results §12); phase 2 (borrows for locals) on branch -`own-phase-2` (results §13). Plans in `docs/superpowers/plans/`. +(moves by reachability) merged as #401 (results §12); phase 2 (borrows for locals) merged as #402 +(results §13); phase 3 (containers and unions) on branch `own-phase-3` (results §14). Plans in +`docs/superpowers/plans/`. ## 1. Purpose @@ -291,7 +292,8 @@ Each phase is a PR series leaving `main` green. compile, run and reclaim with no counting. First measured numbers. 1. **Move inference** (§4.2 step 3, first verdict) and use-after-move. 2. **Borrow inference** for locals and temporaries: tenures, dropping mutations, loop rule. -3. **Containers**: stores as moves, reads as borrows; `any`/union boxing. +3. **Containers**: stores as moves, reads as borrows; `any`/union boxing. As built (§14): unions + and reads; `any` moved to phase 4. 4. **Signature pass**: owned-by-callee parameters, borrowed-from-argument results, export beside the marker and import on the other side. 5. **Closures**: escaping vs non-escaping, cells. @@ -655,3 +657,119 @@ Other negatives: 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). + +## 14. Phase 3 results, 2026-09-29 + +Plan: `docs/superpowers/plans/2026-09-29-own-phase-3.md`. Only `OwnershipInferencePass.cpp` +changed. + +### 14.1 What phase 3 accepts + +**Views.** An op whose result is the block its operand holds is transparent. The pass strips it to +its root wherever it names a value, and looks through it wherever it lists a value's uses. The +views are: + +- a `ts.Cast` between class, union and optional types; +- `ts.CreateUnionInstance` and `ts.GetValueFromUnionOp`; +- `ts.OptionalValue` and `ts.Value`. + +So `let u: C | null = new C(i)`, `let u: C | string = c` and `let u: C | undefined = ...` are one +owner, not a second reference. An empty optional (`u = undefined`, `ts.OptionalUndef`) holds no +block, like `null` widened. Testing a value is a read, not a taker: `if (d)`, `d?.x`, `typeof u`, +and a cast to `boolean`. + +**Reads out of containers are borrows (§2.3).** `const c = h.c`, `arr[i]`, and every temporary +read of a heap value through a `ts.PropertyRef` or `ts.ElementRef` own nothing. Before phase 3 +nothing bounded them, and `const c = h.c; h.c = new C(); c.x` compiled into a read of freed +memory. rc has the same hole. + +A read may not be kept: storing, pushing, returning or retaining it is +`'c' borrows 'h.c' and cannot be stored, returned or captured`. None of its uses, and no use of +anything it produces, may be reachable from a *drop*: + +- an overwrite (`ts.ReleaseSlot`) of any place on the way from the read to its root. Places are + matched by position and reference type, which two references to one field of one object always + share; an unrelated field that happens to match only adds an error; +- a `pop`, `shift`, `splice` or `length =` of an array whose element type is on the way; +- an end of the root: a local's releases, assignments, and every declaration or retain that may + move it away; a made value's releases and every use that takes it; a global's assignments; +- a call. A call drops the read if it is given something derived from the root that the read did + not produce (`reset(h)` drops `h.c`; `use(c)` does not). Where the root is a parameter, `this` + or a global, **any** call drops it: the callee may reach the owner through a global. + +A path back to a use through the read itself is a new borrow, so reading `h.c` at the top of a +loop body and overwriting it at the bottom is fine. The error is +`'c' borrows 'h.c' but is used here after it may be released or overwritten`, with a note at the +drop. + +A borrow is followed through what hands the same value on. That covers casts and views, the +argument of a block a branch passes it to (`n.c ?? m.c`), and the reads of a local that owns +nothing and is assigned it (`for (x of arr)` into an outer `let x` declared without a value). + +**A `let` declared from a read borrows it**, as phase 2's borrowers do: `let c = h.c`, +`let e = arr[j]`. Its `ts.RetainSlot` and `ts.ReleaseSlot`s go, and its reads join the read's +uses. Assigning it is `'c' borrows 'h.c' and cannot be assigned`. + +### 14.2 Measured + +1M iterations (100K calls), AOT, `measure.ps1`, at `-O3` and `-O1` (identical within 4%): + +| program | gc | rc | none | own | +| --- | --- | --- | --- | --- | +| `own_field_borrow` | 6.5 | 4.8 | 562.2 | 4.8 | +| `own_field_let_borrow` | 6.5 | 4.8 | 546.6 | 4.8 | +| `own_union_nullable` | 6.6 | 4.8 | 546.6 | 4.8 | +| `own_union_tagged` | 6.6 | 4.8 | 546.6 | 4.8 | +| `own_element_borrow` (10K) | 6.5 | 4.7 | 11.0 | 4.7 | + +### 14.3 Teeth + +- With a move's releases kept, the three union positives fail with heap corruption (6 of 6 runs). +- With a place-read `let`'s releases kept, both `let` positives fail the same way (4 of 4). +- A folded `const` read erases nothing, so it has nothing to disable. Its teeth are the negatives, + which compiled before this phase: five of them read freed memory under rc and own (`999` for + `0`). + +### 14.4 Tests and the corpus + +- **Positives:** `own_union_nullable`, `own_union_tagged`, `own_union_optional`, + `own_optional_access`, `own_field_borrow`, `own_element_borrow`, `own_container_loop`, + `own_borrow_merge`, `own_field_let_borrow`, `own_element_let_borrow`. +- **Negatives:** `own_err_union_moved`, `own_err_field_read_overwritten`, + `own_err_element_read_overwritten`, `own_err_element_read_popped`, + `own_err_element_read_loop_pop`, `own_err_field_read_owner_assigned`, + `own_err_field_read_owner_moved`, `own_err_field_read_call`, `own_err_param_read_call`, + `own_err_global_read_assigned`, `own_err_merge_read_overwritten`, `own_err_alias_read_popped`, + `own_err_container_read_stored`, `own_err_field_let_overwritten`, `own_err_field_let_assigned`. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`): 252 of 564 compiled before, +272 after. 23 files now compile. 3 stopped: + +- `export_class_abstract`; +- `export_class_abstract_virtual_dispatch`; +- `export_class_implements_interface_abstract`. + +Each has ``${this.color} area=${this.area()}``, a read of a field of `this` that is still used +after a call: the any-call rule above. + +### 14.5 Deferred: `any` + +§7 put `any` boxing in phase 3; it moves to phase 4. Unboxing is a call to a generated +`___unbox`, which retains the payload inside and returns it +1. `___cast` allocates on one +branch and returns the payload on the other. Treating either as a borrow is phase 4's +borrowed-from-argument result. + +An rc bug found here belongs with it: `const a: any = new C(i)` never releases the box +(`measure.ps1`: rc 66.2 MB, gc 6.1 MB). The boxing cast is +0, and a folded `const` takes no +reference. + +### 14.6 Known limits (the input to phase 4) + +- Reads of a parameter's or `this`'s fields across any call, including + ``${this.color} ${this.area()}``. Callee facts decide which calls may reach the owner. +- Returning a field (`get c() { return this.c; }`): a borrowed-from-argument result. +- `any` boxing and unboxing (§14.5). +- A read out of a container reached through an interface (`ts.InterfaceSymbolRef`), which is not a + `ts.PropertyRef` and is not bounded yet. +- From §13.5: borrow chains through assigned borrowers, moves after the last use of every + borrower, and a temporary taken by a `let` and consumed elsewhere. From c3fa747937aeb4027734917245e76148425bc144 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 29 Sep 2026 16:11:43 +0100 Subject: [PATCH 6/6] -mm=own phase 3: review fixes - a merged value is made again where it merges The final review found three holes in following a borrow past a merge: - a borrow stored into a local that owns nothing and read on a later iteration, after the overwrite, was taken for the next iteration's borrow and compiled into a read of freed memory; - a merge of two reads was rejected after any earlier call, through the other branch; - a read through a merge of two owning locals saw one root it did not own, with no ends, and compiled an assignment of either local into a read of freed memory. Each borrowed use now carries where its value is made again: the read or the borrowing let for its own uses, the merge's block past a merge, and a local's declaration and assignments past a local that owns nothing. The branch or store that hands the value on is a use bounded by the read. reachableAfter takes a set of kills. A container reached through a merge has each merged value as a root. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 18 +- .../lib/TypeScript/OwnershipInferencePass.cpp | 389 ++++++++++++------ tslang/test/tester/CMakeLists.txt | 4 +- .../tester/own/own_borrow_merge_after_call.ts | 72 ++++ .../tester/own/own_err_alias_read_stale.ts | 37 ++ .../tester/own/own_err_merge_root_assigned.ts | 41 ++ 6 files changed, 431 insertions(+), 130 deletions(-) create mode 100644 tslang/test/tester/own/own_borrow_merge_after_call.ts create mode 100644 tslang/test/tester/own/own_err_alias_read_stale.ts create mode 100644 tslang/test/tester/own/own_err_merge_root_assigned.ts 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 3f20727d5..dceed5a7d 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 @@ -705,6 +705,18 @@ drop. A borrow is followed through what hands the same value on. That covers casts and views, the argument of a block a branch passes it to (`n.c ?? m.c`), and the reads of a local that owns nothing and is assigned it (`for (x of arr)` into an outer `let x` declared without a value). +Past a merge or such a local, the value is made again where that is replaced: the merge's block, +or the local's assignments. The branch or store that hands it on is itself a use, bounded by the +read. The final review found the first version, which kept the read as the only point where the +value is made again, wrong both ways: + +- It compiled a local assigned on one iteration and read on the next, after the overwrite, into a + read of freed memory. +- It rejected `c ? h.c : h.d` after any earlier call, through the other branch. + +A container reached through a merge (`(i > 0 ? a : b).c`) has each merged value as a root, and +each root's ends bound the read. The first version saw one root it did not own, with no ends, and +compiled `a = z` between the read and its use into a read of freed memory. **A `let` declared from a read borrows it**, as phase 2's borrowers do: `let c = h.c`, `let e = arr[j]`. Its `ts.RetainSlot` and `ts.ReleaseSlot`s go, and its reads join the read's @@ -734,13 +746,15 @@ uses. Assigning it is `'c' borrows 'h.c' and cannot be assigned`. - **Positives:** `own_union_nullable`, `own_union_tagged`, `own_union_optional`, `own_optional_access`, `own_field_borrow`, `own_element_borrow`, `own_container_loop`, - `own_borrow_merge`, `own_field_let_borrow`, `own_element_let_borrow`. + `own_borrow_merge`, `own_field_let_borrow`, `own_element_let_borrow`, and from the final review + `own_borrow_merge_after_call` (also a push onto the array a `for...of` walks). - **Negatives:** `own_err_union_moved`, `own_err_field_read_overwritten`, `own_err_element_read_overwritten`, `own_err_element_read_popped`, `own_err_element_read_loop_pop`, `own_err_field_read_owner_assigned`, `own_err_field_read_owner_moved`, `own_err_field_read_call`, `own_err_param_read_call`, `own_err_global_read_assigned`, `own_err_merge_read_overwritten`, `own_err_alias_read_popped`, - `own_err_container_read_stored`, `own_err_field_let_overwritten`, `own_err_field_let_assigned`. + `own_err_container_read_stored`, `own_err_field_let_overwritten`, `own_err_field_let_assigned`, + and from the final review `own_err_alias_read_stale`, `own_err_merge_root_assigned`. Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`): 252 of 564 compiled before, 272 after. 23 files now compile. 3 stopped: diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index ea88fc1f9..191a4d9ab 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -320,9 +320,10 @@ class OwnershipInferencePass : public mlir::PassWrapper &toErase) { auto name = varName(borrower); - llvm::SmallVector uses; + llvm::SmallVector uses; llvm::SmallVector bookkeeping; - if (auto *kept = collectBorrowerUses(borrower, uses, bookkeeping)) + mlir::Operation *declaration = borrower; + if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) { if (isAssignmentOf(kept, borrower)) { @@ -338,11 +339,11 @@ class OwnershipInferencePass : public mlir::PassWrapper kills; + }; + // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins - // `uses`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, which a borrow erases. - // Answers the first use that would keep what the slot holds - an assignment, or anything - // else that is not a read - or null. - mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::SmallVectorImpl &uses, + // `uses`, made again at `kills`; its `ts.RetainSlot` and `ts.ReleaseSlot`s are `bookkeeping`, + // which a borrow erases. Answers the first use that would keep what the slot holds - an + // assignment, or anything else that is not a read - or null. + mlir::Operation *collectBorrowerUses(mlir_ts::VariableOp borrower, llvm::ArrayRef kills, + llvm::SmallVectorImpl &uses, llvm::SmallVectorImpl &bookkeeping) { for (auto *user : borrower.getResult().getUsers()) @@ -373,8 +385,8 @@ class OwnershipInferencePass : public mlir::PassWrapper &values) + // whether the use is one of those, even where nothing comes out of it. Where the value lands + // somewhere that is replaced - a block argument, a local - `renewedAt` gets what replaces it: + // the block's entry, the local's declaration and assignments. + static bool passesOn(mlir::OpOperand &use, llvm::SmallVectorImpl &values, + llvm::SmallVectorImpl *renewedAt = nullptr) { auto *user = use.getOwner(); if (mlir::isa(user) || isView(user)) @@ -407,6 +422,11 @@ class OwnershipInferencePass : public mlir::PassWrapperpush_back(&argument->getOwner()->front()); + } + return true; } @@ -418,12 +438,21 @@ class OwnershipInferencePass : public mlir::PassWrapper(); varOp && !isOwningVariable(varOp) && !varOp.getCaptured().value_or(false)) { + if (renewedAt) + { + renewedAt->push_back(varOp); + } + for (auto *varUser : varOp.getResult().getUsers()) { if (auto loadOp = mlir::dyn_cast(varUser)) { values.push_back(loadOp.getResult()); } + else if (renewedAt && isAssignmentOf(varUser, varOp)) + { + renewedAt->push_back(varUser); + } } return true; @@ -434,46 +463,59 @@ class OwnershipInferencePass : public mlir::PassWrapper &uses, - llvm::function_ref isBorrower = nullptr) - { - llvm::SmallVector> values{{start, true}}; + // into the block, joins `uses`, made again at `kills`. The value itself - through what passesOn + // hands it on to - 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 borrow too. A value that cannot point + // anywhere (a number, a boolean) ends the walk. Where passesOn hands the value to something + // that is replaced, the uses beyond are made again where it is replaced instead; the op that + // hands it on is itself a use, bounded by `kills`. `isBorrower` accepts a use of the value + // itself that the caller follows on its own (a borrowing `let`), made again at the kills it is + // given. Answers the first use that would keep the value. + mlir::Operation *walkBorrowed( + mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, + llvm::function_ref)> isBorrower = nullptr) + { + struct Pending + { + mlir::Value value; + bool borrowed; + llvm::SmallVector kills; + }; + + llvm::SmallVector values; + values.push_back({start, true, {kills.begin(), kills.end()}}); llvm::SmallPtrSet seen{start}; while (!values.empty()) { - auto [current, borrowed] = values.pop_back_val(); - for (auto &use : current.getUses()) + auto pending = values.pop_back_val(); + for (auto &use : pending.value.getUses()) { auto *user = use.getOwner(); - uses.push_back(user); - if (borrowed) + uses.push_back({user, pending.kills}); + if (pending.borrowed) { llvm::SmallVector same; - if (passesOn(use, same)) + llvm::SmallVector renewedAt; + if (passesOn(use, same, &renewedAt)) { for (auto value : same) { if (mayPointInto(value) && seen.insert(value).second) { - values.push_back({value, true}); + values.push_back({value, true, renewedAt.empty() ? pending.kills : renewedAt}); } } continue; } - if (isBorrower && isBorrower(user)) + if (isBorrower && isBorrower(user, pending.kills)) { continue; } - if (mlir::isa(user) || !isBorrow(user, current)) + if (mlir::isa(user) || !isBorrow(user, pending.value)) { return user; } @@ -483,7 +525,7 @@ class OwnershipInferencePass : public mlir::PassWrapper> places; - mlir::Value root; - Kind kind = NotOwned; + llvm::SmallVector> roots; + + bool owned() const + { + return llvm::none_of(roots, [](auto &root) { return root.second == NotOwned; }); + } }; Chain chainOf(mlir_ts::LoadOp read) { Chain chain; - auto ref = read.getReference(); - while (true) + llvm::SmallVector refs{read.getReference()}; + llvm::SmallPtrSet seen; + while (!refs.empty()) { + auto ref = refs.pop_back_val(); mlir::Value base; if (auto propertyRefOp = ref.getDefiningOp()) { @@ -570,117 +619,178 @@ class OwnershipInferencePass : public mlir::PassWrapper bases{rootOf(base)}; + while (!bases.empty()) { - ref = base; // a field of a value-type field: the same object - continue; - } + base = bases.pop_back_val(); + if (!seen.insert(base).second) + { + continue; + } - if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) - { - ref = loadOp.getReference(); - continue; - } + if (isPlace(base)) + { + refs.push_back(base); // a field of a value-type field: the same object + continue; + } - chain.root = base; - if (base.getDefiningOp()) - { - chain.kind = Chain::Slot; + if (auto loadOp = base.getDefiningOp(); loadOp && isPlace(loadOp.getReference())) + { + refs.push_back(loadOp.getReference()); + continue; + } + + if (auto argument = mlir::dyn_cast(base); argument && mergedInto(argument, bases)) + { + continue; + } + + if (base.getDefiningOp()) + { + chain.roots.push_back({base, Chain::Slot}); + } + else if (auto slotLoad = slotLoadOf(base)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else + { + chain.roots.push_back({base, isFresh(base) ? Chain::Owned : Chain::NotOwned}); + } } - else if (auto slotLoad = slotLoadOf(base)) + } + + return chain; + } + + // The values the branches into its block pass for `argument`, each seen through its views. + // False, adding nothing, when a way in is not a branch that passes one - the entry block's + // parameters, a region's arguments. + static bool mergedInto(mlir::BlockArgument argument, llvm::SmallVectorImpl &values) + { + auto *block = argument.getOwner(); + if (block->isEntryBlock()) + { + return false; + } + + llvm::SmallVector found; + for (auto *predecessor : block->getPredecessors()) + { + auto branchOp = mlir::dyn_cast(predecessor->getTerminator()); + if (!branchOp) { - chain.root = slotLoad.getReference(); - chain.kind = Chain::Slot; + return false; } - else if (isFresh(base)) + + for (unsigned index = 0; index < branchOp->getNumSuccessors(); ++index) { - chain.kind = Chain::Owned; - } + if (branchOp->getSuccessor(index) != block) + { + continue; + } - return chain; + auto passed = branchOp.getSuccessorOperands(index)[argument.getArgNumber()]; + if (!passed) + { + return false; + } + + found.push_back(rootOf(passed)); + } } + + values.append(found.begin(), found.end()); + return true; } - // Where the root stops holding what the chain reads: a local's releases and assignments and + // Where the roots stop holding what the chain reads: a local's releases and assignments and // every declaration or retain that may move it away; a made value's releases and every use // that takes it; a global's assignments. llvm::SmallVector rootEnds(const Chain &chain) { llvm::SmallVector ends; - if (chain.kind == Chain::Slot) + for (auto [root, kind] : chain.roots) { - for (auto *user : chain.root.getUsers()) + if (kind == Chain::Slot) { - if (mlir::isa(user)) - { - ends.push_back(user); - } - else if (auto loadOp = mlir::dyn_cast(user)) - { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value) { - auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use) || (varOp && isOwningVariable(varOp))) - { - ends.push_back(use); - } - }); - } - } - } - else if (chain.kind == Chain::Owned) - { - forEachUse(chain.root, [&](mlir::Operation *use, mlir::Value used) { - if (mlir::isa(use) || !isBorrow(use, used)) + for (auto *user : root.getUsers()) { - ends.push_back(use); - } - }); - } - else if (auto loadOp = chain.root.getDefiningOp()) - { - if (auto global = loadOp.getReference().getDefiningOp()) - { - getFunction().walk([&](mlir::Operation *op) { - mlir::Value slot; - if (auto releaseSlotOp = mlir::dyn_cast(op)) + if (mlir::isa(user)) { - slot = releaseSlotOp.getSlot(); + ends.push_back(user); } - else if (auto storeOp = mlir::dyn_cast(op)) + else if (auto loadOp = mlir::dyn_cast(user)) { - slot = storeOp.getReference(); + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value) { + auto varOp = mlir::dyn_cast(use); + if (mlir::isa(use) || (varOp && isOwningVariable(varOp))) + { + ends.push_back(use); + } + }); } - - auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); - if (other && other.getGlobalName() == global.getGlobalName()) + } + } + else if (kind == Chain::Owned) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) { - ends.push_back(op); + ends.push_back(use); } }); } + else if (auto loadOp = root.getDefiningOp()) + { + if (auto global = loadOp.getReference().getDefiningOp()) + { + getFunction().walk([&](mlir::Operation *op) { + mlir::Value slot; + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + slot = releaseSlotOp.getSlot(); + } + else if (auto storeOp = mlir::dyn_cast(op)) + { + slot = storeOp.getReference(); + } + + auto other = slot ? slot.getDefiningOp() : mlir_ts::AddressOfOp(); + if (other && other.getGlobalName() == global.getGlobalName()) + { + ends.push_back(op); + } + }); + } + } } return ends; } - // What a call has to be given to reach the root: the root, and where the root is a local + // What a call has to be given to reach a root: the root, and where the root is a local // declared from another (a borrow, or a move that left the other dead), that one too. static llvm::SmallVector rootSources(const Chain &chain) { - llvm::SmallVector sources{chain.root}; - auto value = chain.root; - while (auto varOp = value.getDefiningOp()) + llvm::SmallVector sources; + for (auto &root : chain.roots) { - if (!varOp.getInitializer() || sources.size() > 8) + auto value = root.first; + sources.push_back(value); + for (auto steps = 0; steps < 8; ++steps) { - break; - } + auto varOp = value.getDefiningOp(); + if (!varOp || !varOp.getInitializer()) + { + break; + } - auto initializer = rootOf(varOp.getInitializer()); - auto slotLoad = slotLoadOf(initializer); - value = slotLoad ? slotLoad.getReference() : initializer; - sources.push_back(value); + auto initializer = rootOf(varOp.getInitializer()); + auto slotLoad = slotLoadOf(initializer); + value = slotLoad ? slotLoad.getReference() : initializer; + sources.push_back(value); + } } return sources; @@ -755,7 +865,7 @@ class OwnershipInferencePass : public mlir::PassWrapper uses; + llvm::SmallVector uses; llvm::SmallVector bookkeeping; mlir_ts::VariableOp letKeeps; mlir::Operation *letKept = nullptr; - auto *kept = walkBorrowed(read.getResult(), uses, [&](mlir::Operation *user) { + mlir::Operation *readOp = read; + auto *kept = walkBorrowed(read.getResult(), readOp, uses, [&](mlir::Operation *user, + llvm::ArrayRef kills) { auto borrower = borrowerOf(user); if (!borrower || borrower.getOperation() != user) { @@ -803,7 +915,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(kill) : llvm::ArrayRef()); + } + + // The same, where a path through any of `kills` is a new value. A use that is itself a kill + // runs after the value is made again. + bool reachableAfter(mlir::Operation *from, mlir::Operation *to, llvm::ArrayRef kills) + { + if (llvm::is_contained(kills, to)) + { + return false; + } + auto &body = getFunction().getBody(); from = body.findAncestorOpInRegion(*from); to = body.findAncestorOpInRegion(*to); - if (kill) + llvm::SmallVector killAt; + for (auto *kill : kills) { - kill = body.findAncestorOpInRegion(*kill); + if (auto *top = body.findAncestorOpInRegion(*kill)) + { + killAt.push_back(top); + } } if (!from || !to) @@ -1416,17 +1544,24 @@ class OwnershipInferencePass : public mlir::PassWrappergetBlock() == block && (!after || after->isBeforeInBlock(kill)) && + (!before || kill->isBeforeInBlock(before)); + }); + }; + auto *fromBlock = from->getBlock(); auto *toBlock = to->getBlock(); - auto killIn = [&](mlir::Block *block) { return kill && kill->getBlock() == block; }; - if (fromBlock == toBlock && from->isBeforeInBlock(to)) { - return !(killIn(fromBlock) && from->isBeforeInBlock(kill) && kill->isBeforeInBlock(to)); + return !killsIn(fromBlock, from, to); } - // every way out of this block runs `kill` first - if (killIn(fromBlock) && from->isBeforeInBlock(kill)) + // every way out of this block runs a kill first + if (killsIn(fromBlock, from, nullptr)) { return false; } @@ -1441,12 +1576,12 @@ class OwnershipInferencePass : public mlir::PassWrapperisBeforeInBlock(kill))) + if (block == toBlock && !killsIn(block, nullptr, to)) { return true; } - if (killIn(block)) + if (killsIn(block, nullptr, nullptr)) { continue; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 82a4e21f7..a480fee64 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2408,7 +2408,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_union_nullable own_union_tagged own_union_optional own_field_borrow own_element_borrow own_container_loop own_optional_access own_borrow_merge - own_field_let_borrow own_element_let_borrow) + own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call) 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. @@ -2468,6 +2468,8 @@ set(own_error_cases "own_err_global_read_assigned|borrows .* but is used here after it may be released or overwritten" "own_err_merge_read_overwritten|borrows .* but is used here after it may be released or overwritten" "own_err_alias_read_popped|borrows .* but is used here after it may be released or overwritten" + "own_err_alias_read_stale|borrows .* but is used here after it may be released or overwritten" + "own_err_merge_root_assigned|borrows .* but is used here after it may be released or overwritten" "own_err_container_read_stored|borrows .* and cannot be stored, returned or captured" "own_err_field_let_overwritten|borrows .* but is used here after it may be released or overwritten" "own_err_field_let_assigned|borrows .* and cannot be assigned" diff --git a/tslang/test/tester/own/own_borrow_merge_after_call.ts b/tslang/test/tester/own/own_borrow_merge_after_call.ts new file mode 100644 index 000000000..8dd0c9787 --- /dev/null +++ b/tslang/test/tester/own/own_borrow_merge_after_call.ts @@ -0,0 +1,72 @@ +// -mm=own, phase 3: a merge of two reads borrows from each. Something that may destroy them before +// the reads - the container's own constructor, a call - does not reach a use through the merge, +// since each branch reads afresh. A push onto the array a `for...of` walks drops nothing. +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); + } +} + +class H { + c: C = new C(0); + d: C = new C(7); +} + +class N { + c: C | null = null; +} + +function noop() { +} + +function pick(i: number) { + const h = new H(); + const c = i % 2 == 0 ? h.c : h.d; + churn(); + return c.x; +} + +function either(n: N, m: N) { + noop(); + const c = n.c ?? m.c; + if (c) { + return c.x; + } + + return 0; +} + +function main() { + let t: number = 0; + const h = new H(); + for (let i = 0; i < 10000; i++) { + t += pick(i); + const m = new N(); + m.c = new C(3); + t += either(new N(), m); + const c = i % 3 == 0 ? h.c : h.d; + t += c.x; + h.c = new C(1); + h.d = new C(2); + } + + const arr: C[] = [new C(1)]; + for (const e of arr) { + t += e.x; + if (arr.length < 500) { + arr.push(new C(1)); + } + } + + churn(); + assert(t == 82165); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_alias_read_stale.ts b/tslang/test/tester/own/own_err_alias_read_stale.ts new file mode 100644 index 000000000..415dfeb27 --- /dev/null +++ b/tslang/test/tester/own/own_err_alias_read_stale.ts @@ -0,0 +1,37 @@ +// -mm=own, phase 3 rejects: `x` owns nothing and keeps the first iteration's `h.c`; the next +// iteration reads `h.c` again, but `x` was not assigned on that path, so it still names the value +// the overwrite destroyed. +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); + } +} + +class H { + c: C = new C(0); +} + +function main() { + const h = new H(); + h.c = new C(5); + let x: C; + for (let i = 0; i < 3; i++) { + const t = h.c; + if (i == 0) { + x = t; + } else { + print(x.x); + } + + h.c = new C(i + 10); + churn(); + } +} diff --git a/tslang/test/tester/own/own_err_merge_root_assigned.ts b/tslang/test/tester/own/own_err_merge_root_assigned.ts new file mode 100644 index 000000000..ae1902cc9 --- /dev/null +++ b/tslang/test/tester/own/own_err_merge_root_assigned.ts @@ -0,0 +1,41 @@ +// -mm=own, phase 3 rejects: `r` is `a` or `b`, so `r.c` borrows from both; assigning `a` destroys +// the old `a` and its `c`. +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); + } +} + +class H { + c: C = new C(0); +} + +function run(i: number) { + let a = new H(); + a.c = new C(5); + let b = new H(); + b.c = new C(6); + let z = new H(); + z.c = new C(7); + const r = i > 0 ? a : b; + const c = r.c; + a = z; + const junk: number[][] = []; + for (let k = 0; k < 64; k++) { + junk.push([999, 999, 999, 999]); + } + + return c.x + junk.length; +} + +function main() { + print(run(1)); +}