diff --git a/tslang/docs/superpowers/plans/2026-09-29-own-phase-4.md b/tslang/docs/superpowers/plans/2026-09-29-own-phase-4.md new file mode 100644 index 000000000..1af2afda7 --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-09-29-own-phase-4.md @@ -0,0 +1,218 @@ +# `-mm=own` Phase 4 Implementation Plan: function signatures and `any` + +> **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 function's body tells its callers three facts (spec §2.4, §3.2), and +the inference uses them at every call it can resolve. Today: + +- `function keep(h: H, c: C) { h.c = c; }` is rejected ("takes a second reference"): nothing lets + a callee keep what it is given. +- `get c() { return this.c; }` and `function first(h: H) { return h.c; }` are rejected ("borrows + 'this.c' and cannot be stored, returned or captured"). +- `` `${this.color} area=${this.area()}` `` is rejected: any call drops a borrow of a field of + `this`, even one to a method that only reads numbers (§14.4's three corpus regressions). +- `any` is rejected, and rc leaks every `any` box that no `let` holds (§14.5: rc 66.2 MB). + +After this phase all four compile, and the unsound shapes around them are errors. + +**Architecture:** + +1. **`OwnershipSignaturePass`** (new module pass, own only, just before inference). It resolves + every call to the functions it may reach, computes each function's facts, and pins them on the + call ops, where the per-function inference reads them without looking anything up: + - `__own_params` (array of indices): the parameters the callee keeps (owned-by-callee). + - `__own_result_borrows` (index): the result is a borrow of that argument. + - `__own_no_drops` (unit): the callee destroys nothing its caller can reach. + The same attributes on a `ts.Func` are the facts its own body must honour. +2. **`OwnershipInferencePass`** learns the three facts on both sides of a call. +3. **MLIRGen**: the boxing cast into `any` carries its birth reference like every other fresh + value (`__owned_result` + `ts.Retain`), which is rc's leak fix and own's owner. + +**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`: §2.4 (calls), §3.2 (the +signature pass), §14.5-14.6 (this phase's input). + +## Global Constraints + +- Branch `own-phase-4` from `origin/main` (edaf802a, with #403 and #404 merged). +- `gc`, `rc` and `none` output does not change, except Task 5's deliberate rc leak fix (the `any` + box), which changes rc's and gc's IR by one retain and one release per box. +- Never branch on the memory model in MLIRGen (§4.3). The signature pass runs only under own. +- `__tslang_inc_ref` and `__tslang_dec_ref` never appear in own output. +- Own tests run with `--no-default-lib`, under JIT and AOT, and read through a `churn()` after the + point where a wrong release would free. Expected values are checked under `gc` first. +- 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. +- Source files are CRLF. Commits are GPG-signed, each ending with the session's `Co-Authored-By:`. +- Check IR shapes with `--emit=mlir-affine --own-skip-inference` and, since CSE runs first under + `--opt`/JIT, with `--emit=jit --mlir-print-ir-after-all --mlir-disable-threading`. +- Baseline corpus on this tree: 272 of 564 compile under `-mm=own --no-default-lib`. + +## Shapes (measured on edaf802a) + +- A parameter arrives as a non-owning `ts.Variable(%argN)`; `this` is `%arg0`. rc never releases + it. A callee that keeps it reads the slot, `ts.Retain`s what it read and stores it. +- A result goes through a non-owning result slot: `ts.Store(v, %ret)`, then `ts.Load(%ret)` into + `ts.ReturnInternal`. A returned field read is `ts.Retain`ed before its store: rc returns +1. +- The caller releases a call's result as a temporary (`ts.Release`), or declares a + `__owned_consumed` `let` from it (no `ts.RetainSlot`, one `ts.ReleaseSlot`). +- Every method call goes through the vtable, even with no subclass: `ts.ThisVirtualSymbolRef` or + `ts.VirtualSymbolRef` (`identifier`, `index`) feeding `ts.CallInternal`, directly or through + `ts.GetMethod`. An override sits at the same position of its class's `..vtbl` global under the + same method name (`B.get` and `D.get` at 2). +- `___unbox` reads `ts.Unbox` of its parameter, calls the payload's `.instanceOf` through the + vtable's first slot (`ts.VTableOffsetRef` index 0, twice), casts `!ts.opaque` to `T`, retains it + and returns it. `___cast` returns its parameter narrowed on each branch, or `null`. +- The boxing cast is +0 and unmarked. A box a `let` holds gets a `ts.RetainSlot` and is freed; a + box held by a folded `const` or passed straight to a call is never released. + +## Rulings made while planning + +- **Closed world.** An owned parameter or a borrowed result is sound only if *every* call that can + reach the function knows it: a caller that does not know borrows the argument and releases it, + or releases a result it does not own - a double free either way. So a function has those facts + only if it is not public, and every reference to its symbol is a direct call + (`ts.SymbolCallInternal`), its entry in a class's `..vtbl` global, or the `identifier` of a + virtual reference used only as a call's callee (directly or through `ts.GetMethod`/`ts.GetThis`). + A function reached any other way - a method taken as a value, an interface, an export - keeps + rc's convention, and its body's retain stays an error, now saying why. `__own_no_drops` goes the + other way (an unknown call is assumed to drop), so it needs no such check. Cost if wrong: none + for soundness; exported functions and interface methods get no facts until the export/import + work. +- **Virtual families meet.** A virtual call may reach every entry at its index with its method + name, in any `..vtbl` of the module (an unrelated class that happens to match only adds a + candidate). The call has an owned parameter only if every candidate owns it, a borrowed result + only if every candidate borrows the same argument, no drops only if no candidate drops. A + function whose own facts differ from the meet of a family it is in loses them (it keeps rc's + convention, and its body errors). Cost if wrong: a spurious error where a subclass disagrees. +- **A returned parameter is a borrowed result, not an owned parameter.** §2.4 says a callee that + returns a parameter owns it. `___cast` returns its parameter narrowed, and owning it would make + every `u` move `u`. Rust's elision (`fn id(c: &C) -> &C`) is the model. Cost if wrong: + `h.c = id(new C())` is an error (a borrow cannot be stored) where the owned reading would accept + it. +- **An owned parameter is moved on every path or it is an error.** The callee has no release for + it, so a path that does not move it would leak it and one that moves it twice would free it + twice. The move must dominate every `ts.ReturnInternal`, must not be in a loop, and nothing may + read the parameter after it. A throw before the move leaks it, as rc leaks a try-less function's + locals. Cost if wrong: `if (x) h.c = c;` is rejected (drop elaboration is not in scope). +- **A parameter that is assigned has no facts.** Its reads no longer see the argument. +- **A borrowed result's places are a wildcard.** The caller does not know which place under the + argument the result came from (`h.c`, `h.c.d`), so any overwrite of any field or element, and + any array op that removes elements, drops it. Its roots are the argument's, found as a place + read's are. Cost if wrong: spurious errors, never a missed one. +- **Drops.** A function may drop if it overwrites a field or an element (`ts.ReleaseSlot` of a + place) whose root it does not own, assigns a global, pops, shifts, splices or sets the length of + an array it does not own, or makes a call that may drop (an unresolved one always may). Least + fixpoint over the call graph. A constructor's stores into its own `this` are not drops: the + object is new, and nothing outside holds a borrow into it. Cost if wrong: a constructor called + through `super` on an object whose fields someone borrows - impossible in the current MLIRGen, + where a borrow of a field of `this` inside a constructor already ends at the call. +- **A call is a use of what it is given for as long as it runs.** Phase 3 skipped a call's own + arguments when the call was the drop ("read before it runs"). That is unsound: `f(h.c)` where + `f` resets `h.c` through a global and then reads its parameter reads freed memory. A call that + drops a chain is now an error when it is also given the borrow. Cost if wrong: extra errors in + code that passes a field to a function that overwrites fields. +- **The `.instanceOf` slot.** A call through a vtable's first slot is built only by + `mlirGenInstanceOfOpaque`, to reach a class's generated `..instanceOf` (a string compare), which + drops nothing. Without this, `___unbox` has an unknown call between its read and its return. + Found while reviewing: a class that implements an interface keeps the interface's vtable in that + slot, so the call crashes under every model (§15.7). It destroys nothing either way. +- **The `any` box fix is in MLIRGen and changes rc.** The boxing cast gets `__owned_result` and a + `ts.Retain`, as `markFreshStringOwned` does for a printed number. That is rc's §14.5 leak fix, + and under own it makes the box a fresh value with one owner. Cost if wrong: rc suite failures, + which the full suite shows. +- **Export and import of facts is deferred.** §3.2 has a `-shared` build export them beside the + `__tsmm_own_*` marker. Under the closed-world rule an exported function gets no facts, so the + default stays sound without it. It is its own PR. + +## Review Focus + +1. Soundness of the closed world: any reference to a function's symbol that the classifier does + not recognise must drop the function's owned/borrowed facts. Grep the IR of every new test for + `SymbolRef` uses outside `..vtbl` globals. +2. A caller passing something it does not own (a parameter, a place read, a global) to an owned + parameter is an error, never a silent move. +3. The borrowed-result caller erases *both* the temporary's `ts.Release` and a consumed `let`'s + `ts.ReleaseSlot`s, and the callee erases the retain before its result store. Each is a teeth + test. +4. The `.instanceOf` resolution matches only a callee loaded from vtable slot 0 of a vtable + loaded from slot 0 of an object (`___unbox`'s shape). + +--- + +### Task 1: the signature pass and drops + +**Files:** +- Create: `lib/TypeScript/OwnershipSignaturePass.cpp`, `lib/TypeScript/OwnershipFacts.h` +- Modify: `include/TypeScript/Passes.h`, `lib/TypeScript/CMakeLists.txt`, `tslang/transform.cpp`, + `lib/TypeScript/OwnershipInferencePass.cpp` +- Tests: `test/tester/own/own_call_no_drops.ts` (``${this.color} area=${this.area()}``, a + `churn()` between a field read and its use), `own_err_call_drops_indirectly.ts` (a field of a + parameter read across a call to a function that calls one that assigns the global owner) + +Steps: resolve calls (`resolveCallees`: direct, virtual family, `.instanceOf` slot, else unknown); +compute `__own_no_drops` to a least fixpoint and pin it on functions and resolved calls. In the +inference, `dropsChain` asks a call's `__own_no_drops` first; `isCall` drops stay otherwise. +Remove the "a call's own arguments are read before it runs" exclusion when the call drops. +Corpus: the three `export_class_abstract*` files were expected to compile again. They do not: +their class is exported, so `area`'s family may have an override in another module (see §15.7). +Commit: `-mm=own phase 4: a call drops a borrow only if its callee may drop`. + +### Task 2: borrowed-from-argument results + +**Tests:** `own_getter_borrow.ts` (getter, method and free function returning a field, used and +held by a `let`), `own_cast_borrow.ts` (a function returning its parameter), negatives +`own_err_borrowed_result_stored.ts` (stored into a field), `own_err_borrowed_result_outlives.ts` +(the argument's field overwritten, then the result used), `own_err_borrowed_result_escapes.ts` +(the getter's function reached as a value: the callee body's error says why), +`own_err_borrowed_result_family.ts` (an override that returns a new value). + +Signature: a function whose every returned heap value is, through views, opaque casts, `ts.Unbox` +and place reads, the parameter `K`'s slot (never assigned) - `null`/`undefined` and non-heap values +allowed - has `__own_result_borrows = K`, subject to the closed world and the family meet. +Inference, callee: a place read or parameter view that reaches the result slot of such a function +is not kept; its `ts.Retain` is erased. Caller: a call carrying `__own_result_borrows` is a borrow +read like a place read - wildcard places, roots through argument `K` - whose `ts.Release` and whose +consumed `let`s' `ts.ReleaseSlot`s are erased. +Commit: `-mm=own phase 4: a result that borrows an argument is a borrow at the call`. + +### Task 3: owned-by-callee parameters + +**Tests:** `own_param_kept.ts` (`keep(h, new C(i))`, `keep(h, a)` from a `let`, forwarding through +a second function, a method storing its parameter into `this`), negatives +`own_err_param_kept_used.ts` (the argument read after the call), +`own_err_param_kept_some_paths.ts`, `own_err_param_kept_loop.ts` (an argument made outside a loop), +`own_err_param_kept_not_owned.ts` (a caller passing its own parameter or a field). + +Signature: parameter `K` (never assigned) is owned if a read of it is taken - stored into a place, +inserted into an array, or passed to an owned parameter - subject to the closed world and the +meet; fixpoint for forwarding. Inference, callee: the take is a move out of the parameter's slot +(no releases to erase; the retain goes); it must dominate every return, not loop, and nothing may +read the parameter after it. Caller: the call is a taker of each owned argument +(`takerAcquires` is true: rc's retain is inside the callee); a slot argument is a slot receiver +whose acquisition is the call; an argument the caller does not own is an error: +`argument 'x' is given to 'f', which keeps it, but -mm=own cannot move it here`. +Commit: `-mm=own phase 4: a callee may keep a parameter its caller moves to it`. + +### Task 4: `any` + +**Tests:** `own_any_box.ts` (`let` and `const` boxes, a box passed to a call, unboxed and read +after a `churn()`), negative `own_err_any_unboxed_outlives.ts` (the unboxed value used after its +box is reassigned). rc: `00owned_any_boxing.ts` and the §14.5 program under `measure.ps1`. + +MLIRGen: mark the boxing cast fresh (`__owned_result` + `ts.Retain`). Inference: a boxing cast is +a taker of its payload (it already is) and a fresh value; `___unbox` gets its borrowed result from +Task 2's signature rules through `ts.Unbox`, and its `.instanceOf` call is drop-free (Task 1). +Commit: `-mm=own phase 4: any`. + +### Task 5: results + +Measure (`measure.ps1`, O3 and O1) an owned-parameter loop, a getter loop and an `any` loop under +all four models, and rc on §14.5's program. Corpus report with the histogram. Teeth: keep the +caller's release of an owned argument; keep the callee's retain of a borrowed result; keep the +caller's release of a borrowed result. Debug `ctest -R own`. Spec §15 and the status line. +Commit: `-mm=own phase 4: 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 dceed5a7d..e9ba5c466 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 @@ -3,8 +3,8 @@ Date: 2026-09-24. Status: design approved in conversation; written review 2026-09-28 (§10), amendments folded in. Phase 0 merged as #399 (results §11); phase 1 (moves by reachability) 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/`. +(results §13); phase 3 (containers and unions) merged as #403 (results §14); phase 4 (function +signatures and `any`) on branch `own-phase-4` (results §15). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -295,7 +295,8 @@ Each phase is a PR series leaving `main` green. 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. + the marker and import on the other side. As built (§15): the facts, drop-free callees and + `any`; export and import deferred. 5. **Closures**: escaping vs non-escaping, cells. 6. **Corpus report and diagnostics polish**: refine the shapes the histogram says matter. 7. **Generators and async**: the state object as owner. @@ -787,3 +788,162 @@ reference. `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. + +## 15. Phase 4 results, 2026-09-29 + +Plan: `docs/superpowers/plans/2026-09-29-own-phase-4.md`. New: `OwnershipSignaturePass.cpp` and +`OwnershipFacts.h` (the helpers both passes read the ops with). Changed: +`OwnershipInferencePass.cpp`, and two MLIRGen sites for `any` (§15.4). + +### 15.1 The signature pass + +A module pass, own only, just before inference. It resolves every call it can, computes three +facts per function, and pins on each resolved call the facts its candidates agree on: + +- `__own_no_drops`: the callee destroys nothing its caller can reach. That means no overwrite of a + field or element whose root it did not make, no global assignment, no removal from an array it + did not make, no `delete`, and no call that may drop. It is a least fixpoint. A constructor + filling its own `this` is not a drop. +- `__own_result_borrows = K`: every heap value the function returns is parameter `K`'s. That is + seen through views, `!ts.opaque` casts, `ts.Unbox`, field and element reads, the object a method + is called on, and results that borrow. `null` agrees with any. +- `__own_params`: the parameters the body keeps. A kept parameter is stored into a field, element + or global, pushed, or passed to a kept parameter. + +**Resolving a call.** The candidates are found as follows: + +- a direct call has one candidate; +- a virtual call reaches every entry at its index under its method name in any class `..vtbl`. + This is its family, and every member must be private and defined here; +- a call through the vtable's first slot is only built to reach a generated `..instanceOf`, + which drops nothing (§15.7 has the bug this found); +- anything else (a closure, a function read from a field, an interface) is unknown. + +**The closed world.** `__own_no_drops` goes one way: an unknown call may drop, which can only add +an error. The other two facts are relied on by the callee's body. A caller that does not know them +borrows the argument and releases it, or releases a result it does not own, a double free either +way. So a function has them only if all of these hold: + +- it is private; +- every use of its symbol is a direct call, a class vtable entry, or a method reference used only + as a callee; +- every family it is in agrees. + +A function that loses them keeps rc's convention. The error in its body gets a note saying why: +`'firstOf' could return a borrow or keep a parameter, but it is used other than by a call` (or +`it can be called from another module`, `an override in its class family disagrees`, +`an override may be defined in another module`). The facts are recomputed with the calls pinned +until nothing changes, so a method returning another's borrow, and a function forwarding a +parameter to one that keeps it, get the fact too. + +`mlir::SymbolTable::getSymbolUses(module)` does not look inside the module, which is a symbol +table. The first version saw no uses, so every function was closed. `own_err_borrowed_result_escapes` +compiled until the walk used the module's body region. + +### 15.2 What the inference accepts + +- **Calls that drop.** A call drops a borrow only if its callee may drop (`__own_no_drops` absent). + `` `${this.color} area=${this.area()}` `` compiles again. Phase 3 skipped a call's own arguments + ("read before it runs"). That was unsound: `f(h.c)` compiled where `f` resets `h.c` through a + global and then reads its parameter. Now a call given a borrow uses it for as long as the callee + runs. Two things are exempt, because they are read as the call starts: the function value an + indirect call goes through, and the object a field-held method is bound to (`o.m()`). +- **Results that borrow.** A call with `__own_result_borrows` is a borrow read like a field read. + Its places are a wildcard, so any overwrite of any field or element, and any removal, drops it. + Its roots are the argument's. Its temporary release and a consuming `let`'s releases are erased. + In the callee, the retain rc made for the caller goes, and the return is a use, not an escape. +- **Kept parameters.** In the callee, the take is a move out of the parameter's slot. It must be + the last use, not loop, and dominate every return. The callee has no release for the parameter, + so `if (f) h.c = c` is an error. In the caller, the call is a taker of each kept argument, and rc's + retain for it is in the callee. It can move a fresh value, an owning `let` or a kept parameter. A + parameter, a global or a field it does not own is + `argument 'x' is given to 'keep', which keeps it, but -mm=own cannot move it here`. +- **A thrown value read by a catch's copy thunk** (`.eh.copy.*`) is a move. A throw hands the + thrower's reference to the exception object, and nothing gives it back (rc leaks it the same + way). A TypeScript rethrow is a new throw, so each exception is copied into one catch. + `___unbox`'s failure path throws a string, so every unboxing needs this. + +A bug the positive tests found: a move whose acquisition is the call itself was "erased with its +retain". The call to `forward(h, a)` vanished, and the program read uninitialised memory. + +### 15.3 Measured + +AOT, `measure.ps1`, at `-O3` and `-O1` (identical within 1%), in MB: + +| program | gc | rc | none | own | +| --- | --- | --- | --- | --- | +| `own_param_kept` | 6.5 | 4.8 | 1657.7 | 4.8 | +| `own_getter_borrow` | 6.5 | 12.6 | 1653.3 | 4.8 | +| `own_any_box` | 6.5 | 4.8 | 558.9 | 4.8 | +| `own_call_no_drops` | 6.5 | 4.8 | 1100.7 | 4.8 | + +rc climbs in `own_getter_borrow`: it never releases a getter's result used as a temporary +(`h.cc.x`: the `ts.CallInternal` result has no `ts.Release`). Own has no reference to give back. +§14.5's `const a: any = new C(i)` loop reads 4.6 MB under all four models, at both levels. `none` +does not grow, so LLVM removes the allocation and that program shows nothing about reclamation. + +### 15.4 `any` + +The boxing cast now carries its birth reference (`__owned_result` + `ts.Retain`), as a printed +number does (`markFreshStringOwned` became `markFreshBlockOwned`). That is rc's §14.5 leak fix: +a box held by a folded `const`, or passed straight to a call, was never released. Under own it +makes the box a fresh value with one owner. An `any` cast to `any` allocates nothing and is not +marked. The fix broke rc's catch copy thunks at first: the thunk's plain store into the catch slot +let the box's new reference go back at the end of the thunk, so the catch read a freed box (10 +suite failures). The store now takes it over. The catch slot is runtime memory. + +`a` is `___unbox(a)`. With `___unbox`'s borrowed result (through `ts.Unbox`) and its +drop-free `.instanceOf` call, the payload is borrowed for as long as the box lives. + +### 15.5 Teeth + +- With the releases a kept argument's move erases kept, `own_param_kept` fails under JIT and AOT. +- With a borrowed result's temporary release kept, `own_getter_borrow` and `own_any_box` fail. +- Keeping the callee's retain for a borrowed return makes the same two fail at compile time: the + retain reads as an escape. So it proves nothing about the runtime. At runtime it would be a leak, + not a double free. +- `own_call_no_drops` erases nothing. Its teeth are the negatives. + +### 15.6 Tests and the corpus + +- **Positives:** `own_call_no_drops`, `own_getter_borrow` (getter, method, free function, a method + forwarding another's borrow, a field-or-`null` result, a returned parameter), `own_param_kept` + (field, forwarding, method into `this`, push, constructor parameter property), `own_any_box`. +- **Negatives:** + - calls that drop: `own_err_call_drops_indirectly`, `own_err_call_given_borrow`, + `own_err_call_this_borrow`, `own_err_exported_virtual_call`; + - borrowed results: `own_err_borrowed_result_stored`, `own_err_borrowed_result_outlives`, + `own_err_borrowed_result_let_outlives`, `own_err_borrowed_result_escapes`, + `own_err_borrowed_result_family`; + - kept parameters: `own_err_param_kept_used`, `own_err_param_kept_some_paths`, + `own_err_param_kept_loop`, `own_err_param_kept_not_owned`; + - `any`: `own_err_any_unboxed_outlives`. + +Corpus (`test/tester/tests/*.ts` under `-mm=own --no-default-lib`): 272 of 564 compiled before, +303 after, and none stopped. "Takes a second reference" fell from 189 files to 149. About half of +the new ones throw or catch (the copy thunks); several more cast out of `any`. Release suite: 3226 +of 3226. + +### 15.7 Known limits (the input to phase 5 and later) + +- Export and import of the facts (§3.2). An exported function, or a method of an exported class, + gets no owned or borrowed facts, and a virtual call on one gets no `__own_no_drops`. That is sound + and restrictive for `-shared` modules. +- A parameter kept on some paths only (needs drop elaboration: a release on the others). +- A borrowed result's places are a wildcard, so any field overwrite between the call and the use + drops it, even of an unrelated object. +- Interface calls (`ts.InterfaceSymbolRef`) are unresolved: no facts, and they drop. +- A virtual family is matched by method name and index, so an unrelated class's method at the + same index can only take facts away. +- The three corpus files §14.4 lost (`export_class_abstract*`) stay rejected. Their class is + exported, so the virtual call to `area` has candidates this module cannot see, and it may drop + (`own_err_exported_virtual_call` is the same shape). The error does not yet say why: the note + on lost facts is attached to escapes and second references, not to drops. +- rc: a getter's result used as a temporary is never released (§15.3). +- Every model: `anyValue` segfaults when `B` implements an interface. + `mlirGenInstanceOfOpaque` calls vtable slot 0 as `..instanceOf`, but such a class keeps the + interface's vtable there (`B..vtbl = {I, .instanceOf, ...}`). Fixing it changes the vtable + layout, so it is left for its own PR. +- From §14.6: a read reached through an interface, 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/include/TypeScript/Passes.h b/tslang/include/TypeScript/Passes.h index 396d99724..81cf46239 100644 --- a/tslang/include/TypeScript/Passes.h +++ b/tslang/include/TypeScript/Passes.h @@ -35,6 +35,11 @@ std::unique_ptr createOwnershipVerifierPass(); /// acquisition and reports every other one. Runs at the affine level under own only. std::unique_ptr createOwnershipInferencePass(); +/// The callee facts -mm=own infers across the module - a parameter the callee keeps, a result that +/// borrows an argument, a callee that destroys nothing its caller can reach - pinned on each call it +/// resolves. Runs just before OwnershipInferencePass, under own only. +std::unique_ptr createOwnershipSignaturePass(); + /// Lets a call take over the reference its callee returned instead of retaining a second one. /// Runs on the whole module, after MLIRGen, because deciding which callees retain their result /// needs every function to be present - see docs/reference-counting-evaluation.md section 9.27. diff --git a/tslang/lib/TypeScript/CMakeLists.txt b/tslang/lib/TypeScript/CMakeLists.txt index 3f42cc3dd..49b8be4f6 100644 --- a/tslang/lib/TypeScript/CMakeLists.txt +++ b/tslang/lib/TypeScript/CMakeLists.txt @@ -33,6 +33,7 @@ add_mlir_dialect_library(MLIRTypeScript RelocateConstantPass.cpp OwnershipVerifierPass.cpp OwnershipInferencePass.cpp + OwnershipSignaturePass.cpp OwnedReturnConsumptionPass.cpp GCPass.cpp AsyncTargetWidthPass.cpp diff --git a/tslang/lib/TypeScript/MLIRGenCast.cpp b/tslang/lib/TypeScript/MLIRGenCast.cpp index 81928e493..7c7c7c8e4 100644 --- a/tslang/lib/TypeScript/MLIRGenCast.cpp +++ b/tslang/lib/TypeScript/MLIRGenCast.cpp @@ -677,7 +677,17 @@ namespace mlirgen // time (§9.37). if (isa(type) && castToStringAllocates(valueType)) { - markFreshStringOwned(location, castResult); + markFreshBlockOwned(location, castResult); + } + + // Boxing into `any` allocates the box, and the cast handed it back with no reference at + // all: a box a `let` holds was retained by the `let` and freed, but one held by a folded + // `const`, or passed straight to a call, was never released (1M `const a: any = new C()`: + // rc 66 MB, gc 6 MB). The box takes the payload's reference above; this is the box's own. + // An `any` cast to `any` is the same box, and allocates nothing. + if (isa(type) && !isa(valueType)) + { + markFreshBlockOwned(location, castResult); } return V(castResult); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 30124a2d1..49362abd8 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -1070,17 +1070,17 @@ class MLIRGenImpl return isa(valueType) || valueType.isIntOrIndex(); } - // Gives a freshly built string the same standing as every other producer of a new heap - // value: the retain makes the reference real, and the mark says a receiver may take it over - // rather than adding one of its own - which is also what lets §9.30 give it back where - // nothing receives it at all. + // Gives a freshly built string, or a freshly made `any` box, the same standing as every other + // producer of a new heap value: the retain makes the reference real, and the mark says a + // receiver may take it over rather than adding one of its own - which is also what lets §9.30 + // give it back where nothing receives it at all. // // Both halves are needed together, and the retain is what makes this safe to be generous // with: a value wrongly counted as fresh gains a reference and a release for it, which is // balanced, where a mark on its own would hand a receiver a reference nobody took. - void markFreshStringOwned(mlir::Location location, mlir::Value value) + void markFreshBlockOwned(mlir::Location location, mlir::Value value) { - if (!value || !isa(value.getType())) + if (!value || !isa(value.getType())) { return; } @@ -6212,9 +6212,9 @@ class MLIRGenImpl // `ts.StringConcat`, which builds a new string. A receiver takes that reference // over; `("a" + b).length`, where there is no receiver, gives it back at the end of // the block instead of leaking (§9.37). - if (opCode == SyntaxKind::PlusToken) + if (opCode == SyntaxKind::PlusToken && isa(result.getType())) { - markFreshStringOwned(location, result); + markFreshBlockOwned(location, result); } break; diff --git a/tslang/lib/TypeScript/MLIRGenStatements.cpp b/tslang/lib/TypeScript/MLIRGenStatements.cpp index 105b419ee..12c59d678 100644 --- a/tslang/lib/TypeScript/MLIRGenStatements.cpp +++ b/tslang/lib/TypeScript/MLIRGenStatements.cpp @@ -1158,6 +1158,13 @@ namespace mlirgen return mlir::failure(); } + // The catch object the runtime copies into holds what it is given: a box made here carries + // a reference of its own, which goes with it rather than back at the end of the thunk. + if (producesOwnedReference(V(result))) + { + consumeOwnedReference(V(result)); + } + builder.create(location, V(result), arguments[0]); builder.create(location, mlir::Value()); diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h new file mode 100644 index 000000000..00ee0cde1 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -0,0 +1,434 @@ +#ifndef MLIR_TYPESCRIPT_OWNERSHIPFACTS_H +#define MLIR_TYPESCRIPT_OWNERSHIPFACTS_H + +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" + +#include "mlir/Interfaces/ControlFlowInterfaces.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/STLFunctionalExtras.h" + +// What -mm=own's two passes both need to read the ownership ops: which op is a view of the block +// its operand holds, where a value was made, which ops are calls and places. See +// docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 3.2, 3.3 and 14. + +namespace own_facts +{ + +namespace mlir_ts = mlir::typescript; + + +// The facts OwnershipSignaturePass pins on a call it resolved, and on a function whose callers all +// know them (spec 2.4, 3.2). Absent means rc's convention: every parameter borrowed, the result +// owned, and the callee may destroy anything it can reach. +// +// The parameters the callee keeps: the caller moves each such argument into the call. +#define OWN_PARAMS_ATTR_NAME "__own_params" +// The argument the result is a borrow of: the caller owns nothing it gets back. +#define OWN_RESULT_BORROWS_ATTR_NAME "__own_result_borrows" +// The callee destroys nothing its caller can reach: no overwrite of a place it did not make, no +// assignment of a global, no call that may. +#define OWN_NO_DROPS_ATTR_NAME "__own_no_drops" +// On a function whose body relies on a fact its callers cannot all know (a parameter it keeps, a +// result that borrows): why, for the error the body gets instead. +#define OWN_FACTS_LOST_ATTR_NAME "__own_facts_lost" + +// The arguments of a call, without the callee value of an indirect one. +inline mlir::OperandRange callArgs(mlir::Operation *op) +{ + if (mlir::isa(op)) + { + return op->getOperands().drop_front(); + } + + return op->getOperands(); +} + +inline bool callNoDrops(mlir::Operation *op) +{ + return op->hasAttr(OWN_NO_DROPS_ATTR_NAME); +} + +// The argument indices a call's callee keeps. +inline llvm::ArrayRef ownedParams(mlir::Operation *op) +{ + if (auto attr = op->getAttrOfType(OWN_PARAMS_ATTR_NAME)) + { + return attr.asArrayRef(); + } + + return {}; +} + +// The argument a call's result borrows, or -1. +inline int resultBorrows(mlir::Operation *op) +{ + if (auto attr = op->getAttrOfType(OWN_RESULT_BORROWS_ATTR_NAME)) + { + return static_cast(attr.getInt()); + } + + return -1; +} + +// 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. +inline 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. +inline mlir::Value rootOf(mlir::Value value) +{ + while (auto *def = value.getDefiningOp()) + { + if (!isView(def)) + { + 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. +inline 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); + } + } +} + +inline bool isOwningVariable(mlir_ts::VariableOp varOp) +{ + return varOp->hasAttr(OWNED_LOCAL_ATTR_NAME) || varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME); +} + +// A string literal or a constant array cast to its value type. The result is either the +// immortal global itself, which a release skips, or a copy nobody else holds, which a +// release destroys - so, owned once, it has one owner either way. `null` and `undefined` +// widened to a nullable type (`c: C | null = null`), and an empty optional +// (`u: C | undefined = undefined`), hold no block at all. +inline bool isLiteral(mlir::Operation *def) +{ + if (mlir::isa(def)) + { + return true; + } + + auto castOp = mlir::dyn_cast(def); + if (!castOp) + { + return false; + } + + auto *in = castOp.getIn().getDefiningOp(); + return in && mlir::isa(in); +} + +// Made here and held by nobody else: an allocation, a literal cast to its value type, an +// operation rc marks as arriving with a reference, or a direct call of a function this module +// defines (every function returns its result retained, rc 9.24; the call's own mark does not +// survive the affine lowering). A declared callee, an indirect call, a parameter, a load, a +// value merged from branches, or a result that borrows an argument is not fresh. +inline bool isFresh(mlir::Value value) +{ + auto *def = value.getDefiningOp(); + if (!def || resultBorrows(def) >= 0) + { + return false; + } + + if (mlir::isa(def)) + { + return true; + } + + if (auto callOp = mlir::dyn_cast(def)) + { + auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, callOp.getCalleeAttr()); + return callee && !callee.isDeclaration(); + } + + if (mlir::isa(def)) + { + return false; + } + + return def->hasAttr(OWNED_RESULT_ATTR_NAME) || isLiteral(def); +} + +inline bool isCall(mlir::Operation *op) +{ + return mlir::isa(op); +} + +inline bool isPlace(mlir::Value ref) +{ + return mlir::isa_and_nonnull(ref.getDefiningOp()); +} + +// 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. +inline 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; +} + +// The load of an owning local that `value` was read from, through its views (a class widened +// to a union with null, an upcast, a union made from it). None when the value is anything +// else - a parameter, a field, a boxing cast - which phase 1 does not move out of. +inline mlir_ts::LoadOp slotLoadOf(mlir::Value value) +{ + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp) + { + return {}; + } + + auto varOp = loadOp.getReference().getDefiningOp(); + if (!varOp || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return {}; + } + + return loadOp; +} + +// Does a value of this type own a block that a release would destroy? +inline bool ownsHeap(mlir::Value value) +{ + MLIRTypeHelper mth(value.getContext(), CompileOptions{}); + return mth.ownsHeapMemory(value.getLoc(), value.getType()); +} + +// Holds no block: a number, `null` or `undefined` widened, an empty optional, a string literal. +inline bool holdsNoBlock(mlir::Value value) +{ + auto *def = rootOf(value).getDefiningOp(); + return !ownsHeap(value) || (def && isLiteral(def)); +} + +// Is this the slot of parameter `argument` - a local declared from it that owns nothing, is not +// captured, and is never assigned, so every read of it is the argument? +inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) +{ + auto init = varOp.getInitializer(); + auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || + !mlir::isa(argument.getOwner()->getParentOp()) || isOwningVariable(varOp) || + varOp.getCaptured().value_or(false)) + { + return false; + } + + auto assigned = llvm::any_of(varOp.getResult().getUsers(), [&](mlir::Operation *user) { + auto storeOp = mlir::dyn_cast(user); + return storeOp && storeOp.getReference() == varOp.getResult(); + }); + if (assigned) + { + return false; + } + + index = argument.getArgNumber(); + return true; +} + +// The object a method call is made on: `ts.GetThis` of a method bound to it. Null otherwise. +inline mlir::Value boundThis(mlir::Value value) +{ + auto getThisOp = value.getDefiningOp(); + auto *bound = getThisOp ? getThisOp.getOperand().getDefiningOp() : nullptr; + if (auto refOp = mlir::dyn_cast_or_null(bound)) + { + return refOp.getThisVal(); + } + + if (auto refOp = mlir::dyn_cast_or_null(bound)) + { + return refOp.getThisVal(); + } + + return {}; +} + +// The parameter whose argument `value` is a borrow of, or -1 (spec 2.4, borrowed-from-argument). +// Seen through views, casts out of `!ts.opaque`, `ts.Unbox` (the payload an `any` box holds), the +// object a method is called on, +// reads out of fields and elements (the argument's block holds them), and results that borrow an +// argument, down to a read of a parameter's slot or the parameter itself. Merged values must all +// agree; one that holds no block (`null`) agrees with any. A value the function made, a local, a +// global: -1. +inline int borrowedParam(mlir::Value value, int depth = 0) +{ + if (depth > 32) + { + return -1; + } + + value = rootOf(value); + if (auto argument = mlir::dyn_cast(value)) + { + if (argument.getOwner()->isEntryBlock()) + { + return mlir::isa(argument.getOwner()->getParentOp()) ? argument.getArgNumber() : -1; + } + + llvm::SmallVector merged; + if (!mergedInto(argument, merged)) + { + return -1; + } + + auto found = -1; + for (auto passed : merged) + { + if (holdsNoBlock(passed)) + { + continue; + } + + auto index = borrowedParam(passed, depth + 1); + if (index < 0 || (found >= 0 && index != found)) + { + return -1; + } + + found = index; + } + + return found; + } + + if (auto object = boundThis(value)) + { + return borrowedParam(object, depth + 1); + } + + auto *def = value.getDefiningOp(); + if (auto castOp = mlir::dyn_cast_or_null(def); + castOp && mlir::isa(castOp.getIn().getType())) + { + return borrowedParam(castOp.getIn(), depth + 1); + } + + if (auto unboxOp = mlir::dyn_cast_or_null(def)) + { + return borrowedParam(unboxOp.getIn(), depth + 1); + } + + if (auto propertyRefOp = mlir::dyn_cast_or_null(def)) + { + return borrowedParam(propertyRefOp.getObjectRef(), depth + 1); + } + + if (auto elementRefOp = mlir::dyn_cast_or_null(def)) + { + return borrowedParam(elementRefOp.getArray(), depth + 1); + } + + if (auto loadOp = mlir::dyn_cast_or_null(def)) + { + auto ref = loadOp.getReference(); + if (isPlace(ref)) + { + return borrowedParam(ref, depth + 1); + } + + auto index = -1; + auto varOp = ref.getDefiningOp(); + return varOp && isParameterSlot(varOp, index) ? index : -1; + } + + if (auto varOp = mlir::dyn_cast_or_null(def)) + { + auto index = -1; + return isParameterSlot(varOp, index) ? index : -1; + } + + if (def && isCall(def) && resultBorrows(def) >= 0 && + static_cast(resultBorrows(def)) < callArgs(def).size()) + { + return borrowedParam(callArgs(def)[resultBorrows(def)], depth + 1); + } + + return -1; +} + +} // namespace own_facts + +#endif // MLIR_TYPESCRIPT_OWNERSHIPFACTS_H diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 191a4d9ab..47ad7bb1d 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -10,6 +10,8 @@ #include "TypeScript/Defines.h" #include "TypeScript/MLIRLogic/MLIRTypeHelper.h" +#include "OwnershipFacts.h" + #include "llvm/ADT/MapVector.h" #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallPtrSet.h" @@ -23,6 +25,8 @@ namespace mlir_ts = mlir::typescript; namespace { +using namespace own_facts; + // Ownership inference for -mm=own, phases 0 and 1. See // docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 4.2, 10.6, 11 and 12. // @@ -53,10 +57,14 @@ 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; + // reads out of fields and elements, and results that borrow an argument, and what may + // destroy what they read (spec 2.3, 2.4) + llvm::SmallVector placeReads; + // calls whose callee keeps an argument: each such argument moves into the call + llvm::SmallVector keepingCalls; drops.clear(); derivedCache.clear(); + returnsBorrowOf = resultBorrows(f); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { @@ -110,6 +118,15 @@ class OwnershipInferencePass : public mlir::PassWrappergetNumResults() == 1 && placeReadOf(op->getResult(0)) == op) + { + placeReads.push_back(op); + } + + if (!ownedParams(op).empty()) + { + keepingCalls.push_back(op); + } } for (auto result : op->getResults()) @@ -145,6 +162,7 @@ class OwnershipInferencePass : public mlir::PassWrapper> slotReceivers; + llvm::MapVector> paramReceivers; for (auto *op : retains) { auto value = retainedValue(op); @@ -159,27 +177,98 @@ class OwnershipInferencePass : public mlir::PassWrapper= 0 && borrowedParam(value) == returnsBorrowOf) + { + if (returnedParams.insert(value).second) + { + checkReturnedParam(value, toErase); + } + + continue; + } + if (auto load = value ? slotLoadOf(value) : mlir_ts::LoadOp()) { slotReceivers[load.getReference()].push_back({op, value, load}); continue; } + if (value && isReadOutOfException(value)) + { + toErase.insert(op); + continue; + } + if (!value || isFresh(value) == false) { reportSecondReference(op); } } + // An argument a callee keeps moves into the call (spec 2.4): out of an owning local or a + // parameter this function keeps, like any other taker; a fresh value is decided with the + // other takers of it, a read out of a container by its own check. Anything else is not + // this function's to give. + for (auto *call : keepingCalls) + { + auto args = callArgs(call); + for (auto index : ownedParams(call)) + { + if (static_cast(index) >= args.size() || holdsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto arg = args[index]; + auto root = rootOf(arg); + if (auto load = slotLoadOf(arg)) + { + slotReceivers[load.getReference()].push_back({call, root, load}); + } + else if (auto load = keptParamLoadOf(arg)) + { + paramReceivers[load.getReference()].push_back({call, root, load}); + } + else if (!isFresh(root) && !placeReadOf(root)) + { + reportGivenNotOwned(call, arg); + } + } + } + for (auto &entry : slotReceivers) { decideSlot(entry.first, entry.second, toErase); } + for (auto &entry : paramReceivers) + { + decideParam(entry.first, entry.second, toErase); + } + for (auto *op : toErase) { op->erase(); } + + // the facts were for this pass only + returnedParams.clear(); + f.walk([](mlir::Operation *op) { + for (auto *name : {OWN_PARAMS_ATTR_NAME, OWN_RESULT_BORROWS_ATTR_NAME, OWN_NO_DROPS_ATTR_NAME, + OWN_FACTS_LOST_ATTR_NAME}) + { + op->removeAttr(name); + } + }); } private: @@ -196,6 +285,11 @@ class OwnershipInferencePass : public mlir::PassWrapper returnedParams; + struct SlotReceiver { mlir::Operation *retain; @@ -220,7 +314,12 @@ class OwnershipInferencePass : public mlir::PassWrapper receivers, llvm::SetVector &toErase) + { + auto name = varName(slot.getDefiningOp()); + auto &receiver = receivers.front(); + auto *taker = takerOf(receiver.retain, receiver.value); + if (receivers.size() > 1) + { + auto &second = receivers[1]; + auto *secondTaker = takerOf(second.retain, second.value); + reportUseAfterMove(secondTaker ? secondTaker : second.retain, taker ? taker : receiver.retain, name); + return; + } + + if (!checkSlotMove(receiver.retain, receiver.value, receiver.load, toErase)) + { + return; + } + + auto &body = getFunction().getBody(); + auto *moved = body.findAncestorOpInRegion(*taker); + auto everyPath = true; + getFunction().walk([&](mlir_ts::ReturnInternalOp returnOp) { + auto *exit = body.findAncestorOpInRegion(*returnOp); + if (everyPath && (!moved || !exit || !dominance->dominates(moved, exit))) + { + reportKeptOnSomePaths(taker, returnOp, name); + everyPath = false; + } + }); + + if (everyPath && mlir::isa(receiver.retain)) + { + toErase.insert(receiver.retain); + } + } + + // The thrown value, read out of the exception object by a catch's copy thunk (`.eh.copy.*`, + // which the C++ runtime calls to copy the exception into the catch). rc retains it for the + // copy; own moves it: the exception object never gives its reference back (a throw hands the + // thrower's reference to it, and nothing releases it - rc leaks it the same way), and each + // exception is copied into one catch, since a TypeScript rethrow is a new throw. + bool isReadOutOfException(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); + return argument && argument.getOwner()->isEntryBlock() && getFunction().getSymName().starts_with(".eh.copy."); + } + + // The read of a parameter this function keeps, through its views. None for anything else. + mlir_ts::LoadOp keptParamLoadOf(mlir::Value value) + { + auto loadOp = rootOf(value).getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index) || !llvm::is_contained(ownedParams(getFunction()), index)) + { + return {}; + } + + return loadOp; + } + // 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. @@ -270,8 +434,10 @@ class OwnershipInferencePass : public mlir::PassWrapper(acquisition)) @@ -283,12 +449,16 @@ class OwnershipInferencePass : public mlir::PassWrapper(acquisition); } - if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || - varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME) || varOp.getCaptured().value_or(false)) + if (!varOp || !varOp.getInitializer() || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) { return {}; } + if (varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return consumed ? varOp : mlir_ts::VariableOp(); + } + auto retains = llvm::any_of(varOp.getResult().getUsers(), [](mlir::Operation *user) { return mlir::isa(user); }); return retains ? varOp : mlir_ts::VariableOp(); @@ -361,6 +531,9 @@ class OwnershipInferencePass : public mlir::PassWrapper kills; + // the function value an indirect call goes through: read when the call starts, not while + // the callee runs + bool callee = false; }; // A borrowing `let`'s slot: each read, and everything it leads to (walkBorrowed), joins @@ -472,9 +645,24 @@ class OwnershipInferencePass : public mlir::PassWrapper *bookkeeping; + bool returns; + }; + mlir::Operation *walkBorrowed( mlir::Value start, llvm::ArrayRef kills, llvm::SmallVectorImpl &uses, - llvm::function_ref)> isBorrower = nullptr) + llvm::function_ref)> isBorrower = nullptr, + Keeping keeping = Keeping{nullptr, false}) { struct Pending { @@ -492,7 +680,20 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) && borrowingCall(rootOf(pending.value))) || + (keeping.returns && mlir::isa(user)))) + { + keeping.bookkeeping->push_back(user); + continue; + } + + uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)}); + if (pending.borrowed && keeping.returns && mlir::isa(user)) + { + continue; + } + if (pending.borrowed) { llvm::SmallVector same; @@ -521,11 +722,12 @@ class OwnershipInferencePass : public mlir::PassWrappergetResults()) { if (mayPointInto(result) && seen.insert(result).second) { - values.push_back({result, false, pending.kills}); + values.push_back({result, !!borrowingCall(result), pending.kills}); } } } @@ -534,6 +736,31 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + { + return false; + } + + if (use.getOperandNumber() == 0) + { + return true; + } + + auto getThisOp = use.get().getDefiningOp(); + auto getMethodOp = user->getOperand(0).getDefiningOp(); + auto methodField = getThisOp ? getThisOp.getOperand().getDefiningOp() : mlir_ts::LoadOp(); + return use.getOperandNumber() == 1 && getMethodOp && methodField && + getThisOp.getOperand() == getMethodOp.getBoundFunc() && + mlir::isa(methodField.getReference().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) @@ -552,28 +779,31 @@ class OwnershipInferencePass : public mlir::PassWrapper(op); - } - - static bool isPlace(mlir::Value ref) + // A read of a heap value out of a field or an element, or a call whose result borrows an + // argument (spec 2.4, phase 4), seen through its views. Null for anything else. + mlir::Operation *placeReadOf(mlir::Value value) { - return mlir::isa_and_nonnull(ref.getDefiningOp()); - } + auto root = rootOf(value); + auto *def = root.getDefiningOp(); + if (!def || !ownsHeap(root)) + { + return nullptr; + } - // 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())) + if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) { - return {}; + return def; } - return loadOp; + return borrowingCall(root); + } + + // The call a value is the result of, when that result borrows an argument. + static mlir::Operation *borrowingCall(mlir::Value value) + { + auto *def = value.getDefiningOp(); + auto index = def && isCall(def) ? resultBorrows(def) : -1; + return index >= 0 && static_cast(index) < callArgs(def).size() ? def : nullptr; } // The places from a read up to its roots, and the roots: the values the function reached the @@ -591,6 +821,9 @@ class OwnershipInferencePass : public mlir::PassWrapper> places; llvm::SmallVector> roots; + // Somewhere under a root, but where is not known: a result that borrows an argument may + // be any field or element under it. Every overwrite and every removal drops it. + bool anyPlace = false; bool owned() const { @@ -598,31 +831,51 @@ class OwnershipInferencePass : public mlir::PassWrapper refs{read.getReference()}; + llvm::SmallVector refs; + llvm::SmallVector first; + if (auto loadOp = mlir::dyn_cast(read)) + { + refs.push_back(loadOp.getReference()); + } + else + { + chain.anyPlace = true; + first.push_back(rootOf(callArgs(read)[resultBorrows(read)])); + } + llvm::SmallPtrSet seen; - while (!refs.empty()) + while (!refs.empty() || !first.empty()) { - auto ref = refs.pop_back_val(); - mlir::Value base; - if (auto propertyRefOp = ref.getDefiningOp()) + llvm::SmallVector bases; + if (!first.empty()) { - chain.places.push_back({propertyRefOp.getPosition(), propertyRefOp.getType()}); - base = propertyRefOp.getObjectRef(); + bases.swap(first); } else { - auto elementRefOp = ref.getDefiningOp(); - chain.places.push_back({-1, elementRefOp.getType()}); - base = elementRefOp.getArray(); + 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(); + } + + bases.push_back(rootOf(base)); } - llvm::SmallVector bases{rootOf(base)}; while (!bases.empty()) { - base = bases.pop_back_val(); + auto base = bases.pop_back_val(); if (!seen.insert(base).second) { continue; @@ -640,6 +893,21 @@ class OwnershipInferencePass : public mlir::PassWrapper(base); argument && mergedInto(argument, bases)) { continue; @@ -663,47 +931,6 @@ class OwnershipInferencePass : public mlir::PassWrapper &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. @@ -722,9 +949,10 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) { - forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value) { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { auto varOp = mlir::dyn_cast(use); - if (mlir::isa(use) || (varOp && isOwningVariable(varOp))) + if (mlir::isa(use) || (varOp && isOwningVariable(varOp)) || + (isCall(use) && isKeptArgument(use, used))) { ends.push_back(use); } @@ -844,11 +1072,22 @@ class OwnershipInferencePass : public mlir::PassWrapper(drop)) { auto slot = releaseSlotOp.getSlot(); @@ -865,12 +1104,17 @@ class OwnershipInferencePass : public mlir::PassWrappergetResult(0)); auto sources = rootSources(chain); return llvm::any_of(drop->getOperands(), [&](mlir::Value operand) { return !fromRead.contains(operand) && llvm::any_of(sources, [&](mlir::Value source) { @@ -897,18 +1141,22 @@ class OwnershipInferencePass : public mlir::PassWrapper &toErase) + bool checkPlaceRead(mlir::Operation *read, llvm::SetVector &toErase) { - auto place = describePlace(read.getReference()); - llvm::StringRef name = ownerName(read.getResult()); + auto loadOp = mlir::dyn_cast(read); + auto place = loadOp ? describePlace(loadOp.getReference()) : describeCall(read); + auto result = read->getResult(0); + llvm::StringRef name = ownerName(result); 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); + // A result that borrows arrives with rc's temporary release, or rc's consuming `let`, and + // in a function returning a borrow of the same parameter it may go out with rc's retain. + Keeping keeping{&bookkeeping, returnsBorrowOf >= 0 && borrowedParam(result) == returnsBorrowOf}; + auto *kept = walkBorrowed(result, read, uses, [&](mlir::Operation *user, + llvm::ArrayRef kills) { + auto borrower = borrowerOf(user, /*consumed=*/true); if (!borrower || borrower.getOperation() != user) { return false; @@ -922,7 +1170,7 @@ class OwnershipInferencePass : public mlir::PassWrapperu`, the payload `ts.Unbox` reads out of an `any`), that + // this function returns as a borrow (`__own_result_borrows`). rc retained it for the caller, + // who now takes no reference: the retain goes, and nothing may keep it. The caller's own + // borrow of the argument bounds it; nothing here can end that but a call, and a call that may + // is the caller's use of what it passed (spec 2.4, phase 4). + void checkReturnedParam(mlir::Value value, llvm::SetVector &toErase) + { + llvm::SmallVector uses; + llvm::SmallVector bookkeeping; + if (auto *kept = walkBorrowed(value, {}, uses, nullptr, Keeping{&bookkeeping, true})) + { + reportBorrowEscapes(kept, ownerName(value), "a parameter"); + return; + } + + toErase.insert(bookkeeping.begin(), bookkeeping.end()); + } + + // `the result of 'first'`, for a call whose result borrows an argument. + static std::string describeCall(mlir::Operation *call) + { + auto callee = calleeName(call); + return callee.empty() ? "a call's result" : "the result of '" + callee + "'"; + } + + // The function a call names - directly, or the method it calls through a vtable - or empty. + static std::string calleeName(mlir::Operation *call) + { + mlir::StringAttr callee; + if (auto callOp = mlir::dyn_cast(call)) + { + callee = callOp.getCalleeAttr().getAttr(); + } + else if (auto calleeOp = call->getOperand(0).getDefiningOp()) + { + if (auto getMethodOp = mlir::dyn_cast(calleeOp)) + { + calleeOp = getMethodOp.getBoundFunc().getDefiningOp(); + } + + if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(calleeOp)) + { + callee = refOp.getIdentifierAttr().getAttr(); + } + } + + return callee ? callee.getValue().str() : std::string(); + } + // `'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) @@ -1103,69 +1410,6 @@ class OwnershipInferencePass : public mlir::PassWrapper(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)) - { - 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); - } - } - } - // 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. @@ -1180,44 +1424,6 @@ class OwnershipInferencePass : public mlir::PassWrapperhasAttr(OWNED_LOCAL_ATTR_NAME) || varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME); - } - - // Made here and held by nobody else: an allocation, a literal cast to its value type, an - // operation rc marks as arriving with a reference, or a direct call of a function this module - // defines (every function returns its result retained, rc 9.24; the call's own mark does not - // survive the affine lowering). A declared callee, an indirect call, a parameter, a load or a - // value merged from branches is not fresh. - static bool isFresh(mlir::Value value) - { - auto *def = value.getDefiningOp(); - if (!def) - { - return false; - } - - if (mlir::isa(def)) - { - return true; - } - - if (auto callOp = mlir::dyn_cast(def)) - { - auto callee = mlir::SymbolTable::lookupNearestSymbolFrom(def, callOp.getCalleeAttr()); - return callee && !callee.isDeclaration(); - } - - if (mlir::isa(def)) - { - return false; - } - - return def->hasAttr(OWNED_RESULT_ATTR_NAME) || isLiteral(def); - } - // Does a value of this type own a block that a release would destroy? bool ownsHeap(mlir::Value value) { @@ -1233,28 +1439,6 @@ class OwnershipInferencePass : public mlir::PassWrapper(); } - // A string literal or a constant array cast to its value type. The result is either the - // immortal global itself, which a release skips, or a copy nobody else holds, which a - // release destroys - so, owned once, it has one owner either way. `null` and `undefined` - // widened to a nullable type (`c: C | null = null`), and an empty optional - // (`u: C | undefined = undefined`), hold no block at all. - static bool isLiteral(mlir::Operation *def) - { - if (mlir::isa(def)) - { - return true; - } - - auto castOp = mlir::dyn_cast(def); - if (!castOp) - { - return false; - } - - auto *in = castOp.getIn().getDefiningOp(); - return in && mlir::isa(in); - } - // A use that reads the value without keeping it. Kept deliberately short: anything not listed // is treated as taking the value, which can only turn a program into an error. static bool isBorrow(mlir::Operation *user, mlir::Value value) @@ -1285,12 +1469,11 @@ class OwnershipInferencePass : public mlir::PassWrapper(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(user)) + // arguments are borrowed, but one the callee keeps (`__own_params`) is taken; a callee + // with no facts that keeps one retains it, and that retain is its own error + if (isCall(user)) { - return true; + return !isKeptArgument(user, value); } // the array being pushed onto is written, not taken; what is inserted is taken @@ -1318,6 +1501,15 @@ class OwnershipInferencePass : public mlir::PassWrapper(index) < args.size() && args[index] == value; + }); + } + // The moves out of a fresh or unknown SSA value. Each taking use (spec 11.1) is a move: // - nothing else may use the value after it - another taker included - and a taker that // control comes back to without the value being made again is a move on every iteration @@ -1592,26 +1784,6 @@ class OwnershipInferencePass : public mlir::PassWrapper(); - if (!loadOp) - { - return {}; - } - - auto varOp = loadOp.getReference().getDefiningOp(); - if (!varOp || !isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) - { - 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, 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) @@ -1621,6 +1793,12 @@ class OwnershipInferencePass : public mlir::PassWrapper(taker)) { return llvm::any_of(varOp.getResult().getUsers(), @@ -1845,7 +2029,58 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + auto diag = op->emitError("'") << name << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + noteLostFacts(diag); + signalPassFailure(); + } + + // Where the body would have been fine had its callers known a fact about it, say why they + // cannot. + void noteLostFacts(mlir::InFlightDiagnostic &diag) + { + auto f = getFunction(); + if (auto why = f->getAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) + { + diag.attachNote(f.getLoc()) << "'" << f.getSymName() << "' could return a borrow or keep a parameter, but " + << why.getValue(); + } + } + + void reportKeptOnSomePaths(mlir::Operation *move, mlir::Operation *exit, llvm::StringRef name) + { + if (quiet) + { + return; + } + + auto diag = move->emitError("'") << name + << "' is moved here on some paths only; -mm=own cannot release it " + "on the others yet"; + diag.attachNote(exit->getLoc()) << "returns here without moving it; the caller gave it up"; + signalPassFailure(); + } + + void reportGivenNotOwned(mlir::Operation *call, mlir::Value arg) + { + if (quiet) + { + return; + } + + llvm::StringRef name = ownerName(arg); + if (auto loadOp = rootOf(arg).getDefiningOp()) + { + if (auto varOp = loadOp.getReference().getDefiningOp()) + { + name = varName(varOp); + } + } + + auto callee = calleeName(call); + auto diag = call->emitError("argument '") << name << "' is given to '" << (callee.empty() ? "a function" : callee) + << "', which keeps it, but -mm=own cannot move it here: this " + "function does not own it"; + noteLostFacts(diag); signalPassFailure(); } @@ -1919,7 +2154,8 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + auto diag = op->emitError("'") << name << "' borrows " << place << " and cannot be stored, returned or captured"; + noteLostFacts(diag); signalPassFailure(); } @@ -1942,7 +2178,8 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + auto diag = op->emitError("'") << name << "' borrows '" << owner << "' and cannot be stored, returned or captured"; + noteLostFacts(diag); signalPassFailure(); } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp new file mode 100644 index 000000000..24bc9ced6 --- /dev/null +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -0,0 +1,971 @@ +#include "mlir/Pass/Pass.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" + +#include "TypeScript/TypeScriptDialect.h" +#include "TypeScript/TypeScriptOps.h" +#include "TypeScript/Passes.h" +#include "TypeScript/Defines.h" + +#include "OwnershipFacts.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" + +#include + +#define DEBUG_TYPE "own" + +namespace mlir_ts = mlir::typescript; + +namespace +{ + +using namespace own_facts; + +// The callee facts of -mm=own (spec 2.4, 3.2), computed over the whole module before the +// per-function inference runs. Each call the pass can resolve - a direct call, a virtual call +// whose candidates are all defined here, the `.instanceOf` slot - gets the facts its candidates +// agree on, pinned on the call op, so the inference reads them without looking anything up. +// +// `__own_no_drops` goes one way: a call with no facts may destroy anything, which can only turn a +// program into an error. The facts a callee's own body relies on - a parameter it keeps, a result +// that borrows an argument - go the other way: a caller that does not know them borrows the +// argument and releases it, or releases a result it does not own, a double free either way. So a +// function has those only when every call that can reach it is one this pass resolved (the closed +// world, see `open`). +class OwnershipSignaturePass : public mlir::PassWrapper> +{ + public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipSignaturePass) + + void runOnOperation() override + { + auto module = getOperation(); + module.walk([&](mlir_ts::FuncOp funcOp) { functions[funcOp.getSymName()] = funcOp; }); + collectClassVTables(module); + + module.walk([&](mlir::Operation *op) { + if (isCall(op) && op->getParentOfType()) + { + calls.push_back({op, resolve(op)}); + } + }); + + findOpen(module); + computeFacts(); + computeDrops(); + + for (auto &call : calls) + { + if (!call.callees.known) + { + continue; + } + + if (call.callees.instanceOf || + llvm::all_of(call.callees.funcs, [&](mlir_ts::FuncOp callee) { return noDrops.contains(callee); })) + { + call.op->setAttr(OWN_NO_DROPS_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + } + + private: + // What a call may reach: known, when every candidate is a function defined in this module (or + // the call goes through the `.instanceOf` slot); else anything. + struct Callees + { + bool known = false; + bool instanceOf = false; + llvm::SmallVector funcs; + }; + + struct Call + { + mlir::Operation *op; + Callees callees; + }; + + llvm::StringMap functions; + llvm::SmallVector calls; + + // Class names, from their vtables, to split a method's symbol into class and method name. + llvm::StringSet<> classNames; + // (vtable position, method name) -> the functions at that position under that name, in every + // class vtable of the module: what a virtual call through that slot may reach. + std::map, llvm::SmallVector> families; + + // Functions whose callers are not all known - a caller outside the module, or a reference to + // the symbol that is not a call this pass resolves - and why, for the error in their body. + llvm::DenseMap open; + + // The parameter a function's result borrows and the parameters it keeps (spec 2.4); absent is + // rc's convention. + struct Facts + { + int borrows = -1; + llvm::SmallVector owned; + + bool empty() const + { + return borrows < 0 && owned.empty(); + } + + bool operator==(const Facts &other) const + { + return borrows == other.borrows && owned == other.owned; + } + + bool operator!=(const Facts &other) const + { + return !(*this == other); + } + }; + + // Each function's facts, from its body (local) and as its callers see them (effective: closed, + // and agreed by every family it is in). + llvm::DenseMap factsLocal; + llvm::DenseMap factsEffective; + + // Functions that destroy nothing their caller can reach. + llvm::DenseSet noDrops; + + // A class's vtable is a global named `..vtbl` that holds its `..instanceOf`. An + // interface's vtable for a class (`B.I..vtbl`) holds only the methods and is not one: a call + // through an interface does not name what it reaches. + void collectClassVTables(mlir::ModuleOp module) + { + llvm::SmallVector>>> vtables; + module.walk([&](mlir_ts::GlobalOp globalOp) { + auto entries = classVTableEntries(globalOp); + if (!entries.empty()) + { + auto name = globalOp.getSymName(); + classNames.insert(name.drop_back(llvm::StringRef("..vtbl").size())); + vtables.push_back({name, std::move(entries)}); + } + }); + + for (auto &[name, entries] : vtables) + { + for (auto [position, symbol] : entries) + { + families[{position, methodName(symbol.getValue())}].push_back(symbol); + } + } + } + + // The functions a class vtable holds, by position; empty for any other global. + static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) + { + llvm::SmallVector> entries; + if (!globalOp.getSymName().ends_with("..vtbl")) + { + return entries; + } + + auto isClass = false; + globalOp.getInitializerRegion().walk([&](mlir_ts::InsertPropertyOp insertOp) { + auto symbolRefOp = insertOp.getValue().getDefiningOp(); + if (!symbolRefOp || insertOp.getPosition().size() != 1) + { + return; + } + + auto symbol = symbolRefOp.getIdentifierAttr().getAttr(); + isClass = isClass || symbol.getValue().ends_with("..instanceOf"); + entries.push_back({insertOp.getPosition()[0], symbol}); + }); + + if (!isClass) + { + entries.clear(); + } + + return entries; + } + + static bool isClassVTableEntry(mlir::Operation *op) + { + auto globalOp = op->getParentOfType(); + return globalOp && !classVTableEntries(globalOp).empty(); + } + + // `B.get` -> `get`: the symbol with the longest class name that prefixes it taken off. An + // override keeps its base's method name, which is what puts both in one family. + std::string methodName(llvm::StringRef symbol) + { + size_t best = 0; + for (auto &entry : classNames) + { + auto name = entry.getKey(); + if (name.size() > best && symbol.size() > name.size() && symbol.starts_with(name) && + symbol[name.size()] == '.') + { + best = name.size() + 1; + } + } + + return symbol.drop_front(best).str(); + } + + llvm::SmallVector familyOf(int64_t index, mlir::StringAttr identifier) + { + llvm::SmallVector members; + if (auto found = families.find({index, methodName(identifier.getValue())}); found != families.end()) + { + members = found->second; + } + + if (!llvm::is_contained(members, identifier)) + { + members.push_back(identifier); + } + + return members; + } + + // Adds `symbol`'s function to `callees`; false when it is not defined in this module. + bool addDefined(mlir::StringAttr symbol, Callees &callees) + { + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp || funcOp.isDeclaration()) + { + return false; + } + + if (!llvm::is_contained(callees.funcs, funcOp)) + { + callees.funcs.push_back(funcOp); + } + + return true; + } + + Callees resolve(mlir::Operation *op) + { + Callees callees; + if (auto callOp = mlir::dyn_cast(op)) + { + callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees); + } + else if (auto callOp = mlir::dyn_cast(op)) + { + callees.known = addDefined(callOp.getCalleeAttr().getAttr(), callees); + } + else if (mlir::isa(op)) + { + resolveValue(op->getOperand(0), callees); + } + + if (!callees.known) + { + callees.funcs.clear(); + } + + return callees; + } + + // The function value a call goes through: a method of a class, virtual or not, a symbol, or + // the `.instanceOf` slot. Anything else - a closure, a function read from a field, an + // interface's method - is unknown. + void resolveValue(mlir::Value callee, Callees &callees) + { + if (auto getMethodOp = callee.getDefiningOp()) + { + callee = getMethodOp.getBoundFunc(); + } + + auto *def = callee.getDefiningOp(); + mlir::StringAttr identifier; + int64_t index = -1; + if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (auto refOp = mlir::dyn_cast_or_null(def)) + { + identifier = refOp.getIdentifierAttr().getAttr(); + } + else if (isInstanceOfSlot(def)) + { + callees.known = true; + callees.instanceOf = true; + return; + } + + if (!identifier) + { + return; + } + + if (index < 0) + { + callees.known = addDefined(identifier, callees); + return; + } + + resolveFamily(index, identifier, callees); + } + + // A class another module can see may be extended there, and its override is a candidate this + // module never sees: every member has to be private and defined here. + void resolveFamily(int64_t index, mlir::StringAttr identifier, Callees &callees) + { + callees.known = true; + for (auto member : familyOf(index, identifier)) + { + callees.known = addDefined(member, callees) && callees.known; + } + + callees.known = callees.known && llvm::all_of(callees.funcs, [](mlir_ts::FuncOp funcOp) { return funcOp.isPrivate(); }); + } + + // `ts.Cast(ts.VTableOffsetRef(ts.VTableOffsetRef(object, 0), 0))`: the first slot of the + // vtable an object's first word points to. Only mlirGenInstanceOfOpaque (`instanceof` on an + // opaque object, as in `___unbox`) builds a call through it, to reach the class's generated + // `..instanceOf`, a string compare that destroys nothing. A class that implements an + // interface keeps the interface's vtable there instead, and that call crashes under every + // model - a bug of its own; it destroys nothing either way. + static bool isInstanceOfSlot(mlir::Operation *def) + { + auto castOp = mlir::dyn_cast_or_null(def); + auto slot = castOp ? castOp.getIn().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + auto vtable = slot ? slot.getVtable().getDefiningOp() : mlir_ts::VTableOffsetRefOp(); + return vtable && slot.getIndex() == 0 && vtable.getIndex() == 0; + } + + // Is every use of this function value the callee of a call - through `ts.GetMethod`, with + // `ts.GetThis` reading the object beside it? + static bool onlyCalled(mlir::Value value) + { + for (auto &use : value.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user) && use.getOperandNumber() == 0) + { + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && onlyCalled(user->getResult(0))) + { + continue; + } + + return false; + } + + return true; + } + + // The closed world. A function is open when a caller this pass cannot see may reach it: it is + // not private (another module may call it), or its symbol is used other than by a direct call, + // a class vtable's entry, or a method reference used only as a callee. A virtual reference + // that escapes opens its whole family. + void findOpen(mlir::ModuleOp module) + { + for (auto &entry : functions) + { + if (!entry.second.isPrivate()) + { + open.try_emplace(entry.second, "it can be called from another module"); + } + } + + // the module is a symbol table, which getSymbolUses does not look inside: walk its body + auto uses = mlir::SymbolTable::getSymbolUses(&module.getBodyRegion()); + if (!uses) + { + // an op this pass cannot read symbols through: nothing is closed + for (auto &entry : functions) + { + open.try_emplace(entry.second, "its callers cannot all be found"); + } + + return; + } + + for (auto &use : *uses) + { + auto symbol = use.getSymbolRef().getRootReference(); + auto funcOp = functions.lookup(symbol.getValue()); + if (!funcOp) + { + continue; + } + + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user) && isClassVTableEntry(user)) + { + continue; + } + + int64_t index = -1; + if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + else if (auto refOp = mlir::dyn_cast(user)) + { + index = refOp.getIndex(); + } + + auto inFunction = !!user->getParentOfType(); + if (inFunction && mlir::isa(user) && + onlyCalled(user->getResult(0))) + { + // a virtual call this pass cannot resolve reaches its family without the facts + if (index >= 0) + { + Callees callees; + resolveFamily(index, symbol, callees); + if (!callees.known) + { + openFamily(index, symbol, "an override may be defined in another module"); + } + } + + continue; + } + + open.try_emplace(funcOp, "it is used other than by a call"); + if (index >= 0) + { + openFamily(index, symbol, "it is used other than by a call"); + } + } + } + + void openFamily(int64_t index, mlir::StringAttr symbol, const char *why) + { + for (auto member : familyOf(index, symbol)) + { + if (auto memberOp = functions.lookup(member.getValue())) + { + open.try_emplace(memberOp, why); + } + } + } + + // ---- Results that borrow an argument ---- + + // The values a function returns: what is stored into the local its returns read (MLIRGen's + // result slot), or what a return hands back directly. + static llvm::SmallVector returnedValues(mlir_ts::FuncOp funcOp) + { + llvm::SmallVector values; + funcOp.walk([&](mlir_ts::ReturnInternalOp returnOp) { + for (auto operand : returnOp.getRetOperands()) + { + auto loadOp = operand.getDefiningOp(); + auto slot = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!slot || slot.getInitializer() || isOwningVariable(slot)) + { + values.push_back(operand); + continue; + } + + for (auto *user : slot.getResult().getUsers()) + { + auto storeOp = mlir::dyn_cast(user); + if (storeOp && storeOp.getReference() == slot.getResult()) + { + values.push_back(storeOp.getValue()); + } + } + } + }); + + return values; + } + + // The parameter every heap value the function returns borrows, or -1 (spec 2.4). A result + // that holds no block (`null`, a number) agrees with any. + static int localResultBorrows(mlir_ts::FuncOp funcOp) + { + auto found = -1; + for (auto value : returnedValues(funcOp)) + { + if (holdsNoBlock(value)) + { + continue; + } + + auto index = borrowedParam(value); + if (index < 0 || (found >= 0 && index != found)) + { + return -1; + } + + found = index; + } + + return found; + } + + // ---- Parameters the callee keeps ---- + + // Does `user` take `used` - a read of a parameter - into something that outlives the call: a + // store into a field, an element or a global, an insertion into an array, or a call that keeps + // it in turn? A local that holds it, and the result it is returned as, do not count: the first + // is a borrow, the second a borrowed result. + static bool keeps(mlir::Operation *user, mlir::Value used) + { + // a number, a boolean, a string literal: nothing to keep + if (holdsNoBlock(used)) + { + return false; + } + + if (auto storeOp = mlir::dyn_cast(user)) + { + auto ref = storeOp.getReference(); + return storeOp.getValue() == used && (isPlace(ref) || ref.getDefiningOp()); + } + + if (auto pushOp = mlir::dyn_cast(user)) + { + return llvm::is_contained(pushOp.getItems(), used); + } + + if (auto unshiftOp = mlir::dyn_cast(user)) + { + return llvm::is_contained(unshiftOp.getItems(), used); + } + + if (auto spliceOp = mlir::dyn_cast(user)) + { + return llvm::is_contained(spliceOp.getItems(), used); + } + + if (isCall(user)) + { + auto args = callArgs(user); + return llvm::any_of(ownedParams(user), [&](int32_t index) { + return static_cast(index) < args.size() && args[index] == used; + }); + } + + return false; + } + + // The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot - + // never assigned - that something keeps. + static llvm::SmallVector localOwnedParams(mlir_ts::FuncOp funcOp) + { + llvm::SmallVector owned; + if (funcOp.getBody().empty()) + { + return owned; + } + + for (auto argument : funcOp.getBody().front().getArguments()) + { + auto kept = false; + for (auto *user : argument.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + auto index = -1; + if (!varOp || !isParameterSlot(varOp, index)) + { + continue; + } + + for (auto *slotUser : varOp.getResult().getUsers()) + { + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + forEachUse(loadOp.getResult(), [&](mlir::Operation *use, mlir::Value used) { + kept = kept || keeps(use, used); + }); + } + } + } + + if (kept) + { + owned.push_back(argument.getArgNumber()); + } + } + + return owned; + } + + // ---- The facts callers must know ---- + + // Owned parameters and a result that borrows only hold where every caller knows them, so the + // facts survive only on a closed function whose every family agrees, and families are + // demoted until they do: a member that loses its facts can make another family disagree. A + // caller that returns what such a call returns, or passes a parameter on to a kept one, gets + // the fact too, so the facts are recomputed with the calls pinned until nothing changes; if + // that does not settle, nobody gets any. + void computeFacts() + { + llvm::DenseMap pinned; + for (auto round = 0; round < 16; ++round) + { + for (auto &entry : functions) + { + if (!entry.second.isDeclaration()) + { + factsLocal[entry.second] = {localResultBorrows(entry.second), localOwnedParams(entry.second)}; + } + } + + meetFacts(); + + llvm::DenseMap next; + for (auto &call : calls) + { + if (auto facts = callFacts(call.callees); !facts.empty()) + { + next[call.op] = facts; + } + } + + auto settled = next == pinned; + pinned = std::move(next); + pinFacts(pinned); + if (settled) + { + return; + } + } + + factsEffective.clear(); + pinFacts(llvm::DenseMap()); + } + + void meetFacts() + { + factsEffective.clear(); + for (auto &[funcOp, facts] : factsLocal) + { + if (!facts.empty() && !open.contains(funcOp)) + { + factsEffective[funcOp] = facts; + } + } + + for (auto changed = true; changed;) + { + changed = false; + for (auto &[key, members] : families) + { + llvm::SmallVector seen; + for (auto member : members) + { + auto funcOp = functions.lookup(member.getValue()); + seen.push_back(funcOp ? factsEffective.lookup(funcOp) : Facts()); + } + + if (llvm::all_equal(seen)) + { + continue; + } + + for (auto member : members) + { + if (auto funcOp = functions.lookup(member.getValue()); funcOp && factsEffective.erase(funcOp)) + { + changed = true; + } + } + } + } + } + + Facts callFacts(const Callees &callees) + { + if (!callees.known || callees.instanceOf || callees.funcs.empty()) + { + return {}; + } + + auto facts = factsEffective.lookup(callees.funcs.front()); + auto agree = llvm::all_of(callees.funcs, [&](mlir_ts::FuncOp callee) { + return factsEffective.lookup(callee) == facts; + }); + return agree ? facts : Facts(); + } + + static void setFacts(mlir::Operation *op, const Facts &facts) + { + auto *context = op->getContext(); + op->removeAttr(OWN_RESULT_BORROWS_ATTR_NAME); + op->removeAttr(OWN_PARAMS_ATTR_NAME); + if (facts.borrows >= 0) + { + op->setAttr(OWN_RESULT_BORROWS_ATTR_NAME, + mlir::IntegerAttr::get(mlir::IntegerType::get(context, 32), facts.borrows)); + } + + if (!facts.owned.empty()) + { + op->setAttr(OWN_PARAMS_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, facts.owned)); + } + } + + // The calls get the facts their callees agree on; each function gets its own, or the reason it + // lost those its body has, for the error there. + void pinFacts(const llvm::DenseMap &pinned) + { + for (auto &call : calls) + { + setFacts(call.op, pinned.lookup(call.op)); + } + + for (auto &[funcOp, local] : factsLocal) + { + auto effective = factsEffective.lookup(funcOp); + setFacts(funcOp, effective); + funcOp->removeAttr(OWN_FACTS_LOST_ATTR_NAME); + if (effective.empty() && !local.empty()) + { + auto *why = open.lookup(funcOp); + funcOp->setAttr(OWN_FACTS_LOST_ATTR_NAME, + mlir::StringAttr::get(&getContext(), why ? why : "an override in its class family disagrees")); + } + } + } + + // ---- Drops ---- + + // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. + void computeDrops() + { + llvm::DenseMap> callsIn; + for (auto &call : calls) + { + callsIn[call.op->getParentOfType()].push_back(&call); + } + + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (!funcOp.isDeclaration() && !dropsInBody(funcOp)) + { + noDrops.insert(funcOp); + } + } + + for (auto changed = true; changed;) + { + changed = false; + for (auto &entry : functions) + { + auto funcOp = entry.second; + if (!noDrops.contains(funcOp)) + { + continue; + } + + for (auto *call : callsIn[funcOp]) + { + if (!callMayDrop(call->callees)) + { + continue; + } + + noDrops.erase(funcOp); + changed = true; + break; + } + } + } + } + + bool callMayDrop(const Callees &callees) + { + if (!callees.known) + { + return true; + } + + return !callees.instanceOf && + llvm::any_of(callees.funcs, [&](mlir_ts::FuncOp callee) { return !noDrops.contains(callee); }); + } + + // Does the body itself destroy something a caller may reach: overwrite a field or an element + // of a block it did not make, assign a global, remove elements from an array it did not make, + // or `delete`? A local's own releases are not drops, nor is the constructor's filling of the + // object it is building. + bool dropsInBody(mlir_ts::FuncOp funcOp) + { + auto drops = false; + funcOp.walk([&](mlir::Operation *op) { + if (drops) + { + return; + } + + if (auto releaseSlotOp = mlir::dyn_cast(op)) + { + auto slot = releaseSlotOp.getSlot(); + drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)); + } + else if (mlir::isa(op)) + { + drops = reachesOutside(op->getOperand(0), funcOp); + } + else if (mlir::isa(op)) + { + drops = true; + } + }); + + return drops; + } + + // Can the block this reference points into be one the function's caller can reach? Walks up + // from a place to what holds it. Not when every way up ends in a block the function made (an + // allocation, a call's result it owns) or in an owning local that only ever held such blocks, + // or, in a constructor, in the object being built. + bool reachesOutside(mlir::Value ref, mlir_ts::FuncOp funcOp) + { + auto isConstructor = funcOp.getSymName().ends_with(".constructor"); + llvm::SmallVector work{ref}; + llvm::DenseSet seen; + while (!work.empty()) + { + auto value = rootOf(work.pop_back_val()); + if (!seen.insert(value).second) + { + continue; + } + + if (auto object = boundThis(value)) + { + work.push_back(object); + continue; + } + + if (auto propertyRefOp = value.getDefiningOp()) + { + work.push_back(propertyRefOp.getObjectRef()); + continue; + } + + if (auto elementRefOp = value.getDefiningOp()) + { + work.push_back(elementRefOp.getArray()); + continue; + } + + if (auto argument = mlir::dyn_cast(value)) + { + llvm::SmallVector merged; + if (mergedInto(argument, merged)) + { + work.append(merged.begin(), merged.end()); + continue; + } + + if (isConstructor && argument.getOwner()->isEntryBlock() && argument.getArgNumber() == 0) + { + continue; // the object being built + } + + return true; + } + + if (auto varOp = value.getDefiningOp()) + { + if (holdsOnlyFresh(varOp)) + { + continue; + } + + // a parameter's slot: what the caller passed + if (auto init = varOp.getInitializer()) + { + if (auto argument = mlir::dyn_cast(init); + argument && isConstructor && argument.getOwner()->isEntryBlock() && argument.getArgNumber() == 0 && + !isAssigned(varOp)) + { + continue; + } + } + + return true; + } + + if (auto loadOp = value.getDefiningOp()) + { + work.push_back(loadOp.getReference()); + continue; + } + + if (isFresh(value)) + { + continue; + } + + return true; + } + + return false; + } + + static bool isAssigned(mlir_ts::VariableOp varOp) + { + return llvm::any_of(varOp.getResult().getUsers(), [&](mlir::Operation *user) { + auto storeOp = mlir::dyn_cast(user); + return storeOp && storeOp.getReference() == varOp.getResult(); + }); + } + + // An owning, uncaptured local whose every value - its initializer and each assignment - is one + // the function made. + static bool holdsOnlyFresh(mlir_ts::VariableOp varOp) + { + if (!isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return false; + } + + if (auto init = varOp.getInitializer(); init && !isFresh(rootOf(init))) + { + return false; + } + + return llvm::all_of(varOp.getResult().getUsers(), [&](mlir::Operation *user) { + auto storeOp = mlir::dyn_cast(user); + return !storeOp || storeOp.getReference() != varOp.getResult() || isFresh(rootOf(storeOp.getValue())); + }); + } +}; + +} // end anonymous namespace + +#undef DEBUG_TYPE + +std::unique_ptr mlir_ts::createOwnershipSignaturePass() +{ + return std::make_unique(); +} diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 8fb9333d9..1fb3a54b1 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2408,7 +2408,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_union_nullable own_union_tagged own_union_optional own_field_borrow own_element_borrow own_container_loop own_optional_access own_borrow_merge - own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call) + own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call + own_call_no_drops own_getter_borrow own_param_kept own_any_box) 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. @@ -2473,6 +2474,20 @@ set(own_error_cases "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_call_drops_indirectly|borrows .* but is used here after it may be released or overwritten" + "own_err_call_given_borrow|borrows .* but is used here after it may be released or overwritten" + "own_err_call_this_borrow|borrows .* but is used here after it may be released or overwritten" + "own_err_exported_virtual_call|borrows .* but is used here after it may be released or overwritten" + "own_err_borrowed_result_stored|borrows the result of 'firstOf' and cannot be stored, returned or captured" + "own_err_borrowed_result_outlives|borrows the result of 'firstOf' but is used here after it may be released or overwritten" + "own_err_borrowed_result_let_outlives|borrows the result of 'H.first' but is used here after it may be released or overwritten" + "own_err_borrowed_result_escapes|could return a borrow or keep a parameter, but it is used other than by a call" + "own_err_borrowed_result_family|an override in its class family disagrees" + "own_err_param_kept_used|is used here after its value was moved" + "own_err_any_unboxed_outlives|borrows the result of '___unbox<.*>' but is used here after it may be released or overwritten" + "own_err_param_kept_some_paths|is moved here on some paths only" + "own_err_param_kept_loop|is moved inside a loop" + "own_err_param_kept_not_owned|is given to 'keep', which keeps it, but -mm=own cannot move it here" "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_any_box.ts b/tslang/test/tester/own/own_any_box.ts new file mode 100644 index 000000000..377afb4cf --- /dev/null +++ b/tslang/test/tester/own/own_any_box.ts @@ -0,0 +1,35 @@ +// -mm=own, phase 4: an `any` box owns what it holds. Boxing moves the value in and makes a box +// with one owner - a `let`, a folded `const`, or the temporary passed to a call - and unboxing +// (`___unbox`, whose result borrows its argument) borrows the payload for as long as the box +// lives. Every value is read after a `churn()`. +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 kind(v: any) { + return typeof v == "class" ? 1 : 0; +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + let a: any = new C(i % 10); + const b: any = new C(1); + const ca = a; + churn(); + t += ca.x + (b).x + kind(new C(2)) + kind(a); + } + + assert(t == 750000); + print("done."); +} diff --git a/tslang/test/tester/own/own_call_no_drops.ts b/tslang/test/tester/own/own_call_no_drops.ts new file mode 100644 index 000000000..23ca37555 --- /dev/null +++ b/tslang/test/tester/own/own_call_no_drops.ts @@ -0,0 +1,50 @@ +// -mm=own, phase 4: a borrow of a field of `this` or of a parameter survives a call to a function +// that destroys nothing its caller can reach (`__own_no_drops`): a method that reads numbers, a +// helper that builds and fills its own objects. Phase 3 dropped it at any call. +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 S { + c: C = new C(0); + w: number = 2; + area() { + return this.w * 2; + } + + total() { + const c = this.c; + const a = this.area(); + churn(); + return c.x + c.v.length + a; + } +} + +function sum(s: S) { + const c = s.c; + churn(); + return c.x + s.area(); +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + const s = new S(); + s.c = new C(i % 10); + s.c.v.push(1); + t += s.total() + sum(s); + } + + assert(t == 1800000); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_any_unboxed_outlives.ts b/tslang/test/tester/own/own_err_any_unboxed_outlives.ts new file mode 100644 index 000000000..4296d3c57 --- /dev/null +++ b/tslang/test/tester/own/own_err_any_unboxed_outlives.ts @@ -0,0 +1,12 @@ +// -mm=own, phase 4 rejects: `c` is the payload of the box `a` holds, borrowed through `___unbox`; +// assigning `a` destroys the box and its payload before `c.x` reads it. +class C { + constructor(public x: number) {} +} + +function main() { + let a: any = new C(1); + const c = a; + a = new C(2); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_borrowed_result_escapes.ts b/tslang/test/tester/own/own_err_borrowed_result_escapes.ts new file mode 100644 index 000000000..00c0a6fe2 --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_escapes.ts @@ -0,0 +1,23 @@ +// -mm=own, phase 4 rejects: `firstOf` is passed as a value, so `apply` calls it where -mm=own +// cannot see which function it is, and would take the result as its own and release it. +// `firstOf`'s body keeps rc's convention and fails, saying why. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); +} + +function firstOf(h: H) { + return h.c; +} + +function apply(g: (h: H) => C, h: H) { + return g(h).x; +} + +function main() { + const h = new H(); + print(apply(firstOf, h)); +} diff --git a/tslang/test/tester/own/own_err_borrowed_result_family.ts b/tslang/test/tester/own/own_err_borrowed_result_family.ts new file mode 100644 index 000000000..bf084adda --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_family.ts @@ -0,0 +1,23 @@ +// -mm=own, phase 4 rejects: `B.get` returns a field, but its override `D.get` returns a new +// object, so a call through `B` cannot know whether it owns the result. +class C { + constructor(public x: number) {} +} + +class B { + c: C = new C(1); + get() { + return this.c; + } +} + +class D extends B { + get() { + return new C(2); + } +} + +function main() { + const b: B = new D(); + print(b.get().x); +} diff --git a/tslang/test/tester/own/own_err_borrowed_result_let_outlives.ts b/tslang/test/tester/own/own_err_borrowed_result_let_outlives.ts new file mode 100644 index 000000000..ba1c9023a --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_let_outlives.ts @@ -0,0 +1,19 @@ +// -mm=own, phase 4 rejects: `b` is declared from a method's borrowed result, then the owner of +// the field it borrows is assigned. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); + first() { + return this.c; + } +} + +function main() { + let h = new H(); + let b = h.first(); + h = new H(); + print(b.x); +} diff --git a/tslang/test/tester/own/own_err_borrowed_result_outlives.ts b/tslang/test/tester/own/own_err_borrowed_result_outlives.ts new file mode 100644 index 000000000..234f2690a --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_outlives.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 4 rejects: `c` borrows what `firstOf(h)` returned, a field under `h`; the +// overwrite of `h.c` destroys it before `c.x` reads it. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); +} + +function firstOf(h: H) { + return h.c; +} + +function main() { + const h = new H(); + const c = firstOf(h); + h.c = new C(9); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_borrowed_result_stored.ts b/tslang/test/tester/own/own_err_borrowed_result_stored.ts new file mode 100644 index 000000000..f3e30d3fc --- /dev/null +++ b/tslang/test/tester/own/own_err_borrowed_result_stored.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 4 rejects: `firstOf(h)` borrows `h`, so it cannot be stored into another +// object, which would outlive `h`'s hold on it. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); +} + +function firstOf(h: H) { + return h.c; +} + +function main() { + const h = new H(); + const h2 = new H(); + h2.c = firstOf(h); + print(h2.c.x); +} diff --git a/tslang/test/tester/own/own_err_call_drops_indirectly.ts b/tslang/test/tester/own/own_err_call_drops_indirectly.ts new file mode 100644 index 000000000..68a71bd83 --- /dev/null +++ b/tslang/test/tester/own/own_err_call_drops_indirectly.ts @@ -0,0 +1,29 @@ +// -mm=own, phase 4 rejects: `reset` destroys nothing itself, but it calls `clear`, which assigns +// the global that owns what `c` borrows. The fact goes up the call graph. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); +} + +let g = new H(); + +function clear() { + g.c = new C(9); +} + +function reset() { + clear(); +} + +function read(h: H) { + const c = h.c; + reset(); + print(c.x); +} + +function main() { + read(g); +} diff --git a/tslang/test/tester/own/own_err_call_given_borrow.ts b/tslang/test/tester/own/own_err_call_given_borrow.ts new file mode 100644 index 000000000..a4c4df4da --- /dev/null +++ b/tslang/test/tester/own/own_err_call_given_borrow.ts @@ -0,0 +1,22 @@ +// -mm=own, phase 4 rejects: `use` is given `h.c`, then assigns the global that owns it and reads +// its parameter. A call given a borrow uses it for as long as the callee runs, so a callee that +// may destroy it is a use after the destroy. +class C { + constructor(public x: number) {} +} + +class H { + c: C = new C(0); +} + +let g = new H(); + +function use(c: C) { + g.c = new C(9); + print(c.x); +} + +function main() { + const h = g; + use(h.c); +} diff --git a/tslang/test/tester/own/own_err_call_this_borrow.ts b/tslang/test/tester/own/own_err_call_this_borrow.ts new file mode 100644 index 000000000..67765cec6 --- /dev/null +++ b/tslang/test/tester/own/own_err_call_this_borrow.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 4 rejects: `g.c.m()` calls `m` on the borrow of `g.c` itself. `m` assigns +// `g.c`, which destroys its own `this`, and then reads `this`. +class C { + constructor(public x: number) {} + m() { + g.c = new C(9); + print(this.x); + } +} + +class H { + c: C = new C(0); +} + +let g = new H(); + +function main() { + const h = g; + h.c.m(); +} diff --git a/tslang/test/tester/own/own_err_exported_virtual_call.ts b/tslang/test/tester/own/own_err_exported_virtual_call.ts new file mode 100644 index 000000000..9a937d6b6 --- /dev/null +++ b/tslang/test/tester/own/own_err_exported_virtual_call.ts @@ -0,0 +1,17 @@ +// -mm=own, phase 4 rejects: `S` is exported, so another module may extend it and override +// `area` with one that overwrites `color`. The virtual call has candidates this module cannot see. +export class S { + color: string = "red"; + w: number = 2; + area() { + return this.w * 2; + } + + describe() { + return `${this.color} area=${this.area()}`; + } +} + +function main() { + print(new S().describe()); +} diff --git a/tslang/test/tester/own/own_err_param_kept_loop.ts b/tslang/test/tester/own/own_err_param_kept_loop.ts new file mode 100644 index 000000000..faac8793d --- /dev/null +++ b/tslang/test/tester/own/own_err_param_kept_loop.ts @@ -0,0 +1,23 @@ +// -mm=own, phase 4 rejects: `c` is made once, outside the loop, and moved into `keep` on every +// iteration. +class C { + constructor(public x: number) {} +} + +class H { + c: C | null = null; +} + +function keep(h: H, c: C) { + h.c = c; +} + +function main() { + const h = new H(); + const c = new C(1); + for (let i = 0; i < 3; i++) { + keep(h, c); + } + + print(h.c!.x); +} diff --git a/tslang/test/tester/own/own_err_param_kept_not_owned.ts b/tslang/test/tester/own/own_err_param_kept_not_owned.ts new file mode 100644 index 000000000..f37db9bab --- /dev/null +++ b/tslang/test/tester/own/own_err_param_kept_not_owned.ts @@ -0,0 +1,21 @@ +// -mm=own, phase 4 rejects: `keep` keeps its parameter, but `main` passes a global, which it does +// not own and cannot move. +class C { + constructor(public x: number) {} +} + +class H { + c: C | null = null; +} + +let g = new C(1); + +function keep(h: H, c: C) { + h.c = c; +} + +function main() { + const h = new H(); + keep(h, g); + print(h.c!.x); +} diff --git a/tslang/test/tester/own/own_err_param_kept_some_paths.ts b/tslang/test/tester/own/own_err_param_kept_some_paths.ts new file mode 100644 index 000000000..d4fe30b81 --- /dev/null +++ b/tslang/test/tester/own/own_err_param_kept_some_paths.ts @@ -0,0 +1,21 @@ +// -mm=own, phase 4 rejects: `keepIf` keeps its parameter only when `f` is true, and has nothing +// to release it with on the other path. +class C { + constructor(public x: number) {} +} + +class H { + c: C | null = null; +} + +function keepIf(h: H, c: C, f: boolean) { + if (f) { + h.c = c; + } +} + +function main() { + const h = new H(); + keepIf(h, new C(1), true); + print(h.c!.x); +} diff --git a/tslang/test/tester/own/own_err_param_kept_used.ts b/tslang/test/tester/own/own_err_param_kept_used.ts new file mode 100644 index 000000000..ab2b39a96 --- /dev/null +++ b/tslang/test/tester/own/own_err_param_kept_used.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 4 rejects: `keep` keeps its parameter, so `a` moves into the call and cannot be +// read after it. +class C { + constructor(public x: number) {} +} + +class H { + c: C | null = null; +} + +function keep(h: H, c: C) { + h.c = c; +} + +function main() { + const h = new H(); + let a = new C(1); + keep(h, a); + print(a.x); +} diff --git a/tslang/test/tester/own/own_getter_borrow.ts b/tslang/test/tester/own/own_getter_borrow.ts new file mode 100644 index 000000000..02d2abc7e --- /dev/null +++ b/tslang/test/tester/own/own_getter_borrow.ts @@ -0,0 +1,78 @@ +// -mm=own, phase 4: a result that borrows an argument (`__own_result_borrows`). A getter, a method +// and a function returning a field of their argument, a method forwarding another's borrow, a +// field or `null`, and a function returning its parameter: the callee gives no reference, the +// caller takes none, and the result is bounded by the argument, read after a `churn()`. +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); + get cc() { + return this.c; + } + + first() { + return this.c; + } + + firstOrNull(k: number): C | null { + return k > 0 ? this.c : null; + } +} + +class W { + h: H = new H(); + inner() { + return this.h.first(); + } +} + +function firstOf(h: H) { + return h.c; +} + +function same(c: C) { + return c; +} + +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(1); + const a = firstOf(h); + let b = h.first(); + churn(); + t += a.x + b.x + h.cc.x + b.v.length; + + const w = new W(); + w.h.c = new C(1); + const d = w.inner(); + churn(); + t += d.x; + + const n = h.firstOrNull(i % 2); + if (n) { + t += n.x; + } + + let s = same(new C(2)); + churn(); + t += s.x + same(h.c).x; + } + + assert(t == 2450000); + print("done."); +} diff --git a/tslang/test/tester/own/own_param_kept.ts b/tslang/test/tester/own/own_param_kept.ts new file mode 100644 index 000000000..9c4816e02 --- /dev/null +++ b/tslang/test/tester/own/own_param_kept.ts @@ -0,0 +1,66 @@ +// -mm=own, phase 4: a callee that keeps a parameter (`__own_params`) - a function storing it into +// a field, one passing it on to that function, a method storing it into `this`, one pushing it, +// a constructor's parameter property - takes it over: the caller moves the argument in, from a +// temporary or a `let`, and gives nothing back. Every value is read after a `churn()`. +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 | null = null; + items: C[] = []; + set(c: C) { + this.c = c; + } +} + +class K { + constructor(public c: C) {} +} + +function keep(h: H, c: C) { + h.c = c; +} + +function forward(h: H, c: C) { + keep(h, c); +} + +function add(h: H, c: C) { + h.items.push(c); +} + +function main() { + let t: number = 0; + for (let i = 0; i < 100000; i++) { + const h = new H(); + keep(h, new C(i % 10)); + churn(); + t += h.c!.x; + + let a = new C(1); + a.v.push(1); + forward(h, a); + churn(); + t += h.c!.x + h.c!.v.length; + + h.set(new C(2)); + add(h, new C(3)); + const k = new K(new C(4)); + churn(); + t += h.c!.x + h.items[0].x + k.c.x; + } + + assert(t == 1550000); + print("done."); +} diff --git a/tslang/tslang/transform.cpp b/tslang/tslang/transform.cpp index 006699905..9fd3edbf7 100644 --- a/tslang/tslang/transform.cpp +++ b/tslang/tslang/transform.cpp @@ -144,6 +144,7 @@ int runMLIRPasses(mlir::MLIRContext &context, llvm::SourceMgr &sourceMgr, mlir:: // either erases a retain or reports it, and a program it reports stops here. if (compileOptions.memoryModel == MemoryModelOwn && !ownSkipInference) { + pm.addPass(mlir::typescript::createOwnershipSignaturePass()); pm.nest().addPass(mlir::typescript::createOwnershipInferencePass()); }