diff --git a/tslang/docs/superpowers/plans/2026-10-02-own-phase-7.md b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7.md new file mode 100644 index 000000000..cad8e6857 --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7.md @@ -0,0 +1,178 @@ +# `-mm=own` Phase 7 Implementation Plan: generators and async + +> **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 generator compiles when its ownership can be proven (spec §2.7, §7 +phase 7: "the state object as owner"). Today even `function* g() { yield 1; }` is an error, and +about 40 corpus files with a generator, an iterator object or a `.map`/`.filter` (which MLIRGen +builds as a generator) stop on one. + +After this phase: + +- A generator's **state object** is an ordinary owned block: its maker makes it and returns it, + and its caller owns it like any other result. +- The generator's **locals** are fields of the state object. A store into one is a move into the + container, a read is a borrow bounded by it, and reassignment releases the old value, as for any + field. +- A **yielded value** leaves `next` in its result, which the caller owns. +- A **captured variable** of a generator (a parameter, an outer local) is a cell in a box that the + state object holds: the escaping-closure rule of phase 5b, so a cell holding a number moves in + and a parameter's object is an error (7b decides whether it can borrow). +- **Async** already works under own and gets its test. + +**Architecture:** inference and signature passes only. MLIRGen does not change +(`test-own-mlirgen-matches-rc` keeps passing), and nothing new reaches the lowering in 7a. + +**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.1 (the state object is +a mortal block), §2.7, §7. Results go into §18. + +## Global Constraints + +- Branch `own-phase-7` from `origin/main` (8cfb0f17, with #437 merged). +- `gc`, `rc` and `none` output does not change. +- Never branch on the memory model in MLIRGen (§4.3). +- `__tslang_inc_ref` and `__tslang_dec_ref` never appear in own output. +- Own tests run with `--no-default-lib`, 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. +- Every corpus file that a change flips to "compiles" is run through test-runner `-mm=own`, AOT and + JIT, and its output compared with gc's, before the change is committed. +- 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. Build the + branch in WSL (GCC) before pushing. +- Source files are CRLF. Commits are GPG-signed, each ending with the session's `Co-Authored-By:`. + Commit before any temporary teeth switch. +- Check IR shapes with `--emit=mlir-affine --own-skip-inference`. +- `measure.ps1` reads `test/tester/compile.bat`, which only a test-runner run without `-mm` writes + (`ctest -C Release -R "^test-compile-00-print$"`). +- Baseline corpus on this tree: 325 of 591 compile under `-mm=own --no-default-lib`. + +## Shapes (measured on 8cfb0f17) + +- **The maker** `g` builds the initial state as a `ts.Constant` tuple + `{".step", ..., "next" = @.next, ".captured"?}` in a `ts.Variable`, loads it, and stores + it into `N = ts.New : !ts.value_ref`. Then it does `O = ts.Cast(N) : !ts.object`, + `ts.Retain(O)`, stores `O` into its result variable and returns it. The state's release routine + is generated from `T` and destroys every owning field and the `.captured` box (`tsrelcb_`). +- **The initial tuple** is `ts.Retain`ed before the store into `N` when `T` has an owning field (a + local of class type). The constant holds `unit` there, so the retain holds nothing. +- **`next`** takes the state as `%arg0` and keeps it in a `ts.Variable`. Each local is + `ts.PropertyRef(ts.Load(var), i)`, a store into it is a field store, and a reassignment is + `ts.ReleaseSlot` + `ts.Store`, as for any field. A `yield v` stores `v` into the `{value, done}` + result tuple, which rc `ts.Retain`s when `v` owns a block. +- **The caller** calls `next` through the state object: + `ts.CallInternal(ts.GetMethod(F), ts.GetThis(F))`, `F = ts.Load(ts.PropertyRef(S, next))`. The + signature pass does not resolve it, so the result is not fresh. +- **A captured variable** (a parameter, an outer local) is a captured `ts.Variable` cell, put in a + box (`ts.Variable {captured, __capture_box}`) and `ts.RetainCell`ed. The box is stored into the + initial tuple's `.captured` field, which goes into `N`. The frame's `ts.ReleaseCell` of the cell + follows the `ts.Retain(O)`. The box has no frame release, so it moves into the state already. +- **Async** functions lower to an `async.execute` region whose locals are ordinary locals. The + probe (a heap local across an `await`, a fresh object returned, a heap parameter read after an + `await`, a string) compiles and runs under own, AOT and JIT, with gc's output, at 5.8 MB (gc + 6.1, rc 5.7, none 334.5). +- **An object literal with methods** is the same `ts.New`/`ts.Cast` to `!ts.object` shape as + the state object. The cast of one to an interface is `ts.NewInterface {__owned_result}`. + +## Rulings made while planning + +- **The cast of a `ts.New`'d `value_ref` to its `!ts.object` is a view** (`isView`): the same block. + This is what makes the maker's result fresh. +- **`ts.NewInterface` is a view of the object it wraps**, and a view is exactly as fresh as its root. + The object-literal interface cast marks it `__owned_result`, and `isFresh` read that mark as a + block of its own: with the view above, `let raw = {...}; let i = raw` gave `raw`'s block two + owners (double free, found by the gate on `00object_annotated_method_extends_interface`), and + on `main` already a folded literal given twice as an interface (`use(o); use(o)`) is freed twice. + As a view, the interface is a receiver of its object: `let i = raw` borrows `raw` (rc's + `ts.Retain` of the interface is the consuming `let`'s reference, and a borrow erases it), and a + class instance behind an interface is one block (`own_err_interface` compiles). +- **A temporary is released once on each path.** Two releases of one block, one reachable after the + other, are two owners: the error at the second view. +- **A `ts.New`'d `value_ref` seen as its object owns a block** inside the pass (`ownsHeapMemory` + says no for any `value_ref`), so its root is checked, and a store through it (filling it) is a + borrow, not a taker. +- **The initial tuple's retain holds nothing.** A `ts.Retain` of a `ts.Load` of a `ts.Variable` whose + initializer is a `ts.Constant`, and whose owning fields are never stored into, is erased. +- **A call of a generator's `next` resolves to it.** A function read out of field `p` of an object of + tuple type `T` resolves to `@f` when, in this module, every `ts.Constant` of `T` holds `@f` at `p` + and nothing stores into field `p` of a `T`. The call then gets that function's facts, and its + result is fresh (`__own_fresh_result`). Cost if wrong: a fresh result that is someone else's. +- **A generator's capture box is an escaping closure's box.** `closureOfBox` learns the state + object: a box stored into `.captured` of a tuple that goes into a `ts.New`. Phase 5b's rules then + apply as they are: cells move in at the box's fill, the frame's `ts.ReleaseCell`s go, a frame + use after is an error, and a parameter's heap cell is the 5b error. +- **Phasing.** 7a: tasks 0-4. 7b, a parameter's object borrowed by the generator (its result + borrows the argument; a box that frees its cells but not their values) and the synthesized + `.map`/`.filter`, is scoped from the corpus after 7a. One PR each. + +## Review Focus + +1. The views and `ts.NewInterface`: every block still has one owner. An interface over a `let` borrows + it, a literal cast directly is one block, and two interfaces rc counts separately are an error. +2. `next` resolution: a field that can hold anything but the one function must stay unresolved. +3. The initial-tuple retain: erased only when nothing mortal can be in the tuple. + +--- + +### Task 0: the state object is a fresh block + +**Files:** `lib/TypeScript/OwnershipFacts.h` (`isView`, `isFresh`). + +- [x] `ts.Cast` from `!ts.value_ref` to `!ts.object` is a view. +- [x] `ts.NewInterface` is a view; a view is as fresh as its root; a consuming `let` of a view can + borrow. +- [x] A block released twice on one path is an error. +- [x] Gate: the flipped corpus files, the own ctest subset. + +**Tests:** positives `own_generator.ts` (numbers yielded through `for...of` and by hand), +`own_interface_view.ts`; negative `own_err_interface_two_views.ts`. `own_err_interface` and the +first commit's `own_err_object_literal_interface_alias` compile now and are gone. + +### Task 1: the initial state + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp`. + +- [x] Erase the initial tuple's `ts.Retain` when the tuple holds nothing mortal. + +**Tests:** `own_generator.ts`: a class local, reassigned between yields. + +### Task 2: the caller's `next` + +**Files:** `lib/TypeScript/OwnershipSignaturePass.cpp` (`resolveValue`), `lib/TypeScript/OwnershipFacts.h`. + +- [x] Resolve a function read from a field that only ever holds one function. +- [x] Pin `__own_fresh_result` on such a call to a defined function. +- [x] A record built to be returned (a local a `ts.Constant` initializes, read once, the read only + returned) takes fresh values into its fields: each store is the value's taker, and rc's + retain of the record is where it moves in. +- [x] The inference runs on the `func.func`s the async lowering outlines (a `for await` body, an + `await`'s continuation): a retain there was neither erased nor reported. + +**Tests:** `own_generator.ts`: yields of fresh objects and strings, kept by the caller past a +`churn()`. Negative: a field that is assigned another function stays unresolved +(`own_err_field_function_reassigned.ts`). + +### Task 3: captured variables + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp` (`closureOfBox`, `classifyClosures`). + +- [x] A box stored into the state's `.captured` is an escaping closure's box. + +**Tests:** `own_generator.ts`: number parameters. Negative: `own_err_generator_param_object.ts` +(the 5b error). A frame use after the capture cannot be written: the maker's body is the +generator's own, which only builds the state and returns it. An outer local captured by a nested +generator is an inherited cell, 5b's error. + +### Task 4: async, measure, teeth, corpus, spec + +- [x] `own_async.ts` from the probe, and `own_record_return.ts`. +- [x] `measure.ps1` for the positives under all four models. +- [x] Teeth: keep the frame's `ts.ReleaseCell` of a captured local after its move (the generator + reads a freed cell); let a reassigned field's function resolve (the negative compiles); + read every `ts.NewInterface {__owned_result}` as fresh (the alias negative compiles and + frees twice). +- [x] Corpus before/after; spec §18; memory. 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 e9f6b31bb..dd01a288c 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 @@ -5,8 +5,8 @@ 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) merged as #403 (results §14); phase 4 (function signatures and `any`) merged as #406-#410 (results §15); phase 5a (closures that do not escape) -merged as #436 (results §16); phase 5b (closures that escape) on branch `own-phase-5b` (results -§17). Plans in `docs/superpowers/plans/`. +merged as #436 (results §16); phase 5b (closures that escape) merged as #437 (results §17); phase +7a (generators, async) on branch `own-phase-7` (results §18). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -1180,3 +1180,148 @@ integer literal is `s32`. - A closure that escapes from a closure body with a cell it inherited, and a closure over a parameter's object, are errors. `this` captured by an escaping callback is the common case. - From §16.5: a non-escaping closure always borrows; a captured local is never `let`-borrowed. + +## 18. Phase 7a results, 2026-10-02 + +Plan: `docs/superpowers/plans/2026-10-02-own-phase-7.md`. Changed: `OwnershipFacts.h`, +`OwnershipInferencePass.cpp`, `OwnershipSignaturePass.cpp`, and `tslang/transform.cpp` (the +inference runs on `func.func` too). MLIRGen does not change. + +### 18.1 What phase 7a accepts + +A generator's maker builds its initial state as a `ts.Constant` tuple in a local, copies it into a +block `ts.New` makes, and returns that block cast to `!ts.object`. Its `next` keeps the +generator's locals in the state object's fields and returns a `{value, done}` record. The caller +calls `next` through the state object. + +- **The state object is a fresh block.** The cast of a `ts.New`'d `value_ref` to its `!ts.object` + is a view. Inside the pass such a block owns (`ownsHeapMemory` says no for any `value_ref`), so + its root is checked, and the store that fills it is a borrow, not a taker. +- **The initial state holds nothing.** rc's retain of the tuple is erased when its owning fields + hold what the constant put there: null, a number, an immortal literal. +- **Locals are fields.** A store into one is a move into the container, a read a borrow bounded + by it, and reassignment releases the old value (rc's `ts.ReleaseSlot` of the field). Nothing new + was needed. +- **A yield moves its value to the caller.** A record built to be returned (a local a + `ts.Constant` initializes, read once, the read only returned) takes the fresh values stored into + its fields: each store is the value's taker, and rc's retain of the filled record is where the + value moves in. A plain function returning `{ value: new C(), done: false }` is the same shape. +- **The caller's `next` resolves.** The signature pass resolves a function read out of an + object's field when the field only ever holds that function: every `ts.Constant` of the + object's tuple type holds it there, nothing stores into the field (a method sees its object as + `object_storage`, whose fields count too), and no other op makes a value of the type. The call + gets `__own_fresh_result`. `next` is open (its symbol sits in a constant), so its body may not + return a borrow. +- **What a generator captures is an escaping closure's box.** The maker puts the cells of its + parameters into a box and the box into the state's `.captured` field. That store is the box's + escape, and phase 5b's rules apply as they are. A cell holding a number moves in + (`range(from, to)`), and a parameter's object is 5b's "its value is the caller's" error. An + object literal whose methods capture keeps its box the same way. +- **Async.** The async lowering outlines a `for await` body and each `await`'s continuation into a + `func.func`, which the inference did not visit. A retain there was neither erased nor reported, + and the lowering failed with "ownership inference left a retain behind" (`00for_await_yield`). + The inference now runs on every function. An outlined body's arguments are the values it + captures: a retain of one is an error. The signature pass collects the calls in those bodies + too. It did not: a callee that keeps its argument (`stash(c)` pushing into a global) moved it + in its own body, while the `for await` body that called it kept its release of `c`, a use after + free on `main` as well. Verified in the IR only: the release is gone. A run cannot show it, + because `await f()` returns before `f`'s `for await` body has run (§18.5). + +### 18.2 An interface over an object is that object + +The view above showed that `ts.NewInterface` gave a block a second owner. The object-literal +interface cast marks it `__owned_result`, and `isFresh` took the mark at its word. +`let raw = {...}; let i = raw` freed `raw`'s block twice. On `main` already, a folded object +literal given twice as an interface (`use(o); use(o)`) is freed twice: it crashes there. A first +fix, fresh only when the object has no other use, lost 12 corpus files that compile on `main`; +the view below gets them back. + +`ts.NewInterface` is now a view of its object, as a union or an optional is, and a view is as fresh +as its root: + +- `let i = raw` borrows `raw`. rc's `ts.Retain` of the interface is the reference of the `let` + that consumes it, and a borrow erases it. +- A class instance behind an interface is one block, so the phase-0 placeholder + `own_err_interface` compiles and is gone. +- Two releases of one block, one reachable after the other, are two owners: the error at the + second view (`own_err_interface_two_views`). + +### 18.3 Measured + +AOT, `measure.ps1`, in MB: + +| test | gc | rc | none | own | +|---|---|---|---|---| +| `own_generator` | 6.1 | 11.2 | 1548.6 | 4.3 | +| `own_record_return` | 5.9 | 4.2 | 224.0 | 4.2 | +| `own_interface_view` | 5.9 | 4.3 | 551.1 | 4.3 | +| `own_async` | 6.1 | 5.7 | 334.7 | 5.8 | + +rc is above gc on `own_generator` because it leaks yielded objects (§18.5). + +### 18.4 Teeth + +Each made with a temporary `getenv` switch: + +- Keep the frame's `ts.ReleaseCell` of a cell after it moves into a generator's box: + `own_generator` fails, AOT (0xC0000374) and JIT. +- Keep a value's temporary release after it moves into a returned record: `own_record_return` + fails, AOT (0xC0000374) and JIT. In a generator's `next` there is no such release to keep + (§18.5). +- Skip the released-twice check: `own_err_interface_two_views` compiles and crashes. +- Let a field that is assigned another function resolve: `own_err_field_function_reassigned` + compiles. + +The JIT's object cache (`__jit` beside the source) keys on the compiler binary and the options, not +on the environment. Every JIT run with the teeth build after the first reused the first one's +object, so the cache was cleared between them. + +### 18.5 Tests and the corpus + +- **Positives:** + - `own_generator`: numbers, a class local, a local reassigned between yields, a generator given + up after one value, yields of fresh objects and strings through `for...of` and by hand, number + parameters, an object literal with a method; + - `own_record_return`; + - `own_interface_view`: a class instance as an interface, a `let` borrowed through one, a + literal given once, a literal made straight into one; + - `own_async`: a heap local across an `await`, a fresh result, a heap parameter after an + `await`, a string, a `for await` body. +- **Negatives:** `own_err_interface_two_views`, `own_err_field_function_reassigned`, + `own_err_generator_param_object`. +- The no-counting check's failure pattern is `error:` now: `error` alone matched the async + runtime's own message, "Awaited async operand is in error state". + +Corpus: 325 of 591 before, 359 after. Every file that newly compiles runs under test-runner +`-mm=own`, AOT and JIT, and so does every `-shared` pair whose two halves both compile. The +JIT output of each matches gc's, except the two async files (`00for_await`, `00for_await_yield`), +which the JIT runs only with the async runtime test-runner links: they pass under test-runner. Two files that compiled on `main` are errors now, both correctly: + +- `00object` stores one object literal into two tuples, which rc leaks. +- `00interface_object_array` reads an object after it moved into an array. + +Found, not fixed: + +- **A temporary made in front of a `yield`** in the generator's own body is never released, under + rc as well. `OwnedReturnConsumptionPass` does not release past a resume point, so + `yield "s" + i` leaks the number's string (50 MB over 200000 runs). +- **rc leaks yielded objects.** The call's reference and the record's retain are two, and the + caller releases one. +- **Reading a record out of a call or a folded `const`** (`print(f().value.x)`, + `const r = f(); r.value.x`) is an error. A `let` works. +- **A method of an object literal held in a `let`**, returning a block, called and kept, is a + borrow error: `let o = {make(): C {...}}; const c = o.make()`. A `const o` works. +- **`measure.ps1` reads `compile.bat`**, which only a test-runner run without `-mm` writes. +- **`await f()` returns before `f`'s `for await` body has run**, under every model: with + `for await (const x of g()) keep.push(1)`, `keep` is empty after it (`none` fails the assert, + `gc` too). + +### 18.6 Known limits (the input to 7b) + +- A generator over a parameter's object needs its result to borrow the argument, and a box that + frees its cells but not their values. +- A nested generator over an outer local inherits the cell: 5b's "captured from an enclosing + function" error. +- `.map`/`.filter`, which MLIRGen builds as generators over the source array and the callback, stop + on "borrows a field and cannot be stored". +- A yielded borrow (a parameter's element, a state field's value) is an error. diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index bbf2e8944..859e591ec 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -136,14 +136,14 @@ inline int resultBorrows(mlir::Operation *op) } // 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. +// optional or narrowed back, a union made from its payload or read back from it, an object seen +// through an interface (`{vtable, this}`, released through `this`). 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)) + mlir_ts::OptionalValueOp, mlir_ts::ValueOp, mlir_ts::NewInterfaceOp>(op)) { return true; } @@ -162,8 +162,13 @@ inline bool isView(mlir::Operation *op) auto isClosure = [](mlir::Type type) { return mlir::isa(type); }; + // a block `ts.New` made, seen as the object it is: a generator's state object + auto isMadeObject = [&]() { + return mlir::isa(castOp.getIn().getType()) && + mlir::isa(castOp.getType()); + }; return (keeps(castOp.getIn().getType()) && keeps(castOp.getType())) || - (isClosure(castOp.getIn().getType()) && isClosure(castOp.getType())); + (isClosure(castOp.getIn().getType()) && isClosure(castOp.getType())) || isMadeObject(); } // The block a value is a view of. @@ -245,6 +250,13 @@ inline bool isFresh(mlir::Value value) return false; } + // a view is the block it shows: the object literal's interface cast is marked as arriving with + // a reference, but `let raw = {...}; raw` is `raw`'s block + if (isView(def)) + { + return isFresh(rootOf(value)); + } + if (mlir::isa(def)) { diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 64fbe5770..8aec51520 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -2,6 +2,7 @@ #include "mlir/IR/Dominance.h" #include "mlir/Dialect/LLVMIR/LLVMAttrs.h" #include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "mlir/Interfaces/FunctionInterfaces.h" #include "TypeScript/TypeScriptDialect.h" #include "TypeScript/TypeScriptOps.h" @@ -47,12 +48,29 @@ using namespace own_facts; // move on every loop iteration (2.6), and each release the move reaches must be dominated by it // and is erased. A release the move cannot reach stays - the owner on the paths that did not // move. Anything else is an error at its location. -class OwnershipInferencePass : public mlir::PassWrapper +// +// It runs on every function: a `ts.Func`, and the `func.func` the async lowering outlines an +// `await`'s continuation or a `for await` body into, whose arguments are the values it captures. +class OwnershipInferencePass + : public mlir::PassWrapper> { public: MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OwnershipInferencePass) - void runOnFunction() override + void runOnOperation() override + { + if (!getFunction().isExternal()) + { + runOnFunction(); + } + } + + mlir::FunctionOpInterface getFunction() + { + return getOperation(); + } + + void runOnFunction() { auto f = getFunction(); mlir::DominanceInfo dominanceInfo(f); @@ -70,12 +88,14 @@ class OwnershipInferencePass : public mlir::PassWrapper captureRetains; // each closure over a cell, by its `ts.CreateBoundFunction` llvm::DenseMap closureOf; + // each generator's capture box, which escapes with its state object (classifyStateBoxes) + llvm::DenseMap stateBoxes; // Finds the closures over cells and what they borrow, before anything else: whether a box's // stores are reads or takers depends on it. A closure with copies only keeps what phase 4 does @@ -451,6 +479,119 @@ class OwnershipInferencePass : public mlir::PassWrapperhasAttr(CAPTURE_BOX_ATTR_NAME) || closureOfBox(box)) + { + return; + } + + auto closure = std::make_shared(); + closure->box = box; + llvm::SmallVector boxRetains; + for (auto *user : box.getUsers()) + { + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + return; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (mlir::isa(user)) + { + boxRetains.push_back(user); + continue; + } + + if (!closure->escape && isStoreIntoState(user, box)) + { + closure->escape = user; + continue; + } + + return; + } + + if (!closure->escape) + { + return; + } + + for (auto &fill : closure->fills) + { + if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + } + + for (auto *retain : boxRetains) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + stateBoxes[box] = closure.get(); + closures.push_back(std::move(closure)); + }); + } + + // `ts.Store(box, ts.PropertyRef(state, i))`, where `state` is a local a `ts.Constant` + // initializes whose every read fills a block `ts.New` made: a generator's initial state. + static bool isStoreIntoState(mlir::Operation *user, mlir::Value box) + { + auto storeOp = mlir::dyn_cast(user); + auto propertyRefOp = storeOp && storeOp.getValue() == box + ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto stateOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp || !isConstantRecordLocal(stateOp)) + { + return false; + } + + auto reads = 0; + for (auto *stateUser : stateOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(stateUser); + if (!loadOp) + { + continue; + } + + ++reads; + for (auto *readUser : loadOp->getUsers()) + { + auto fillOp = mlir::dyn_cast(readUser); + if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) + { + return false; + } + } + } + + return reads > 0; + } + // rc's reference for the box's copy of `value`: the retain of it nearest before the store that // fills the box, in the same block. mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) @@ -613,6 +754,12 @@ class OwnershipInferencePass : public mlir::PassWrapperop) + { + decideEscaping(*closure, toErase); // a generator's box + continue; + } + auto *boundOp = closure->op.getOperation(); auto name = ownerName(closure->op.getResult()); auto ok = true; @@ -748,10 +895,19 @@ class OwnershipInferencePass : public mlir::PassWrapper(op); auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!propertyRefOp || !mlir::isa(storeOp.getValue().getType())) + { + return false; + } + + if (stateBoxes.count(propertyRefOp.getObjectRef())) + { + return true; + } + + auto boundOp = closureOfBox(propertyRefOp.getObjectRef()); auto found = boundOp ? closureOf.find(boundOp.getOperation()) : closureOf.end(); - return found != closureOf.end() && found->second->escape && - mlir::isa(storeOp.getValue().getType()); + return found != closureOf.end() && found->second->escape; } // A closure that escapes owns what its box holds (spec 2.5, phase 5b). Its copies moved in as @@ -835,7 +991,14 @@ class OwnershipInferencePass : public mlir::PassWrapper gone(closure.ends.begin(), closure.ends.end()); gone.push_back(closure.escape); @@ -952,7 +1115,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); auto argument = loadOp ? mlir::dyn_cast(loadOp.getReference()) : mlir::BlockArgument(); - return argument && argument.getOwner()->isEntryBlock() && getFunction().getSymName().starts_with(".eh.copy."); + return argument && argument.getOwner()->isEntryBlock() && getFunction().getName().starts_with(".eh.copy."); } // The read of a parameter this function keeps, through its views. None for anything else. @@ -1041,7 +1204,7 @@ class OwnershipInferencePass : public mlir::PassWrapper(); } + else if (auto retainOp = mlir::dyn_cast(acquisition)) + { + // rc's reference for a `let` that takes the value over: `let ri = raw` retains the + // interface and declares `ri` consuming it + varOp = consumingLetOf(retainOp.getReference()); + consumed = true; + } else { varOp = mlir::dyn_cast(acquisition); @@ -1091,6 +1261,40 @@ class OwnershipInferencePass : public mlir::PassWrapper ends, + llvm::StringRef owner, llvm::SetVector &toErase) + { + if (!tryBorrow(borrower, ends, owner, toErase)) + { + return false; + } + + if (mlir::isa(acquisition)) + { + toErase.insert(acquisition); + } + + return true; + } + + // The owning `let` declared from `value` that takes over its reference, or none. + static mlir_ts::VariableOp consumingLetOf(mlir::Value value) + { + for (auto *user : value.getUsers()) + { + auto varOp = mlir::dyn_cast(user); + if (varOp && varOp.getInitializer() == value && varOp->hasAttr(OWNED_LOCAL_CONSUMED_ATTR_NAME)) + { + return varOp; + } + } + + return {}; + } + // Where an owning local stops holding its value: each release of its slot and each store // into it. static llvm::SmallVector slotEnds(mlir::Value slot) @@ -2078,9 +2282,185 @@ class OwnershipInferencePass : public mlir::PassWrapper() && mlir::isa(value.getType()) && + llvm::any_of(value.getUsers(), [](mlir::Operation *user) { return mlir::isa(user) && isView(user); })) + { + return true; + } + + return ownsHeap(value.getLoc(), value.getType()); + } + + bool ownsHeap(mlir::Location location, mlir::Type type) { MLIRTypeHelper mth(&getContext(), CompileOptions{}); - return mth.ownsHeapMemory(value.getLoc(), value.getType()); + return mth.ownsHeapMemory(location, type); + } + + // A tuple read out of a local that a `ts.Constant` initializes, whose owning fields hold what + // that constant put there - null, a number, an immortal literal - or a fresh value moved in: + // the initial state a generator's maker copies into its state object, or a record built to be + // returned (`return {value: new C(), done: false}`, a `yield`'s result). rc's retain of it is + // its reference for the values moved in, which own moves instead (recordRetainOf). + bool holdsConstantsOrMoves(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || !isConstantRecordLocal(varOp)) + { + return false; + } + + for (auto *user : varOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + // a method's field is a `!ts.bound_ref`, which holds a function, not a block + auto field = propertyRefOp.getResult(); + auto refType = mlir::dyn_cast(field.getType()); + for (auto *fieldUser : field.getUsers()) + { + if (mlir::isa(fieldUser)) + { + continue; + } + + if (refType && !ownsHeap(field.getLoc(), refType.getElementType())) + { + continue; + } + + // a fresh value moved in, its move decided by its own check against this retain + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != field || !isFresh(rootOf(storeOp.getValue())) || + recordRetainOf(varOp) == nullptr) + { + return false; + } + } + } + + return true; + } + + // A non-owning local a `ts.Constant` initializes, used only through its fields and its reads. + static bool isConstantRecordLocal(mlir_ts::VariableOp varOp) + { + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false) || !varOp.getInitializer() || + !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + return mlir::isa(user); + }); + } + + // rc's one `ts.Retain` of a record local's one read, where the read is only returned: stored + // into a local that owns nothing and is read only by returns (the result slot). It is the + // reference rc gives each fresh value stored into the record, so it is where they move in. + // Null for any other record. + static mlir::Operation *recordRetainOf(mlir_ts::VariableOp varOp) + { + if (!isConstantRecordLocal(varOp)) + { + return nullptr; + } + + mlir_ts::LoadOp read; + for (auto *user : varOp->getUsers()) + { + if (auto loadOp = mlir::dyn_cast(user)) + { + if (read) + { + return nullptr; + } + + read = loadOp; + } + } + + if (!read) + { + return nullptr; + } + + mlir::Operation *retain = nullptr; + for (auto *user : read->getUsers()) + { + if (mlir::isa(user)) + { + if (retain) + { + return nullptr; + } + + retain = user; + continue; + } + + if (mlir::isa(user)) + { + continue; + } + + auto storeOp = mlir::dyn_cast(user); + auto slot = storeOp && storeOp.getValue() == read.getResult() + ? storeOp.getReference().getDefiningOp() + : mlir_ts::VariableOp(); + if (!slot || !isResultSlot(slot)) + { + return nullptr; + } + } + + return retain; + } + + // A local that owns nothing, is never captured, and is read only to be returned. + static bool isResultSlot(mlir_ts::VariableOp varOp) + { + if (isOwningVariable(varOp) || varOp.getCaptured().value_or(false)) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [&](mlir::Operation *user) { + if (auto storeOp = mlir::dyn_cast(user)) + { + return storeOp.getReference() == varOp.getResult(); + } + + auto loadOp = mlir::dyn_cast(user); + return loadOp && llvm::all_of(loadOp->getUsers(), [](mlir::Operation *loadUser) { + return mlir::isa(loadUser); + }); + }); + } + + // The record whose field `taker` stores into, and rc's retain that moves the stored value in; + // null when `taker` is not such a store. + static mlir::Operation *recordRetainOfStore(mlir::Operation *taker) + { + auto storeOp = mlir::dyn_cast(taker); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto varOp = propertyRefOp ? propertyRefOp->getOperand(0).getDefiningOp() : mlir_ts::VariableOp(); + auto *retain = varOp ? recordRetainOf(varOp) : nullptr; + if (!retain || retain->getBlock() != taker->getBlock() || !taker->isBeforeInBlock(retain)) + { + return nullptr; + } + + return retain; } // A string constant cast to `string` is the global itself, which a release skips. @@ -2159,6 +2539,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + { + return mlir::isa(value.getType()) && storeOp.getReference() == value && + storeOp.getValue() != value; + } + return false; } @@ -2222,6 +2609,14 @@ class OwnershipInferencePass : public mlir::PassWrapper releases; + forEachUse(value, [&](mlir::Operation *user, mlir::Value) { + if (mlir::isa(user)) + { + releases.push_back(user); + } + }); + + auto *definition = value.getDefiningOp(); + for (auto *first : releases) + { + for (auto *second : releases) + { + if (first != second && reachableAfter(first, second, definition)) + { + // the view the second release gives back, where rc took its reference + auto *view = second->getOperand(0).getDefiningOp(); + reportSecondReference(view ? view : second); + return false; + } + } + } + + return true; + } + // The move verdict for an SSA value (phase 1). Reports and returns false when it is not one. bool movesOf(mlir::Value value, llvm::SetVector &toErase) { @@ -2346,7 +2770,7 @@ class OwnershipInferencePass : public mlir::PassWrapper killAt; @@ -2618,6 +3042,12 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) && user->getBlock() == taker->getBlock() && @@ -2643,6 +3073,11 @@ class OwnershipInferencePass : public mlir::PassWrapper(taker)) { return llvm::any_of(varOp.getResult().getUsers(), @@ -2733,7 +3168,7 @@ class OwnershipInferencePass : public mlir::PassWrappergetAttrOfType(OWN_FACTS_LOST_ATTR_NAME)) { - diag.attachNote(f.getLoc()) << "'" << f.getSymName() << "' could return a borrow or keep a parameter, but " + diag.attachNote(f.getLoc()) << "'" << f.getName() << "' could return a borrow or keep a parameter, but " << why.getValue(); } } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index bcb01f1c1..53b589530 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1,6 +1,7 @@ #include "mlir/Pass/Pass.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/FunctionInterfaces.h" #include "TypeScript/TypeScriptDialect.h" #include "TypeScript/TypeScriptOps.h" @@ -71,9 +72,11 @@ class OwnershipSignaturePass : public mlir::PassWrappergetParentOfType()) + // in any function: a `ts.Func`, or the `func.func` the async lowering outlines a body into + if (isCall(op) && op->getParentOfType()) { calls.push_back({op, resolve(op)}); } @@ -102,6 +105,14 @@ class OwnershipSignaturePass : public mlir::PassWrappersetAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } + + // a generator's `next`, read out of its state object: a function defined here, which + // returns its result retained like any other (its symbol sits in a constant, so it is + // open, and its body may not return a borrow) + if (call.callees.fieldFunction && call.op->getNumResults() == 1) + { + call.op->setAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } } exportNoDrops(); @@ -238,6 +249,8 @@ class OwnershipSignaturePass : public mlir::PassWrapper funcs; @@ -259,6 +272,12 @@ class OwnershipSignaturePass : public mlir::PassWrapper importedNoDrops; + // (tuple type, field) -> the one function an object of that type holds in that field: every + // `ts.Constant` of the type's shape puts it there, nothing stores into the field, and nothing + // but a constant or a read makes a value of the type. A generator's state object holds its + // `next` so. + llvm::DenseMap, mlir::StringAttr> fieldFunctions; + // 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 @@ -325,6 +344,129 @@ class OwnershipSignaturePass : public mlir::PassWrapper>`, whose + // fields are the tuple's. + mlir_ts::TupleType storageTupleOf(mlir::Type type) + { + if (auto objectType = mlir::dyn_cast(type)) + { + type = objectType.getStorageType(); + if (auto storageType = mlir::dyn_cast(type)) + { + return mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + } + else if (auto refType = mlir::dyn_cast(type)) + { + type = refType.getElementType(); + } + else if (auto valueRefType = mlir::dyn_cast(type)) + { + type = valueRefType.getElementType(); + } + + return mlir::dyn_cast(type); + } + + void collectFieldFunctions(mlir::ModuleOp module) + { + auto *context = &getContext(); + llvm::DenseSet> unknownFields; + llvm::DenseSet unknownTuples; + module.walk([&](mlir::Operation *op) { + if (auto constantOp = mlir::dyn_cast(op)) + { + auto constTupleType = mlir::dyn_cast(constantOp.getType()); + auto values = mlir::dyn_cast(constantOp.getValue()); + if (!constTupleType || !values) + { + return; + } + + mlir::Type tupleType = mlir_ts::TupleType::get(context, constTupleType.getFields()); + for (auto [index, value] : llvm::enumerate(values)) + { + auto symbol = mlir::dyn_cast(value); + if (!symbol) + { + continue; + } + + std::pair key{tupleType, static_cast(index)}; + auto [found, inserted] = fieldFunctions.try_emplace(key, symbol.getAttr()); + if (!inserted && found->second != symbol.getAttr()) + { + unknownFields.insert(key); + } + } + + return; + } + + // a field reference that is not only read: the field may be given anything + if (auto propertyRefOp = mlir::dyn_cast(op)) + { + auto tupleType = storageTupleOf(propertyRefOp->getOperand(0).getType()); + auto onlyRead = llvm::all_of(propertyRefOp->getUsers(), + [](mlir::Operation *user) { return mlir::isa(user); }); + if (tupleType && !onlyRead) + { + unknownFields.insert({tupleType, propertyRefOp.getPosition()}); + } + + return; + } + + // a value of the type made any other way may hold anything in any field + if (mlir::isa(op)) + { + return; + } + + for (auto result : op->getResults()) + { + if (auto tupleType = mlir::dyn_cast(result.getType())) + { + unknownTuples.insert(tupleType); + } + } + }); + + for (auto &key : unknownFields) + { + fieldFunctions.erase(key); + } + + llvm::SmallVector> dropped; + for (auto &entry : fieldFunctions) + { + if (unknownTuples.contains(entry.first.first)) + { + dropped.push_back(entry.first); + } + } + + for (auto &key : dropped) + { + fieldFunctions.erase(key); + } + } + + // `ts.Load(ts.PropertyRef(object, p))` of a field that only ever holds one function. + mlir::StringAttr fieldFunctionOf(mlir::Operation *def) + { + auto loadOp = mlir::dyn_cast_or_null(def); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); + if (!tupleType) + { + return {}; + } + + return fieldFunctions.lookup({tupleType, propertyRefOp.getPosition()}); + } + // The functions a class vtable holds, by position; empty for any other global. static llvm::SmallVector> classVTableEntries(mlir_ts::GlobalOp globalOp) { @@ -533,6 +675,12 @@ class OwnershipSignaturePass : public mlir::PassWrappergetParentOfType(); + auto inFunction = !!user->getParentOfType(); if (inFunction && mlir::isa(user) && onlyCalled(user->getResult(0))) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7468cfa76..1bebfe032 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2563,7 +2563,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o 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 own_string_length_borrow - own_closure_borrow own_closure_escape) + own_closure_borrow own_closure_escape own_generator own_interface_view + own_async own_record_return) 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. @@ -2572,7 +2573,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts" -o -) set_tests_properties(test-own-no-counting-${own_test} PROPERTIES PASS_REGULAR_EXPRESSION "define" - FAIL_REGULAR_EXPRESSION "__tslang_inc_ref|__tslang_dec_ref|error|Stack dump") + FAIL_REGULAR_EXPRESSION "__tslang_inc_ref|__tslang_dec_ref|error:|Stack dump") endforeach() # Across a library boundary: the library exports its functions' `__own_no_drops`, and a borrow held @@ -2641,11 +2642,13 @@ set(own_error_cases "own_err_cell_assigned_in_closure|borrows .* but is used here after it may be released or overwritten" "own_err_closure_assigns_captured_field|borrows .* but is used here after it may be released or overwritten" "own_err_unknown_producer|takes a second reference" + "own_err_interface_two_views|takes a second reference" + "own_err_field_function_reassigned|borrows .* but is used here after it may be released or overwritten" + "own_err_generator_param_object|cannot move this variable into it: its value is the caller's" "own_err_two_owners|is used here after its value was moved" "own_err_push_let|is used here after its value was moved" "own_err_push_loop|is moved inside a loop" "own_err_field_loop|is moved inside a loop" - "own_err_interface|takes a second reference" "own_err_union_moved|is used here after its value was moved" "own_err_field_read_overwritten|borrows .* but is used here after it may be released or overwritten" "own_err_element_read_overwritten|borrows .* but is used here after it may be released or overwritten" diff --git a/tslang/test/tester/own/own_async.ts b/tslang/test/tester/own/own_async.ts new file mode 100644 index 000000000..f9fe7b341 --- /dev/null +++ b/tslang/test/tester/own/own_async.ts @@ -0,0 +1,81 @@ +// -mm=own, phase 7: async functions. Their locals are ordinary locals of the function the async +// lowering outlines each body and continuation into, and the inference runs on those too. Each +// case reads through a churn() after an await, where a wrong release would have freed. +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); + } +} + +async function two() { return 2; } + +// a heap local alive across an await +async function local(k: number) { + const c = new C(k); + c.v.push(k); + const v = await two(); + churn(); + return c.x + c.v.length + v; +} + +// returns a fresh object +async function make(k: number) { + const c = new C(k); + c.v.push(1); + return c; +} + +// a heap parameter read after an await +async function param(c: C) { + const v = await two(); + churn(); + return c.x + v; +} + +async function str(s: string) { + return s + "!"; +} + +// a `for await` body is outlined on its own; a string made there is its temporary +async function* numbers() { + yield 1; + await two(); + yield 2; +} + +async function loop() { + let t = 0; + for await (const x of numbers()) { + const s = "n" + x; + t += s.length; + } + + return t; +} + +async function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 10; + t += await local(n); + const m = await make(n); + churn(); + t += m.x + m.v.length; + const p = new C(n); + t += await param(p); + const s = await str("ab"); + t += s.length; + } + + t += await loop(); + assert(t == 450004, "t"); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_field_function_reassigned.ts b/tslang/test/tester/own/own_err_field_function_reassigned.ts new file mode 100644 index 000000000..b3e19e0f7 --- /dev/null +++ b/tslang/test/tester/own/own_err_field_function_reassigned.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 7 rejects: `o.make` is assigned another function, so the call through the field +// is not known to be `make`'s, and may overwrite the field it is read from. +class C { + constructor(public x: number) {} +} + +function main() { + const o = { + make(): C { + return new C(1); + }, + }; + o.make = function (): C { + return new C(2); + }; + let c = o.make(); + print(c.x); + c = o.make(); + print(c.x); +} diff --git a/tslang/test/tester/own/own_err_generator_param_object.ts b/tslang/test/tester/own/own_err_generator_param_object.ts new file mode 100644 index 000000000..59a96d84f --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_param_object.ts @@ -0,0 +1,16 @@ +// -mm=own, phase 7 rejects: the generator's state object takes `c`'s cell with it, but `c` is the +// caller's, so the state object cannot own it. +class C { + constructor(public x: number) {} +} + +function* g(c: C) { + yield c.x; +} + +function main() { + const c = new C(1); + for (const v of g(c)) { + print(v); + } +} diff --git a/tslang/test/tester/own/own_err_interface.ts b/tslang/test/tester/own/own_err_interface.ts deleted file mode 100644 index 579e278f9..000000000 --- a/tslang/test/tester/own/own_err_interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -// -mm=own, phase 0 rejects: boxing an instance into an interface counts as a second owner beside -// the temporary (interface and `any` boxing are phase 3). -interface I { x: number; } -class C implements I { x: number = 5; v: number[] = [1]; } -function show(i: I) { return i.x; } -function main() { - print(show(new C())); - const i: I = new C(); - print(i.x); - print("done."); -} diff --git a/tslang/test/tester/own/own_err_interface_two_views.ts b/tslang/test/tester/own/own_err_interface_two_views.ts new file mode 100644 index 000000000..b9e26892f --- /dev/null +++ b/tslang/test/tester/own/own_err_interface_two_views.ts @@ -0,0 +1,21 @@ +// -mm=own, phase 7 rejects: each call is given `o` through an interface of its own, and rc counts +// each one; with no count, both would destroy `o`'s block. +interface I { + x: number; + get(): number; +} + +function use(i: I) { + return i.get(); +} + +function main() { + const o = { + x: 4.0, + get(): number { + return this.x; + }, + }; + print(use(o)); + print(use(o)); +} diff --git a/tslang/test/tester/own/own_generator.ts b/tslang/test/tester/own/own_generator.ts new file mode 100644 index 000000000..e7eb61807 --- /dev/null +++ b/tslang/test/tester/own/own_generator.ts @@ -0,0 +1,148 @@ +// -mm=own, phase 7: a generator's state object is an ordinary owned block. Its maker makes it and +// returns it, the caller owns it, and its release destroys whatever the generator's locals hold. +// Each case runs many times, so a state object that is never freed shows in measure.ps1, and +// reads through a churn() between yields, so a local freed too early reads garbage. +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* evens() { + yield 2; + yield 4; +} + +// what the generators below start from: a global, which a generator reads without capturing it +let seed = 0; + +// a local that owns a block: a field of the state object, destroyed with it +function* fromLocal() { + const k = seed; + const c = new C(k); + c.v.push(k); + yield c.x; + yield c.x + c.v.length; +} + +// a local assigned between yields: the old value is released, the state keeps the new one +function* reassigned() { + const k = seed; + let c = new C(k); + yield c.x; + c = new C(k + 1); + yield c.x; +} + +// yields fresh blocks: each moves into the `{value, done}` result, which the caller owns +function* objects() { + const k = seed; + yield new C(k); + yield new C(k + 1); +} + +// a string made in another function: a temporary made in front of a `yield` in the generator's +// own body is never released, under rc as well (OwnedReturnConsumptionPass does not release past a +// resume point) +function label(i: number) { + return "s" + i; +} + +function* strings() { + for (let i = 0; i < 3; i++) { + yield label(i); + } +} + +// parameters holding numbers: their cells move into the box the state object owns +function* range(from: number, to: number) { + for (let i = from; i < to; i++) { + yield i; + } +} + +// an object literal with a method: the same made block, seen as its object +function makeCounter(start: number) { + return { + n: start, + next() { + this.n++; + return this.n; + }, + }; +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + // through for...of + for (const v of evens()) { + t += v; + } + + // by hand, the generator kept in a local + const it = evens(); + let r = it.next(); + while (!r.done) { + t += r.value; + r = it.next(); + } + + const n = i % 10; + seed = n; + for (const v of fromLocal()) { + churn(); + t += v; + } + + for (const v of reassigned()) { + churn(); + t += v; + } + + for (const v of objects()) { + churn(); + t += v.x + v.v.length; + } + + let text = ""; + for (const s of strings()) { + churn(); + text = text + s; + } + + t += text.length; + + // by hand, each result kept across a churn() before it is read + const os = objects(); + let o = os.next(); + churn(); + t += o.value.x; + o = os.next(); + churn(); + t += o.value.x; + + for (const v of range(n, n + 3)) { + churn(); + t += v; + } + + // given up after the first value: the state object still destroys its local + const half = fromLocal(); + t += half.next().value; + + const c = makeCounter(i % 10); + t += c.next() + c.next(); + } + + assert(t == 1820000, "t"); + print("done."); +} diff --git a/tslang/test/tester/own/own_interface_view.ts b/tslang/test/tester/own/own_interface_view.ts new file mode 100644 index 000000000..afc22e958 --- /dev/null +++ b/tslang/test/tester/own/own_interface_view.ts @@ -0,0 +1,88 @@ +// -mm=own, phase 7: an object seen through an interface is that object's block (a view), so the +// interface value and the object have one owner between them. Each case reads through a churn() +// after the point where a second owner would have freed the block. +interface I { + x: number; + get(): number; +} + +class C implements I { + v: number[] = [1]; + constructor(public x: number) {} + get() { + return this.x + this.v.length; + } +} + +class Junk { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: Junk[] = []; + for (let i = 0; i < 64; i++) { + const j = new Junk(999); + j.v.push(99); + junk.push(j); + } +} + +function show(i: I) { + churn(); + return i.get(); +} + +// an object literal made straight into an interface +function make(k: number): I { + return { + x: k, + get(): number { + return this.x; + }, + }; +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 10; + + // a temporary class instance given as an interface + t += show(new C(n)); + + // an interface holding a class instance + const ci: I = new C(n); + churn(); + t += ci.get(); + + // an object literal kept in a `let`, seen through an interface: the interface borrows it + let raw = { + x: n, + get(): number { + return this.x * 2; + }, + }; + let ri = raw; + churn(); + t += ri.get() + raw.x; + + // a folded object literal given once as an interface + const o = { + x: n, + get(): number { + return this.x + 1; + }, + }; + t += show(o); + t += o.x; + + const m = make(n); + churn(); + t += m.get(); + } + + print(t); + assert(t == 780000, "t"); + print("done."); +} diff --git a/tslang/test/tester/own/own_record_return.ts b/tslang/test/tester/own/own_record_return.ts new file mode 100644 index 000000000..4bf286f4e --- /dev/null +++ b/tslang/test/tester/own/own_record_return.ts @@ -0,0 +1,44 @@ +// -mm=own, phase 7: a record built to be returned - here the shape a generator's `next` returns - +// takes the fresh values stored into its fields, and the caller owns the record. Each case reads +// through a churn() after the 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); + } +} + +function make(k: number) { + const c = new C(k); + c.v.push(k); + return { value: c, done: false }; +} + +function fresh(k: number) { + return { value: new C(k), done: k > 5 }; +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 10; + let r = make(n); + churn(); + t += r.value.x + r.value.v.length; + + // assigned again: the first record is released + r = fresh(n); + churn(); + t += r.value.x + (r.done ? 1 : 0); + } + + assert(t == 208000, "t"); + print("done."); +} diff --git a/tslang/tslang/transform.cpp b/tslang/tslang/transform.cpp index 354c7cb51..2d6195061 100644 --- a/tslang/tslang/transform.cpp +++ b/tslang/tslang/transform.cpp @@ -146,6 +146,8 @@ int runMLIRPasses(mlir::MLIRContext &context, llvm::SourceMgr &sourceMgr, mlir:: { pm.addPass(mlir::typescript::createOwnershipSignaturePass()); pm.nest().addPass(mlir::typescript::createOwnershipInferencePass()); + // an `await`'s continuation and a `for await` body, outlined by the async lowering + pm.nest().addPass(mlir::typescript::createOwnershipInferencePass()); } #ifdef ENABLE_OPT_PASSES