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`. 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..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 @@ -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,133 @@ 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). +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 +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`, 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`, + 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: + +- `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. diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 942908237..191a4d9ab 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" @@ -13,6 +14,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" @@ -51,12 +53,17 @@ 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) { 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 +71,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)) @@ -83,10 +90,31 @@ 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()) { - if (isFresh(result) && ownsHeap(result)) + if (!isView(op) && isFresh(result) && ownsHeap(result)) { candidates.insert(result); } @@ -106,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; @@ -184,218 +230,954 @@ class OwnershipInferencePass : public mlir::PassWrapper receivers, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto ends = slotEnds(slot); + mlir_ts::VariableOp someBorrower; + for (auto &receiver : receivers) + { + if (auto borrower = borrowerOf(receiver.retain)) + { + someBorrower = borrower; + tryBorrow(borrower, ends, owner, toErase); + } + } + + for (auto &receiver : receivers) + { + if (borrowerOf(receiver.retain)) + { + continue; + } + + llvm::SetVector unused; + if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + { + reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + } + } + } + + // A `let` that could borrow: owning, declared from a value, holding a reference it took + // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. + static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition) + { + mlir_ts::VariableOp varOp; + if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + { + varOp = retainSlotOp.getSlot().getDefiningOp(); + } + else + { + varOp = mlir::dyn_cast(acquisition); + } + + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || + varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + auto retains = llvm::any_of(varOp.getResult().getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + return retains ? varOp : mlir_ts::VariableOp(); + } + + // Where an owning local stops holding its value: each release of its slot and each store + // into it. + static llvm::SmallVector slotEnds(mlir::Value slot) + { + llvm::SmallVector ends; + for (auto *user : slot.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + + return ends; + } + + // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its + // retain and every release of its slot go, and the owner keeps its own. Sound while no use of + // the borrower can run after any of `ends` - the owner's releases and assignments - and while + // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use + // through the borrower's own declaration is a new borrow, not this one. Reports and returns + // false otherwise. + bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, + llvm::SetVector &toErase) + { + auto name = varName(borrower); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir::Operation *declaration = borrower; + if (auto *kept = collectBorrowerUses(borrower, declaration, uses, bookkeeping)) + { + if (isAssignmentOf(kept, borrower)) + { + reportBorrowerAssigned(kept, name, owner); + } + else + { + reportBorrowEscapes(kept, name, owner); + } + + return false; + } + + for (auto *end : ends) + { + for (auto &use : uses) + { + if (reachableAfter(end, use.op, use.kills)) + { + reportBorrowOutlives(use.op, end, name, owner); + return false; + } + } + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + return true; + } + + // A use of a borrowed value, and where the value it sees is made again: a path from a release + // to the use through one of `kills` reaches a new value, not this one. For the borrow's own + // uses that is the read or the borrowing `let`; for a use through a merge it is the merge's + // block, and through a local that owns nothing, that local's declaration and assignments. + struct BorrowUse + { + mlir::Operation *op; + llvm::SmallVector kills; + }; + + // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins + // `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()) + { + if (mlir::isa(user)) + { + bookkeeping.push_back(user); + continue; + } + + auto readOp = mlir::dyn_cast(user); + if (!readOp) + { + return user; + } + + uses.push_back({user, {kills.begin(), kills.end()}}); + if (auto *kept = walkBorrowed(readOp.getResult(), kills, 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 + // 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)) + { + 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); + if (renewedAt) + { + renewedAt->push_back(&argument->getOwner()->front()); + } + + 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)) + { + 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; + } + } + + 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`, 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 pending = values.pop_back_val(); + for (auto &use : pending.value.getUses()) + { + auto *user = use.getOwner(); + uses.push_back({user, pending.kills}); + if (pending.borrowed) + { + llvm::SmallVector 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, renewedAt.empty() ? pending.kills : renewedAt}); + } + } + + continue; + } + + if (isBorrower && isBorrower(user, pending.kills)) + { + continue; + } + + if (mlir::isa(user) || !isBorrow(user, pending.value)) + { + return user; + } + } + + for (auto result : user->getResults()) + { + if (mayPointInto(result) && seen.insert(result).second) + { + values.push_back({result, false, pending.kills}); + } + } + } + } + + 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 roots, and the roots: the values the function reached the + // first container through. A container reached through a merge (`(c ? a : b).x`) has a root + // on each way in. + 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; + 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; + 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()) + { + 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(); + } + + llvm::SmallVector bases{rootOf(base)}; + while (!bases.empty()) + { + base = bases.pop_back_val(); + if (!seen.insert(base).second) + { + continue; + } + + if (isPlace(base)) + { + refs.push_back(base); // a field of a value-type field: the same object + continue; + } + + 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}); + } + } + } + + 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) + { + return false; + } + + for (unsigned index = 0; index < branchOp->getNumSuccessors(); ++index) + { + if (branchOp->getSuccessor(index) != block) + { + continue; + } + + 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 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; + for (auto [root, kind] : chain.roots) + { + if (kind == Chain::Slot) + { + for (auto *user : 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))) + { + ends.push_back(use); + } + }); + } + } + } + else if (kind == Chain::Owned) + { + forEachUse(root, [&](mlir::Operation *use, mlir::Value used) { + if (mlir::isa(use) || !isBorrow(use, used)) + { + 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 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; + for (auto &root : chain.roots) + { + auto value = root.first; + sources.push_back(value); + for (auto steps = 0; steps < 8; ++steps) + { + 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); + } + } + + 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); + } + } + } + } + + 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.owned()) + { + 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(); + }); + } + + // 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 place = describePlace(read.getReference()); + llvm::StringRef name = ownerName(read.getResult()); + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + mlir_ts::VariableOp letKeeps; + mlir::Operation *letKept = nullptr; + 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) + { + return false; + } + + name = varName(borrower); + if (auto *keeps = collectBorrowerUses(borrower, kills, 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; + } + + auto chain = chainOf(read); + auto outlives = [&](mlir::Operation *drop) { + for (auto &use : uses) + { + // a call's own arguments are read before it runs + if (use.op != drop && reachableAfter(drop, use.op, use.kills)) + { + reportPlaceBorrowOutlives(use.op, 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; + } + + // `'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 path = pathOf(ref); + if (path.empty()) + { + return ref.getDefiningOp() ? "an element" : "a field"; } - decideSlotReceivers(slot, receivers, owner, toErase); + return "'" + path + "'"; } - // More than one receiver of one owning local. Each has to borrow: a borrow pins the owner, so - // none may move it. A receiver that is not a `let` reports its move error - another - // receiver's reads come after it - or, if its move alone would pass, that a borrow pins it. - void decideSlotReceivers(mlir::Value slot, llvm::ArrayRef receivers, llvm::StringRef owner, - llvm::SetVector &toErase) + std::string pathOf(mlir::Value ref) { - auto ends = slotEnds(slot); - mlir_ts::VariableOp someBorrower; - for (auto &receiver : receivers) + mlir::Value base; + std::string step; + if (auto propertyRefOp = ref.getDefiningOp()) { - if (auto borrower = borrowerOf(receiver.retain)) + auto field = fieldName(propertyRefOp); + if (field.empty()) { - someBorrower = borrower; - tryBorrow(borrower, ends, owner, toErase); + return {}; } + + base = propertyRefOp.getObjectRef(); + step = "." + field; + } + else if (auto elementRefOp = ref.getDefiningOp()) + { + base = elementRefOp.getArray(); + step = "[]"; + } + else + { + return {}; } - for (auto &receiver : receivers) + base = rootOf(base); + std::string prefix; + if (isPlace(base)) { - if (borrowerOf(receiver.retain)) + prefix = pathOf(base); + } + else if (auto loadOp = base.getDefiningOp()) + { + auto from = loadOp.getReference(); + if (isPlace(from)) { - continue; + prefix = pathOf(from); } - - llvm::SetVector unused; - if (checkSlotMove(receiver.retain, receiver.value, receiver.load, unused)) + else if (auto varOp = from.getDefiningOp()) { - reportMovedWhileBorrowed(receiver.retain, owner, someBorrower ? varName(someBorrower) : "another variable"); + prefix = varName(varOp).str(); + } + else if (auto global = from.getDefiningOp()) + { + prefix = global.getGlobalName().str(); } } - } - - // A `let` that could borrow: owning, declared from a value, holding a reference it took - // itself (a `ts.RetainSlot`, not the value's own consumed one), and not captured. - static mlir_ts::VariableOp borrowerOf(mlir::Operation *acquisition) - { - mlir_ts::VariableOp varOp; - if (auto retainSlotOp = mlir::dyn_cast(acquisition)) + else if (auto varOp = base.getDefiningOp()) { - varOp = retainSlotOp.getSlot().getDefiningOp(); + prefix = varName(varOp).str(); } else { - varOp = mlir::dyn_cast(acquisition); + prefix = ownerName(base).str(); } - if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || - varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME) || varOp.getCaptured().value_or(false)) + // 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 {}; } - auto retains = llvm::any_of(varOp.getResult().getUsers(), - [](mlir::Operation *user) { return mlir::isa(user); }); - return retains ? varOp : mlir_ts::VariableOp(); + return prefix + step; } - // Where an owning local stops holding its value: each release of its slot and each store - // into it. - static llvm::SmallVector slotEnds(mlir::Value slot) + static std::string fieldName(mlir_ts::PropertyRefOp propertyRefOp) { - llvm::SmallVector ends; - for (auto *user : slot.getUsers()) + auto type = propertyRefOp.getObjectRef().getType(); + if (auto refType = mlir::dyn_cast(type)) { - if (mlir::isa(user)) - { - ends.push_back(user); - } + type = refType.getElementType(); } - return ends; - } + if (auto classType = mlir::dyn_cast(type)) + { + type = classType.getStorageType(); + } - // A borrow (spec 2.2, verdict 2): `borrower` takes no reference and gives none back, so its - // retain and every release of its slot go, and the owner keeps its own. Sound while no use of - // the borrower can run after any of `ends` - the owner's releases and assignments - and while - // nothing it holds is kept: not stored, returned, retained or assigned. A path back to a use - // through the borrower's own declaration is a new borrow, not this one. Reports and returns - // false otherwise. - bool tryBorrow(mlir_ts::VariableOp borrower, llvm::ArrayRef ends, llvm::StringRef owner, - llvm::SetVector &toErase) - { - auto slot = borrower.getResult(); - auto name = varName(borrower); - llvm::SmallVector uses; - llvm::SmallVector bookkeeping; - for (auto *user : slot.getUsers()) + auto position = propertyRefOp.getPosition(); + mlir::Attribute id; + if (auto storageType = mlir::dyn_cast(type); storageType && position < storageType.size()) { - if (mlir::isa(user)) - { - bookkeeping.push_back(user); - continue; - } + id = storageType.getId(position); + } + else if (auto tupleType = mlir::dyn_cast(type); tupleType && position < tupleType.size()) + { + id = tupleType.getId(position); + } - if (mlir::isa(user)) - { - reportBorrowerAssigned(user, name, owner); - return false; - } + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.str(); + } - auto readOp = mlir::dyn_cast(user); - if (!readOp) - { - reportBorrowEscapes(user, name, owner); - return false; - } + if (auto name = mlir::dyn_cast_or_null(id)) + { + return name.getValue().str(); + } - uses.push_back(user); + return {}; + } - // 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()) + // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it + // has a ts.DebugVariable, whose location carries the name. + static llvm::StringRef ownerName(mlir::Value value) + { + llvm::StringRef name = "this value"; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) { - auto [current, borrowed] = values.pop_back_val(); - for (auto *valueUser : current.getUsers()) - { - uses.push_back(valueUser); - if (borrowed) - { - if (auto castOp = mlir::dyn_cast(valueUser)) - { - if (mayPointInto(castOp.getResult()) && seen.insert(castOp.getResult()).second) - { - values.push_back({castOp.getResult(), true}); - } - - continue; - } + name = nameAt(user->getLoc()); + } + }); - if (mlir::isa(valueUser) || !isBorrow(valueUser, current)) - { - reportBorrowEscapes(valueUser, name, owner); - return false; - } - } + return name; + } - for (auto result : valueUser->getResults()) - { - if (mayPointInto(result) && seen.insert(result).second) - { - values.push_back({result, false}); - } - } - } - } + // 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; } - for (auto *end : ends) + auto castOp = mlir::dyn_cast_or_null(op); + if (!castOp) { - for (auto *use : uses) - { - if (reachableAfter(end, use, borrower)) - { - reportBorrowOutlives(use, end, name, owner); - return false; - } - } + return false; } - toErase.insert(bookkeeping.begin(), bookkeeping.end()); - return true; + auto keeps = [](mlir::Type type) { + return mlir::isa(type); + }; + return keeps(castOp.getIn().getType()) && keeps(castOp.getType()); } - // 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) + // The block a value is a view of. + static mlir::Value rootOf(mlir::Value value) { - auto type = value.getType(); - return mlir::isa(type) || - ownsHeap(value); + while (auto *def = value.getDefiningOp()) + { + if (!isView(def)) + { + break; + } + + value = def->getOperand(0); + } + + return value; } - // The name of a temporary that owns: a folded `const` has no ts.Variable, but under --di it - // has a ts.DebugVariable, whose location carries the name. - static llvm::StringRef ownerName(mlir::Value value) + // 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) { - for (auto *user : value.getUsers()) + llvm::SmallVector values{root}; + while (!values.empty()) { - if (mlir::isa(user)) + auto value = values.pop_back_val(); + for (auto &use : value.getUses()) { - return nameAt(user->getLoc()); + auto *user = use.getOwner(); + if (isView(user) && use.getOperandNumber() == 0) + { + values.push_back(user->getResult(0)); + continue; + } + + each(user, value); } } - - return "this value"; } - // The value a retain acquires: a Retain's operand, or a RetainSlot's variable's initializer. - // None for a variable with no initializer - its storage was hoisted in front of a try and its - // value arrives by a store this phase does not follow. + // 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 +1228,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); + auto castOp = rootOf(value).getDefiningOp(); return castOp && mlir::isa(castOp.getType()) && castOp.getIn().getDefiningOp(); } @@ -454,9 +1236,15 @@ class OwnershipInferencePass : public mlir::PassWrapper(def)) + { + return true; + } + auto castOp = mlir::dyn_cast(def); if (!castOp) { @@ -485,6 +1273,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::isa 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 +1385,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 +1403,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) @@ -729,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; } @@ -754,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; } @@ -770,45 +1592,28 @@ 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 +1622,18 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) || isBorrow(user, value)) - { - continue; - } - - if (taker) + auto several = false; + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (mlir::isa(user) || isBorrow(user, used)) { - 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 +1726,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 +1749,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) && user->getBlock() == taker->getBlock() && user->isBeforeInBlock(first)) { first = user; } - } + }); return first; } @@ -973,10 +1773,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 +1814,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) @@ -1084,6 +1887,42 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << 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 reportPlaceBorrowerAssigned(mlir::Operation *op, llvm::StringRef name, llvm::StringRef place) + { + if (quiet) + { + return; + } + + op->emitError("'") << 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) + { + 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 a3e603ca3..a480fee64 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2404,7 +2404,11 @@ 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 + own_field_borrow own_element_borrow own_container_loop + own_optional_access own_borrow_merge + 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. @@ -2452,6 +2456,23 @@ 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_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_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" "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_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_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_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_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_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_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_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_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_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)); +} 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_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_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_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."); +} 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."); +} 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."); +}