diff --git a/tslang/docs/superpowers/plans/2026-10-02-own-phase-6.md b/tslang/docs/superpowers/plans/2026-10-02-own-phase-6.md new file mode 100644 index 000000000..0e7f83100 --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-02-own-phase-6.md @@ -0,0 +1,148 @@ +# `-mm=own` Phase 6 Implementation Plan: the corpus report and diagnostics polish + +> **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:** Spec §7 item 6, "refine the shapes the histogram says matter". After 7b, 364 of the 591 +corpus files compile under `-mm=own`. Of the 227 that fail, 137 stop at the catch-all +`'x' takes a second reference; -mm=own cannot prove a move or a borrow here yet`, which names no +rule. This phase measures what the catch-all hides, accepts the sound shapes, and names the rest. + +**Spec:** §5 (diagnostics), §6.5 (the corpus report). Results go into §20. + +## Global Constraints + +As in phase 7b (plan `2026-10-02-own-phase-7b.md`). Also: + +- Branch `own-phase-6` from `origin/main` (ad8ea908, with #441 merged). +- **The report tooling stays out of the tree.** These live in the scratchpad and are cited in §20: + - `corpus/run.sh`: the first error per file; + - `p6/shapedump.patch`: a temporary stderr dump at the four `reportSecondReference` sites, under + `OWN_SHAPES`: the retained value's definition, its root, its users, its type, whether it owns + a block, the source location, and for a `Load` the place it reads; + - `corpus/show.py`: buckets with source lines. + + Re-apply the patch with `git apply` for each histogram, and never commit it. +- **Re-histogram after each task.** Every fix moves files on to their next blocker. +- **Exit criterion:** every bucket that is the only blocker of 6 or more files is either accepted + or given a named message. The catch-all count is reported before and after. + +## The report on ad8ea908 (first error per file, 227 failures) + +| first error | files | +|---|---| +| takes a second reference (catch-all) | 137 | +| used after its value was moved | 22 | +| borrows a field and cannot be stored, returned or captured | 21 | +| borrows an element and cannot be ... | 9 | +| ownership inference left a retain behind (lowering) | 8 | +| other named rules, unsupported, frontend | 30 | + +The 137, split by the dump (447 sites). The last column counts files where the bucket is the only +one: + +| bucket | sites | files | only blocker | +|---|---|---|---| +| a slot read (`Load` of a `ts.Variable`): mixed | 97 | 39 | 17 | +| a store of a call's result: script-mode globals | 59 | 28 | 14 | +| tuple literals (`[[1, "a"], ...]`) | 52 | 23 | 13 | +| the retained value owns no block (`number \| null`) | 35 | 23 | 9 | +| a block argument (`c ? a : b`) | 18 | 11 | 7 | +| a function literal (`Cast` of a `SymbolRef`) | 24 | 13 | 6 | +| a read of a global (`AddressOf`) | 22 | 8 | 4 | +| a number/bool/bigint converted to a string | 42 | 11 | 0 | +| a string constant | 5 | 1 | 0 | +| other | 55 | 34 | 18 | + +## Findings that set the order + +- **Script-mode globals are an rc fault.** In top-level code, `const t = new C()` stores into + the global with no retain, and §9.30 then releases the value at the end of the block. 5ai fixed + this for an assignment (`t = new C()`, via `isOwnedGlobalSlot`) but not for a declaration + (`MLIRGenImpl.h`, the "not global scope" store). Own reads the store as a move and the release + as a second reference, which is correct on wrong IR. + - Under rc it is not observable today: nothing runs after `main`'s top-level code. A module + with top-level code, imported as source, does not compile ("Global is referenced by + parentless instruction! @puts"); with `-shared`, its exported `const` cannot be resolved. + Those are two pre-existing bugs, every model, outside this phase. +- **"Left a retain behind" (8 files) is a global initializer region.** `const b: I = {...}` at + module level builds a fresh value in the `ts.Global` region and retains it for + `ts.GlobalResult`. No own pass runs there, so the lowering's guard fails the compile. It is + loud, not a hole. + +## Rulings + +- **Globals.** A global is a root, and a fresh value moves into it. The retain for a + `ts.GlobalResult` of a fresh value, or of a view of one, is erased. Anything else retained in + a global region gets a named error, not the lowering's. +- **No block.** A `ts.Retain` whose root owns no block (a union or optional made from a number + or null) is erased: an SSA value never changes. + - A `ts.RetainSlot` is decided on its initializer's root, so it is erased only when nothing + else is ever stored into the slot. `let y: number | string = 1; y = "s"; let z = y;` must + stay an error, or move as it does today. Negative test and tooth. +- **Function literals.** A `ts.Cast` of a `ts.SymbolRef` to a function type holds no capture + box, so it owns no block. The same slot guard applies: a slot that a capturing closure is + later stored into is not covered. +- **Conversions.** A `ts.Cast` from a type that owns no block to `string` makes a fresh string. + It already carries `__owned_result`; `isFresh` should read that. +- **String constants.** A `ts.Constant` string is immortal: retaining it takes nothing. +- **Tuple literals.** A `ts.CreateTuple` whose owning fields are constants or moved fresh values + holds nothing of anyone else's, so rc's retain of it is its reference for the moves. This + extends `holdsConstantsOrMoves` to a `CreateTuple` that feeds a `ts.CreateArray`, a store or a + union. +- **Named messages.** A shape the dump recognises but this phase does not accept gets its own + message in §5's language, not the catch-all, for example: `'x' is a conditional expression's + result; -mm=own cannot prove which branch owns it yet`. + +## Review Focus + +1. Every erased retain is of a value that owns no block, is immortal, or moves into a root. +2. The slot guard: a `RetainSlot` decision on an initializer holds only if the slot is never + stored again. +3. The rc change adds a retain under every counting model: the rc suite must stay unchanged. It + does not branch on the model (§4.3), since gc erases retains. + +--- + +### Task 1: globals (rc, every counting model, a PR of its own) + +- [x] A declaration of a global in top-level code goes through the owning-slot path: it takes + over an owned reference or retains, and releases the slot's old value, as an assignment + does. +- [x] Tests (PR #444): `own_global_script` (positive: `const`, `let` and `var` in top-level code, a class + and a string, read from a function); and the rc corpus unchanged. + +### Task 2: global initializer regions (own) + +- [x] Erase the retain of a fresh value, or of a view of one, given to `ts.GlobalResult`. + Anything else retained in a global region: a named error. +- [x] Tests: `own_global_init` (positive: an interface-typed object literal, a nested array + literal, an object with an accessor, at module level). + +### Task 3: no block, function literals, conversions, string constants + +- [x] As the rulings say, with the slot guard (`isConstantData`). Conversions to a string stayed + out: no file has them as its only blocker. +- [x] Tests (as built: `own_constant_data`, and the negative `own_err_literal_two_records` for the + `ts.New` exclusion; the slot guard is conservative, and no case was found where it changes + the verdict): + - positive `own_no_block`: `number | null`, a function literal in a `let`, a number converted + to a string, string constants through a `?:`; + - negative `own_err_union_slot_reassigned` (the guard); + - teeth: drop the guard, and the negative compiles. + +### Task 4: tuple literals + +- [x] As built: through `isConstantData` (a tuple of constants), and the slot guard reading a + field ref that only reads. Also: a record literal moved into the owning local it is made + for (`recordRetainOf`, `own_record_local`). +- [x] Tests: in `own_constant_data`; `own_tuple_literals`: `[number, string][]` literals, `push`/`unshift`/`splice` of a + literal tuple, a tuple in a union. + +### Task 5: named messages and the specific-rule buckets (the next PR) + +- [ ] Named messages for each recognised, unaccepted bucket left after tasks 1-4. +- [ ] Sample the use-after-move (22) and borrows-a-field (21) files with `--di`. A true positive + stays; a false positive becomes a shape. +- [ ] Re-run the report. Spec §20. 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 3c7260ee3..0331ca121 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 @@ -6,8 +6,9 @@ amendments folded in. Phase 0 merged as #399 (results §11); phase 1 (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) merged as #437 (results §17); phase -7a (generators, async) merged as #439 (results §18); phase 7b (generators that borrow) on branch -`own-phase-7b` (results §19). Plans in `docs/superpowers/plans/`. +7a (generators, async) merged as #439 (results §18); phase 7b (generators that borrow) merged as +#441 (results §19); phase 6 (the corpus report) on branch `own-phase-6`, after #444 (results +§20). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -1432,3 +1433,146 @@ Found, not fixed (every model): `a.map(f).map(g)` fails to compile ("the conditi - **A copy a borrowing field takes of a sub-field** (`const v = c.v` in the generator) is not borrowing, so it is the existing "cannot be stored" error. - **Without `--di`**, the bounded errors name "this value". + +## 20. Phase 6 results, 2026-10-02 (first PR) + +Plan: `docs/superpowers/plans/2026-10-02-own-phase-6.md`. Changed: `MLIRGenImpl.h` (#444, every +counting model), `OwnershipSignaturePass.cpp`, `OwnershipInferencePass.cpp`, `OwnershipFacts.h`. + +### 20.1 The report (spec §6.5) + +The tooling stays out of the tree: +- a script that compiles every corpus file under own and keeps the first error; +- a temporary dump, under `OWN_SHAPES`, at the four places that report the catch-all. It prints the + retained value's definition, its root, its users, its type, whether it owns a block, the source + location, and for a read of a slot, the slot's kind and what takes the value. + +On ad8ea908 (7b merged), 364 of 591 compile. The first error of the 227 that do not: + +| first error | files | +|---|---| +| `takes a second reference` (the catch-all, which names no rule) | 137 | +| used after its value was moved | 22 | +| borrows a field and cannot be stored, returned or captured | 21 | +| borrows an element and cannot be stored, returned or captured | 9 | +| `ownership inference left a retain behind` (the lowering's guard) | 8 | +| the rest: other named rules, unsupported, frontend | 30 | + +The 137, by the dump: 447 sites. The last column counts the files where the bucket is the only +blocker: + +| shape | sites | files | only blocker | +|---|---|---|---| +| a read of a slot (mixed) | 97 | 39 | 17 | +| a call's result stored: script-mode globals | 59 | 28 | 14 | +| tuple literals | 52 | 23 | 13 | +| the value owns no block (`number \| null`) | 35 | 23 | 9 | +| a block argument (`c ? a : b`) | 18 | 11 | 7 | +| a function literal | 24 | 13 | 6 | +| a read of a global | 22 | 8 | 4 | +| a number, bool or bigint converted to a string | 42 | 11 | 0 | +| other | 55 | 34 | 18 | + +### 20.2 What this PR accepts + +- **A global declared in top-level code (#444, rc).** `const t = new C()` there stored into the + global with no reference, and the end-of-block release gave the value's back: 5ai's bug, in + the declaration's store, which 5ai's fix (the assignment's) did not reach. + - The fix: the declaration's store takes the owning-slot path, as an assignment does. + - It is not observable under rc today. Nothing runs after a program's top-level code, and a + module with top-level code cannot be imported (§20.5). + - Under own, the store read as a move and the release as a second reference. +- **A global's initializer** runs in the global's region, which no function's inference sees, + so rc's retain for its `ts.GlobalResult` reached the lowering's guard. The signature pass now + erases such a retain when: + - the value is the only reference to its block: retained once, released nowhere in the region; + - it is fresh, constant data, or a record of constants; + - every use of it ends in the `ts.GlobalResult`: directly, through views, as an element of an + array literal, or copied into an object literal's box. + + Any other retain there is reported by the global's name. +- **Data nothing owns** (`isConstantData`, `OwnershipFacts.h`): what holds no block (a number or + null in a union), a `ts.Constant` or a field of one, a function named by a `ts.SymbolRef` and + its cast to a function value, and a tuple of such. + - A retain of it is erased. + - A `ts.RetainSlot` is decided on its slot's initializer, so only while nothing is stored into + the slot or a field of it, and no closure captures it. + - Such data has no use after a move: under `--opt`, CSE merges equal literals (the left-out + optional field of two tuple literals), and one value goes into several places. + - Two exclusions, from a type-only reading. The header's `ownsHeap` reads the type, and the + inference pass overrides it for a `ts.New` seen as its object; the two disagree by design. + - A `ts.New` is excluded: its `value_ref` owns nothing by type, but it is the object. Without + this, `00object` compiled again: one object literal stored into two records, which 7a + rightly rejects. + - A value a `ts.NewInterface` view boxes is excluded: each view is a block of its own. +- **A record literal made for an owning local**, `let a = { item: new C() }`, and its assignment + form. MLIRGen builds it in a local a `ts.Constant` starts, stores the fresh value into the + field, and reads the record once into `a`. That is 7a's record built to be returned, with an + owning local in place of the result slot: the fresh value moves into the record, the record + into `a`, and `a`'s `ts.RetainSlot` is the reference for what moved in. + +### 20.3 Measured and teeth + +AOT, `measure.ps1`, a loop that builds and drops `let a = { item: new Leaf(i) }`, in MB: + +| iterations | gc | rc | none | own | +|---|---|---|---|---| +| 20000 | 5.9 | 4.2 | 7.7 | 4.2 | +| 200000 | 5.9 | 4.2 | 35.2 | 4.2 | + +Teeth, each a temporary switch, with the JIT cache cleared: +- **The global declaration's retain reverted** (#444): `own_global_script` fails under own (AOT, + JIT, no-counting). rc passes either way, as §20.2 says. +- **The global initializer's erase skipped:** `own_global_init` fails. +- **The constant-data erase skipped:** `own_constant_data` fails. +- **Its no-use-after-move skipped:** `own_constant_data` fails under the runner's `--opt`. +- **The `ts.New` exclusion dropped:** `own_err_literal_two_records` compiles. +- **The boxing exclusion dropped:** `own_err_interface_two_views` still fails. It compiles only with + both exclusions dropped, so for that test each exclusion is a second guard for the other. +- **The record rule's two acceptances dropped:** `own_record_local` fails. + +### 20.4 Tests and the corpus + +- **Positive:** `own_global_script` (also under rc, none and gc), `own_global_init`, + `own_constant_data`, `own_record_local`. +- **Negative:** `own_err_literal_two_records`. + +Corpus: 364 → 402 of 591, none lost. It is 402 with the runner's `--opt --opt_level=3` as well; +the plain count can overstate (`01optional` compiled plain and failed under `--opt` before the +no-use-after-move rule). Each of the 38 passes test-runner `-mm=own`, AOT and JIT, with gc's +output, re-gated on the final build. The first errors after: + +| first error | files | +|---|---| +| `takes a second reference` | 101 | +| used after its value was moved | 24 | +| borrows a field and cannot be stored, returned or captured | 21 | +| borrows an element and cannot be stored, returned or captured | 9 | +| `left a retain behind` | 0 | + +Remaining shapes, by the dump, as the files where each is the only blocker: +- slot reads, 25. They split into: + - a record a `ts.Constant` starts, stored or taken; + - a parameter's value stored, pushed or cast; +- other, 24; +- a conditional's result (`return a > b ? a : b`), 7; +- tuples, 7; +- nested record literals (`{ inner: { item: new C() } }`) and arrays of record literals. + +These are the next PR's input: named messages for what the dump recognises, and the sampling +of the use-after-move and borrows-a-field files for false positives. + +### 20.5 Found, not fixed (every model) + +- **A global initialized from another global reads garbage** (#446). `let a: number[] = [1, 2]; let b = a;` + at module level gives `b.length` = 140711081380496 under gc. With a class, the program faults. +- **A module with a top-level statement, imported as source,** (#447) fails with "Global is referenced + by parentless instruction! @puts" (invalid LLVM IR). +- **With `-shared`, an exported `const` from such a module** (#448) cannot be resolved by the importer. + +### 20.6 Known limits + +- **A borrow of data nothing owns past its owner's overwrite** is still an error: + `let m = n; n = null; use(m)`, with `n: number | null`. +- **Nested record literals, and arrays of record literals,** are the catch-all. +- **A conditional's result** is the catch-all. diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 6be74aa81..d86d1ebfc 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -374,6 +374,78 @@ inline bool holdsNoBlock(mlir::Value value) return !ownsHeap(value) || (def && isLiteral(def)); } +// Some view of `value` is a `ts.NewInterface`, which boxes what it shows into a block. +inline bool boxedByView(mlir::Value value) +{ + llvm::SmallVector values{value}; + while (!values.empty()) + { + for (auto &use : values.pop_back_val().getUses()) + { + auto *user = use.getOwner(); + if (isView(user) && use.getOperandNumber() == 0) + { + if (mlir::isa(user)) + { + return true; + } + + values.push_back(user->getResult(0)); + } + } + } + + return false; +} + +// Data nothing owns: what holds no block, a `ts.Constant` (its strings are immortal) or a field of +// one, and a tuple built of such (`[1, "a"]`, an object literal's fields read out of its constant). +// Retaining it takes nothing anyone gives back. +inline bool isConstantData(mlir::Value value) +{ + // a view that boxes it makes a block of its own: a folded object literal given as an interface + // is a new block each time, which its own release destroys. And a `ts.New` is a block, though + // its `value_ref` type owns nothing: it is the object its view shows. + if (value.getDefiningOp() || boxedByView(value)) + { + return false; + } + + if (holdsNoBlock(value)) + { + return true; + } + + auto *def = value.getDefiningOp(); + if (!def) + { + return false; + } + + if (mlir::isa(def)) + { + return true; + } + + // a function, which a `ts.SymbolRef` names, seen as a function value: it has no capture box + if (auto castOp = mlir::dyn_cast(def); castOp && castOp.getIn().getDefiningOp()) + { + return true; + } + + if (auto extractOp = mlir::dyn_cast(def)) + { + return extractOp.getObject().getDefiningOp() != nullptr; + } + + if (mlir::isa(def)) + { + return llvm::all_of(def->getOperands(), [](mlir::Value operand) { return isConstantData(operand); }); + } + + return false; +} + // A captured variable's cell, as a closure body reaches it: read out of a field of its capture box // (or of any other block - a RefType loaded from somewhere is a cell). Its variable lives in // whatever function declared it, so an assignment through it may destroy a value that function, and diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index ad58058cd..56713f0dc 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -263,6 +263,15 @@ class OwnershipInferencePass continue; } + // data nothing owns (isConstantData): a number or null in a union, a function, a + // literal, a tuple of literals. A slot's retain is decided on its initializer, so only + // while nothing else is ever stored into the slot. + if (value && isConstantData(value) && (mlir::isa(op) || slotKeepsInitializer(op))) + { + toErase.insert(op); + continue; + } + if (!value || isFresh(value) == false) { reportSecondReference(op); @@ -2737,6 +2746,35 @@ class OwnershipInferencePass // 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. + // A `ts.RetainSlot`'s slot holds its initializer for as long as it lives: nothing is stored + // into it or into a field of it, and no closure captures it. + static bool slotKeepsInitializer(mlir::Operation *op) + { + auto slot = mlir::cast(op).getSlot(); + auto varOp = slot.getDefiningOp(); + if (!varOp || !varOp.getInitializer() || isCellVariable(slot)) + { + return false; + } + + return llvm::all_of(slot.getUsers(), [&](mlir::Operation *user) { + return mlir::isa(user) || onlyRead(user); + }); + } + + // A read of what a slot holds, or of a field of it (`a[0]` of a tuple local), and nothing that + // writes through it. + static bool onlyRead(mlir::Operation *user) + { + if (mlir::isa(user)) + { + return true; + } + + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [](mlir::Operation *fieldUser) { return onlyRead(fieldUser); }); + } + static mlir::Value retainedValue(mlir::Operation *op) { if (auto retainOp = mlir::dyn_cast(op)) @@ -2881,11 +2919,28 @@ class OwnershipInferencePass continue; } + // `let a = { item: new C() }`: the read is what an owning local starts with, and rc's + // `ts.RetainSlot` of that local is the record's retain + if (auto ownerOp = mlir::dyn_cast(user)) + { + auto *slotRetain = ownerOp.getInitializer() == read.getResult() && isOwningVariable(ownerOp) + ? onlyRetainSlotOf(ownerOp) + : nullptr; + if (!slotRetain || retain) + { + return nullptr; + } + + retain = slotRetain; + continue; + } + + // stored into the result slot, or into an owning local (`a = { item: new C() }`) auto storeOp = mlir::dyn_cast(user); auto slot = storeOp && storeOp.getValue() == read.getResult() ? storeOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); - if (!slot || !isResultSlot(slot)) + if (!slot || !(isResultSlot(slot) || isOwningVariable(slot))) { return nullptr; } @@ -2894,6 +2949,26 @@ class OwnershipInferencePass return retain; } + // The one `ts.RetainSlot` of a local; null when there are none or several. + static mlir::Operation *onlyRetainSlotOf(mlir_ts::VariableOp varOp) + { + mlir::Operation *found = nullptr; + for (auto *user : varOp->getUsers()) + { + if (mlir::isa(user)) + { + if (found) + { + return nullptr; + } + + found = user; + } + } + + return found; + } + // A local that owns nothing, is never captured, and is read only to be returned. static bool isResultSlot(mlir_ts::VariableOp varOp) { @@ -3071,8 +3146,9 @@ class OwnershipInferencePass // Nothing to move: a number owns no block, and a string literal is the immortal global, // which any number of places may hold. Under --opt, CSE merges identical literals before // this pass, so one such value is routinely stored into several places. A read out of a - // container owns nothing either: checkPlaceRead decides it. - if (!ownsHeap(value) || isImmortalLiteral(value) || placeReadOf(value)) + // container owns nothing either: checkPlaceRead decides it. Nor does other data nothing + // owns: an empty optional, a function, a tuple of literals (isConstantData). + if (!ownsHeap(value) || isImmortalLiteral(value) || isConstantData(value) || placeReadOf(value)) { return true; } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 966dbc21e..6f1e5d119 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -50,6 +50,102 @@ class OwnershipSignaturePass : public mlir::PassWrapper(user)) + { + return true; + } + + if ((isView(user) && use.getOperandNumber() == 0) || mlir::isa(user)) + { + return movesIntoResult(user->getResult(0)); + } + + // copied into a block `ts.New` made, which itself moves in: an object literal's box + if (auto storeOp = mlir::dyn_cast(user); storeOp && use.getOperandNumber() == 0) + { + auto block = storeOp.getReference(); + return block.getDefiningOp() && + llvm::all_of(block.getUses(), [&](mlir::OpOperand &blockUse) { + return blockUse.getOwner() == storeOp.getOperation() || + (isView(blockUse.getOwner()) && movesIntoResult(blockUse.getOwner()->getResult(0))); + }); + } + + return false; + }); + } + + // A record copied out of a local a `ts.Constant` initializes, whose owning fields are given + // nothing that holds a block: it holds constants only - numbers, immortal string literals, + // functions - and nobody else's reference. + static bool holdsOnlyConstants(mlir::Value value) + { + auto loadOp = value.getDefiningOp(); + auto varOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!varOp || !varOp.getInitializer() || !varOp.getInitializer().getDefiningOp()) + { + return false; + } + + return llvm::all_of(varOp->getUsers(), [](mlir::Operation *user) { + if (mlir::isa(user)) + { + return true; + } + + auto propertyRefOp = mlir::dyn_cast(user); + return propertyRefOp && llvm::all_of(propertyRefOp->getUsers(), [&](mlir::Operation *fieldUser) { + auto storeOp = mlir::dyn_cast(fieldUser); + return mlir::isa(fieldUser) || + (storeOp && storeOp.getReference() == propertyRefOp.getResult() && + holdsNoBlock(storeOp.getValue())); + }); + }); + } + + void moveIntoGlobals(mlir::ModuleOp module) + { + module.walk([&](mlir_ts::GlobalOp globalOp) { + llvm::SmallVector retains; + globalOp.walk([&](mlir_ts::RetainOp retainOp) { retains.push_back(retainOp); }); + for (auto retainOp : retains) + { + auto value = retainOp.getReference(); + + // nothing in the region gives the block back, or takes a second reference to it: + // the global is its one owner + auto released = false; + auto retained = 0; + forEachUse(rootOf(value), [&](mlir::Operation *user, mlir::Value) { + released = released || mlir::isa(user); + retained += mlir::isa(user); + }); + + if (retained == 1 && !released && (isFresh(value) || holdsOnlyConstants(value) || isConstantData(value)) && movesIntoResult(value)) + { + retainOp.erase(); + continue; + } + + retainOp.emitError("the initializer of global '") + << globalOp.getSymName() << "' takes a second reference; -mm=own cannot prove a move or a borrow here yet"; + signalPassFailure(); + } + }); + } + void runOnOperation() override { auto module = getOperation(); @@ -72,6 +168,7 @@ class OwnershipSignaturePass : public mlir::PassWrapper number, v: number) { + return f(v); +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function main() { + // a union made from a number, then from null + let n: number | null = 5; + let m = n; + let k: number | null = null; + assert(m == 5 && n == 5 && k == null); + + // functions as values + let f = twice; + const g = (x: number) => x + 1; + assert(apply(f, 3) == 6 && apply(g, 3) == 4); + + // tuples of literals: built, pushed, unshifted, spliced, destructured + let pairs: [number, string][] = [[1, "a"], [2, "b"]]; + pairs.push([3, "c"]); + pairs.unshift([0, "z"]); + pairs.splice(2, 0, [9, "y"]); + assert(churn() == 1000); + assert(pairs.length == 5 && pairs[0][1] == "z" && pairs[2][1] == "y" && pairs[4][1] == "c"); + + // a tuple local, read by index + let t: [string, number] = ["asd", 1.0]; + const nested: [[number, string], number] = [[1.0, "x"], 2.0]; + assert(t[0] == "asd" && t[1] == 1.0 && nested[0][1] == "x"); + + let [num, str] = [1, "foo"]; + assert(num == 1 && str == "foo"); + + // equal literals in two tuples, the optional field left out + const staff: Account[] = [ + [0, "A", "a@"], + [1, "B", "b@"], + [2, "C", "c@", "Lead"], + ]; + let count = 0; + for (const s of staff) count += s[3] ? 1 : 0; + assert(count == 1 && staff[2][3] == "Lead"); + + print("done."); +} diff --git a/tslang/test/tester/own/own_err_literal_two_records.ts b/tslang/test/tester/own/own_err_literal_two_records.ts new file mode 100644 index 000000000..7a0c850ca --- /dev/null +++ b/tslang/test/tester/own/own_err_literal_two_records.ts @@ -0,0 +1,13 @@ +// -mm=own rejects: `c` is one block, an object literal's, stored into two records, and neither +// record owns it alone. Its `ts.New` is a `value_ref`, a type that owns nothing, so a rule that +// reads the type alone takes it for data nobody owns. +function main() { + const c = { + toString() { + return "Hi"; + }, + }; + const r1 = { a: 1, c: c }; + const r2 = { a: 2, c }; + print(r1.a, r2.a); +} diff --git a/tslang/test/tester/own/own_global_init.ts b/tslang/test/tester/own/own_global_init.ts new file mode 100644 index 000000000..e47a1c4b9 --- /dev/null +++ b/tslang/test/tester/own/own_global_init.ts @@ -0,0 +1,41 @@ +// A global's initializer runs in the global's own region, which no function's inference sees. +// What it makes moves into the global, a root nothing gives back: an object literal seen as an +// interface, an intersection, a nested array literal (mutated later), an object with a method. +interface Counter { + count: number; + name: string; +} + +interface Named { + label: string; +} + +interface Sized { + size: number; +} + +const counter: Counter = { count: 3, name: "c" }; +const both: Named & Sized = { label: "b", size: 2 }; +const nested = [[1, 2], [3]]; +const withMethod = { n: 4, twice() { return this.n * 2; } }; + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function main() { + assert(churn() == 1000); + assert(counter.count == 3 && counter.name == "c"); + assert(both.label == "b" && both.size == 2); + assert(nested.length == 2 && nested[0][1] == 2 && nested[1][0] == 3); + + nested[1].push(4); + nested.push([5]); + assert(churn() == 1000); + assert(nested[1].length == 2 && nested[1][1] == 4 && nested[2][0] == 5); + + assert(withMethod.twice() == 8); + print("done."); +} diff --git a/tslang/test/tester/own/own_record_local.ts b/tslang/test/tester/own/own_record_local.ts new file mode 100644 index 000000000..664252f09 --- /dev/null +++ b/tslang/test/tester/own/own_record_local.ts @@ -0,0 +1,46 @@ +// An inline record literal made for an owning local: `let a = { item: new Leaf(i) }`. MLIRGen +// builds it in a local a `ts.Constant` starts, stores the fresh value into its field, and reads the +// whole record once into `a`. The fresh value moves into the record, and the record into `a`, +// which releases it at the end of its scope; an assignment gives up the record `a` held. +class Leaf { + v: number; + s: string; + + constructor(v: number) { + this.v = v; + this.s = "leaf-" + v; + } +} + +function churn() { + let keep: string[] = []; + for (let i = 0; i < 1000; i++) keep.push("k" + i); + return keep.length; +} + +function build(n: number) { + let total = 0; + for (let i = 0; i < n; i++) { + let a = { item: new Leaf(i) }; + total += a.item.v; + } + + return total; +} + +function main() { + let a = { item: new Leaf(1) }; + assert(churn() == 1000); + assert(a.item.v == 1 && a.item.s == "leaf-1"); + + // overwritten: the old record, and its leaf, are given up + a = { item: new Leaf(2) }; + assert(churn() == 1000); + assert(a.item.v == 2 && a.item.s == "leaf-2"); + + let b = { item: new Leaf(3), tag: "b" }; + assert(b.item.s == "leaf-3" && b.tag == "b"); + + assert(build(20000) == 199990000); + print("done."); +}