From ae02fa864f842377233ffbc1bea95a033c19cc18 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 14:46:40 +0100 Subject: [PATCH 1/5] -mm=own phase 7b: a generator over a parameter's block borrows it `function* each(a: number[])` keeps `a`'s cell in a box in its state object's `.captured` field, and its `for...of` copies `a` into a state field. Under 5b that was "its value is the caller's". Now the state object borrows, Rust's `-> impl Iterator + 'a`: - The signature pass finds each generator state type (unique per site: its `next` names a per-site `object_storage`) whose closed maker fills its box with parameters' cells only, and decides which fields borrow: the box, and each field everything stored into is a read of a borrowing cell or field (a least fixpoint). It writes them as the module attribute `ts.own_borrowing_fields`, and pins `__own_result_bounded` (the parameters holding blocks) on the maker and its calls. - The lowering skips a borrowing field in the state object's release routine, and gives a borrowing box the new routine `tsrelcbc_`, which frees each cell but not what it holds. - The inference lets a parameter's heap cell move into a borrowing box. In `next`, a store into a borrowing field takes nothing (rc's retain goes, its release on overwrite goes), and what is read out of a borrowing cell or field may only be borrowed: yielding it would give the caller's value a second owner, and rc retains nothing there for anything else to notice (`own_err_generator_yields_borrow`). - At a call with a bounded result the generator is owned as usual, but no use of it - through its locals and the `next` it is called through - may come after an end of a bounding argument, and it may not be stored, returned or captured (`own_err_generator_outlives_arg`, `own_err_generator_bounded_escapes`). Its releases are not uses: the routine gives back nothing it borrows. `own_generator_borrow` covers an array, a class and a string parameter, a generator given up early and two over one argument. `own_err_generator_param_object` compiles now and is gone. Corpus: 359 -> 362 of 591 (`00funcs_generic_iterator`, `01disposable`, `02disposable`; an object literal whose methods capture a parameter is the same shape), each run under test-runner -mm=own, AOT and JIT, with gc's output. Plan: docs/superpowers/plans/2026-10-02-own-phase-7b.md. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-02-own-phase-7b.md | 144 ++++++++ tslang/include/TypeScript/Defines.h | 6 + .../LowerToLLVM/OwnershipRoutineLogic.h | 67 +++- tslang/lib/TypeScript/OwnershipFacts.h | 4 + .../lib/TypeScript/OwnershipInferencePass.cpp | 317 +++++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 266 +++++++++++++++ tslang/test/tester/CMakeLists.txt | 6 +- .../own/own_err_generator_bounded_escapes.ts | 17 + .../own/own_err_generator_outlives_arg.ts | 13 + .../own/own_err_generator_param_object.ts | 16 - .../own/own_err_generator_yields_borrow.ts | 11 + .../test/tester/own/own_generator_borrow.ts | 78 +++++ 12 files changed, 919 insertions(+), 26 deletions(-) create mode 100644 tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md create mode 100644 tslang/test/tester/own/own_err_generator_bounded_escapes.ts create mode 100644 tslang/test/tester/own/own_err_generator_outlives_arg.ts delete mode 100644 tslang/test/tester/own/own_err_generator_param_object.ts create mode 100644 tslang/test/tester/own/own_err_generator_yields_borrow.ts create mode 100644 tslang/test/tester/own/own_generator_borrow.ts diff --git a/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md new file mode 100644 index 000000000..9219f515f --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md @@ -0,0 +1,144 @@ +# `-mm=own` Phase 7b Implementation Plan: generators that borrow + +> **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 over a parameter that holds a block compiles: `function* +each(a: number[]) { for (const v of a) yield v; }`. This is Rust's `fn each<'a>(a: &'a [f64]) -> +impl Iterator + 'a`. The state object is owned by the caller, but it borrows the argument, and +it may not outlive it. Then `.map`/`.filter`, which MLIRGen builds as such a generator over the +source array. + +**Architecture:** + +1. **The signature pass decides which fields of a state object borrow.** It is a module pass, and + the per-function inference is parallel and cannot share state. It writes the decision as a + module attribute that three places read: + - the lowering, which builds the state object's release routine; + - the inference in `next`; + - the inference in callers. + + A state type is unique to its generator (its `next` field's type names a per-site + `object_storage`), so a fact per type is a fact per generator. +2. **The lowering** skips a borrowed field in the state object's release routine. For a borrowing + `.captured` box, it calls a box routine that frees each cell but not what the cell holds. +3. **The maker's result is bounded by its arguments**: `__own_result_bounded = [indices]`. This is + not `__own_result_borrows`, which means "the result is a borrow" (no release, not fresh). + Bounded means owned (fresh, released as usual) and tenured: no use of the result may run + after an end of a bounding argument, and the result may not be stored, returned or captured. + +**Spec:** §18.6 (this phase's input). Results go into §19. + +## Global Constraints + +As in phase 7a (plan `2026-10-02-own-phase-7.md`). Also: + +- Clear `test/tester/own/__jit/.ts.*` between JIT teeth runs. The cache ignores environment + variables. +- Branch `own-phase-7b` from `origin/main` (af7b150f, with #439 merged). + +## Shapes (measured on af7b150f) + +- **The maker** of `each(a: number[])`: + 1. `a`'s cell is `ts.Variable(%arg0) {captured}` (not owning), `ts.RetainCell`ed. + 2. It is stored into the box `ts.Variable {captured, __capture_box}`, which is `ts.RetainCell`ed + and stored into `.captured` of the initial state. + 3. The frame's `ts.ReleaseCell(cell)` follows. + + Today this is 5b's "its value is the caller's". +- **`next`** reads the cell out of the box (`ts.Load(ts.PropertyRef(box, i))`). The `for...of` + copies the array into a state field `.a`: `ts.Retain(v)`, `ts.Store(v, PropertyRef(state, .a))`. +- **The caller** keeps the generator in an owning `let` and calls `next` through it, as in 7a. +- **`.map(f)`**: the caller builds a closure whose box holds a copy of the source array (rc + `ts.Retain`s it) and the callback, and calls it at once (devirtualized `ts.SymbolCallInternal`). + That closure body (`..iter`) is the maker. It copies its box's fields into a new box (another + `ts.Retain`) and stores that box into the state object. + +## Rulings made while planning + +- **A borrowing field**, for a generator state type `T` (it has a `next` in `fieldFunctions`) whose + maker `M` is closed (private, every caller known): + - **The box.** The `.captured` box borrows when every cell `M` fills it with is a parameter's + cell. Its cells then move into the box, which frees them and not their values. + - **A state field `p`** borrows when every store into `p`, anywhere in the module, stores a + value rooted at a read of a borrowing cell or of another borrowing field. This is computed as + a fixpoint. One store of anything else, and `p` owns as today. +- **`M`'s result is bounded** by each parameter whose cell holds a block and goes into a borrowing + box. `__own_result_bounded` is pinned on `M` and on each call that resolves to it. +- **At a call with a bounded result**, the result is fresh, and owned by whatever holds it, as + today. Every use of it, followed through views and the `let`s that hold it, may not be reachable + after any end of a bounding argument's root: + - its releases; + - an assignment to it; + - a drop of the place it was read from. + + A use that keeps the result (a store into a container, a return, a capture) is an error. Cost if + wrong: a generator that reads its argument after the caller freed it. +- **In `next`:** + - a store into a borrowing field erases rc's retain and takes nothing; + - a `ts.ReleaseSlot` of a borrowing field frees nothing (`__own_slot_borrows`, read by the + lowering, as `__own_cell_borrows` is); + - a read of a borrowing cell or field is a borrow with no end in `next`, as a parameter cell's + is in 5a. +- **Phasing:** 7b-1 is tasks 1-4 (parameters). 7b-2 is task 5 (`.map`/`.filter`). Yielding a borrow + stays a limit: the result record type `{value: C, done}` is shared between generators, so a fact + per type does not work for it. + +## Review Focus + +1. A field is marked borrowing only when nothing but borrows is ever stored into it. +2. The bounded check follows every derived use of the result, and treats anything it does not + know as a keep. +3. An open maker gets no facts, and stays the 5b error. + +--- + +### Task 1: the decision (signature pass) + +- [ ] Find each generator state type and its maker. Decide the borrowing box and fields. Write the + module attribute `__own_borrowing_fields`: per state type, the borrowing field indices, with + the `.captured` index meaning a borrowing box. +- [ ] Pin `__own_result_bounded` on the maker and on its calls. + +### Task 2: the lowering + +- [ ] `releaseFields`, under own: skip a borrowing field. For a borrowing `.captured`, call the box + routine that frees cells only (`tsrelcbc_`). +- [ ] `ts.ReleaseSlot {__own_slot_borrows}` frees nothing. + +### Task 3: the maker and `next` (inference) + +- [ ] The maker: a parameter's heap cell moves into a borrowing state box. There is no 5b error, + and the frame's `ts.ReleaseCell` goes. +- [ ] `next`: stores into borrowing fields, and their releases, as the rulings say. + +### Task 4: callers (inference) + +- [ ] The bounded check. + +**Tests:** positive `own_generator_borrow.ts`: +- an array parameter iterated; +- a class parameter whose fields are yielded; +- a string parameter, measured; +- a generator given up early; +- two generators over one argument. + +Negatives: +- `own_err_generator_outlives_arg` (the argument is assigned while the generator is still used); +- `own_err_generator_bounded_escapes` (returned from the function that owns the argument); +- `own_err_generator_bounded_stored` (pushed into a global). + +### Task 5: `.map` and `.filter` + +- [ ] The same, through the closure `..iter` and its by-value box. The caller's box is a borrowing + closure (5a), so the copy is a borrow: confirm this before assuming it. + +**Tests:** `own_generator_borrow.ts` gets `map`, `filter`, and a chained `map` read by `for...of`. + +### Task 6: measure, teeth, corpus, spec §19, memory + +- [ ] Teeth: + - the owning box routine for a borrowing box (the caller's argument freed twice); + - a borrowing field released (the same); + - the bounded check skipped (the negatives compile). diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index f6a6c15a8..ddab22dfb 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -74,6 +74,12 @@ // `this`), so the release frees the cell and leaves the value to its owner. #define OWN_BORROWS_CAPTURES_ATTR_NAME "__own_borrows_captures" #define OWN_CELL_BORROWS_ATTR_NAME "__own_cell_borrows" +// -mm=own only, set by OwnershipSignaturePass on the module for the lowering and the inference: the +// fields of a generator's state object that borrow what they hold from the generator's arguments, +// as [[type, [field...]], ...] keyed by the state's tuple type, the `.captured` box's index first. +// Its release routine skips them, and a borrowing `.captured` box frees its cells but not what +// they hold. +#define OWN_BORROWING_FIELDS_ATTR_NAME "ts.own_borrowing_fields" #define RETURN_VARIABLE_NAME ".return" #define CAPTURED_NAME ".captured" #define LABEL_ATTR_NAME "label" diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 82658145c..bc52c31e0 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -299,6 +299,14 @@ class OwnershipRoutineLogic // Copying a closure duplicates its one reference to the box and nothing else - what the // box holds is not duplicated - so this stops at the block, as an object's retain does. + // -mm=own: the box of a generator whose state object borrows from its arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME): it frees each cell, but what the cells hold is the caller's. + std::string getOrCreateCaptureBoxCellsRoutine(mlir_ts::RefType captureRefType) + { + return buildCaptureBoxRoutine(captureRefType, "tsrelcbc_", /*retaining=*/false, /*holdsNothing=*/false, + /*borrowsContents=*/true); + } + std::string getOrCreateCaptureBoxRetainRoutine(mlir_ts::RefType captureRefType) { return buildCaptureBoxRoutine(captureRefType, "tsretcb_", /*retaining=*/true); @@ -690,9 +698,18 @@ class OwnershipRoutineLogic auto loc = op->getLoc(); auto llvmRecordType = tch.convertType(recordType); + auto borrowing = borrowingFieldsOf(recordType); for (auto [index, fieldType] : llvm::enumerate(getFieldTypes(recordType))) { + // a field of a generator's state object that borrows from the generator's arguments: + // nothing to give back, but for the `.captured` box itself, whose cells are the state's + auto borrows = llvm::is_contained(borrowing, static_cast(index)); + if (borrows && (borrowing.front() != static_cast(index) || !isa(fieldType))) + { + continue; + } + // A field holding a `ref` to a tuple is a capture box - the `.captured` field of an // object literal with methods, or of the state object a generator becomes - and it is // the one field an object owns that the generic routines cannot see, because a @@ -708,7 +725,8 @@ class OwnershipRoutineLogic { if (isa(refFieldType.getElementType())) { - routineName = getOrCreateCaptureBoxReleaseRoutine(refFieldType); + routineName = borrows ? getOrCreateCaptureBoxCellsRoutine(refFieldType) + : getOrCreateCaptureBoxReleaseRoutine(refFieldType); } } else @@ -804,7 +822,7 @@ class OwnershipRoutineLogic // every other routine, so each begins by loading the box out of it. A box that holds nothing // of its own frees the block and stops there. std::string buildCaptureBoxRoutine(mlir_ts::RefType captureRefType, StringRef prefix, bool retaining, - bool holdsNothing = false) + bool holdsNothing = false, bool borrowsContents = false) { std::stringstream nameStream; nameStream << prefix.str() << (size_t)hash_value(captureRefType); @@ -839,7 +857,7 @@ class OwnershipRoutineLogic emitIfLastReference(boxValue, [&]() { if (!holdsNothing) { - releaseCapturedFields(captureRefType.getElementType(), boxValue); + releaseCapturedFields(captureRefType.getElementType(), boxValue, borrowsContents); } emitFreeBlock(boxValue); @@ -853,8 +871,8 @@ class OwnershipRoutineLogic } // Gives back what a dying box holds: one owner of each captured cell, and the contents of - // each field captured by value. - void releaseCapturedFields(mlir::Type tupleType, mlir::Value boxPtr) + // each field captured by value. A box that borrows its contents frees its cells only. + void releaseCapturedFields(mlir::Type tupleType, mlir::Value boxPtr, bool borrowsContents = false) { TypeHelper th(rewriter); TypeConverterHelper tch(typeConverter); @@ -866,7 +884,7 @@ class OwnershipRoutineLogic for (auto [index, fieldType] : llvm::enumerate(getFieldTypes(tupleType))) { auto refFieldType = dyn_cast(fieldType); - if (!refFieldType && !ownsHeapMemory(fieldType)) + if (!refFieldType && (borrowsContents || !ownsHeapMemory(fieldType))) { continue; } @@ -876,7 +894,8 @@ class OwnershipRoutineLogic if (refFieldType) { // the field holds the cell's address, so the cell is one load further in - emitReleaseCell(refFieldType.getElementType(), rewriter.create(loc, ptrTy, fieldPtr)); + emitReleaseCell(refFieldType.getElementType(), rewriter.create(loc, ptrTy, fieldPtr), + borrowsContents); } else { @@ -885,6 +904,40 @@ class OwnershipRoutineLogic } } + // Under own, the fields of a generator's state object that borrow (OWN_BORROWING_FIELDS_ATTR_NAME), + // the `.captured` box's first; empty for any other record or model. + llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) + { + llvm::SmallVector fields; + if (compileOptions.memoryModel != MemoryModelOwn) + { + return fields; + } + + if (auto storageType = dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(rewriter.getContext(), storageType.getFields()); + } + + auto parentModule = op->getParentOfType(); + auto entries = parentModule ? parentModule->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : ArrayAttr(); + if (!entries) + { + return fields; + } + + for (auto entry : entries.getAsRange()) + { + if (cast(entry[0]).getValue() == recordType) + { + auto indices = cast(entry[1]).asArrayRef(); + fields.append(indices.begin(), indices.end()); + } + } + + return fields; + } + void buildBody(mlir::Type type, mlir::Value slotPtr) { TypeHelper th(rewriter); diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 859e591ec..6be74aa81 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -44,6 +44,10 @@ namespace mlir_ts = mlir::typescript; // cell. A cell that borrows its value (a captured parameter's) cannot be assigned: the old value is // the caller's. #define OWN_ASSIGNS_CAPTURES_ATTR_NAME "__own_assigns_captures" +// The arguments a generator's state object borrows from (OWN_BORROWING_FIELDS_ATTR_NAME): the +// result is the caller's to own, but no use of it may come after any of these arguments is given +// back, and it may not be kept anywhere that outlives them. +#define OWN_RESULT_BOUNDED_ATTR_NAME "__own_result_bounded" // A call inside a try body is a `ts.Invoke` (`ts.InvokeHybrid` through a hybrid function): a // terminator whose normal and unwind edges are its successors. It names its callee as a symbol, or diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 8aec51520..e056f38cf 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -89,6 +89,8 @@ class OwnershipInferencePass closures.clear(); closureOf.clear(); stateBoxes.clear(); + fieldBorrowRetains.clear(); + fieldBorrowReleases.clear(); captureStores.clear(); captureRetains.clear(); returnsBorrowOf = resultBorrows(f); @@ -96,6 +98,7 @@ class OwnershipInferencePass // first: whether a closure borrows what it captures changes what its box's stores are classifyClosures(); classifyStateBoxes(); + classifyBorrowingFields(); // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { @@ -206,6 +209,12 @@ class OwnershipInferencePass continue; // a borrowing box's copy of a value: decideClosures decides it } + if (fieldBorrowRetains.contains(op)) + { + toErase.insert(op); // a borrowing field's: it takes nothing + continue; + } + auto value = retainedValue(op); if (value && proven.contains(value)) { @@ -304,6 +313,9 @@ class OwnershipInferencePass decideClosures(toErase); decideCells(); + checkBorrowedReads(); + checkBoundedResults(); + toErase.insert(fieldBorrowReleases.begin(), fieldBorrowReleases.end()); for (auto *op : toErase) { @@ -315,6 +327,7 @@ class OwnershipInferencePass 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, OWN_FRESH_RESULT_ATTR_NAME, OWN_ASSIGNS_CAPTURES_ATTR_NAME, + OWN_RESULT_BOUNDED_ATTR_NAME, OWN_CAPTURE_BORROW_ATTR_NAME}) { op->removeAttr(name); @@ -368,6 +381,9 @@ class OwnershipInferencePass // held by a local that owns nothing (`let f: () => number; f = () => ...`): an alias, so // the closure's own releases stay its owner bool aliased = false; + // a generator's box whose state object borrows from the generator's arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only + bool borrowsValues = false; }; llvm::SmallVector> closures; @@ -379,6 +395,9 @@ class OwnershipInferencePass llvm::DenseMap closureOf; // each generator's capture box, which escapes with its state object (classifyStateBoxes) llvm::DenseMap stateBoxes; + // rc's retains for stores into borrowing fields, and those fields' releases (classifyBorrowingFields) + llvm::DenseSet fieldBorrowRetains; + llvm::SmallVector fieldBorrowReleases; // 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 @@ -479,6 +498,291 @@ class OwnershipInferencePass }); } + // The fields of a generator's state object of this type that borrow from its arguments + // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. + llvm::SmallVector borrowingFieldsOf(mlir::Type recordType) + { + llvm::SmallVector fields; + if (auto objectType = mlir::dyn_cast(recordType)) + { + recordType = objectType.getStorageType(); + } + + if (auto storageType = mlir::dyn_cast(recordType)) + { + recordType = mlir_ts::TupleType::get(&getContext(), storageType.getFields()); + } + + auto module = getFunction()->getParentOfType(); + auto entries = module ? module->getAttrOfType(OWN_BORROWING_FIELDS_ATTR_NAME) : mlir::ArrayAttr(); + if (!entries) + { + return fields; + } + + for (auto entry : entries.getAsRange()) + { + if (mlir::cast(entry[0]).getValue() == recordType) + { + auto indices = mlir::cast(entry[1]).asArrayRef(); + fields.append(indices.begin(), indices.end()); + } + } + + return fields; + } + + // Is `ref` a field of a generator's state object that borrows (other than the `.captured` box)? + bool isBorrowingField(mlir::Value ref) + { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp) + { + return false; + } + + auto fields = borrowingFieldsOf(propertyRefOp->getOperand(0).getType()); + return fields.size() > 1 && llvm::is_contained(llvm::ArrayRef(fields).drop_front(), + static_cast(propertyRefOp.getPosition())); + } + + // A store into a borrowing field holds what it stores without owning it, as a borrowing box's + // copy does (OWN_CAPTURE_BORROW_ATTR_NAME), and rc's retain for it goes; its release, when the + // field is overwritten, gives nothing back. + void classifyBorrowingFields() + { + getFunction().walk([&](mlir::Operation *op) { + if (auto storeOp = mlir::dyn_cast(op); storeOp && isBorrowingField(storeOp.getReference())) + { + storeOp->setAttr(OWN_CAPTURE_BORROW_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + fieldBorrowRetains.insert(retain); + } + } + else if (auto releaseSlotOp = mlir::dyn_cast(op); + releaseSlotOp && isBorrowingField(releaseSlotOp.getSlot())) + { + fieldBorrowReleases.push_back(op); + } + }); + } + + // Is `ref` a cell read out of the `.captured` box of a generator's state object that borrows: + // `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`? + bool isBorrowingCell(mlir::Value ref) + { + auto cellLoad = ref.getDefiningOp(); + auto cellRef = cellLoad ? cellLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + if (!boxRef) + { + return false; + } + + auto fields = borrowingFieldsOf(boxRef->getOperand(0).getType()); + return !fields.empty() && fields.front() == static_cast(boxRef.getPosition()); + } + + // What a generator reads out of a borrowing cell or field is the caller's: every use of it must + // be a borrow - a store into another borrowing field is one (classifyBorrowingFields). A yield of + // it, a push, a return, a store anywhere else would give the caller's value a second owner, and + // rc retains nothing for some of these, so nothing else would see it. + void checkBorrowedReads() + { + getFunction().walk([&](mlir_ts::LoadOp loadOp) { + auto ref = loadOp.getReference(); + if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingField(ref))) + { + return; + } + + auto reported = false; + forEachUse(loadOp.getResult(), [&](mlir::Operation *user, mlir::Value used) { + if (reported || isBorrow(user, used) || fieldBorrowRetains.contains(user)) + { + return; + } + + reportPlaceBorrowEscapes(user, ownerName(loadOp.getResult()), "the generator's argument"); + reported = true; + }); + }); + } + + // A call whose result borrows from its arguments (OWN_RESULT_BOUNDED_ATTR_NAME): a generator over + // a parameter's block. The result is owned as any fresh value is, but nothing may use it after + // one of those arguments is given back - released, assigned, or dropped from the place it was + // read from - and it may not be kept anywhere that could outlive them. Its releases are not + // uses: the state object's release gives back nothing it borrows. + void checkBoundedResults() + { + getFunction().walk([&](mlir::Operation *call) { + auto bounded = call->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME); + if (!bounded || !isCall(call) || call->getNumResults() != 1) + { + return; + } + + auto name = ownerName(call->getResult(0)); + llvm::SmallVector uses; + if (auto *kept = boundedUses(call->getResult(0), uses)) + { + reportPlaceBorrowEscapes(kept, name, "its generator's arguments"); + return; + } + + auto args = callArgs(call); + for (auto index : bounded.asArrayRef()) + { + if (static_cast(index) >= args.size() || holdsNoBlock(args[index]) || + isImmortalLiteral(args[index])) + { + continue; + } + + auto root = rootOf(args[index]); + if (borrowedParam(root) >= 0) + { + continue; // the caller's caller's: alive for all of this function + } + + Chain chain; + auto *read = placeReadOf(root); + if (read) + { + chain = chainOf(read); + } + else if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + chain.roots.push_back({root, Chain::NotOwned}); + } + + // the root's own ends; and, for a read out of a place, whatever may destroy what the + // place holds (a local or a made value is not destroyed by a call it is given) + auto ends = rootEnds(chain); + for (auto *drop : read ? llvm::ArrayRef(drops) : llvm::ArrayRef()) + { + if (drop != call && dropsChain(drop, chain, read)) + { + ends.push_back(drop); + } + } + + auto owner = ownerName(root); + for (auto *end : ends) + { + if (end == call) + { + continue; + } + + for (auto *use : uses) + { + if (use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, owner); + return; + } + } + } + } + }); + } + + // Every use of a bounded result, through its views, the locals that hold it, and the function + // and object a call through it is made with. Answers the first use that keeps it - anything + // that is not a read, a call, a local, or rc's bookkeeping - or null. + mlir::Operation *boundedUses(mlir::Value result, llvm::SmallVectorImpl &uses) + { + llvm::SmallVector values{result}; + llvm::SmallPtrSet seen; + mlir::Operation *kept = nullptr; + while (!values.empty() && !kept) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (kept || mlir::isa(user)) + { + return; + } + + mlir::Value slot; + if (auto varOp = mlir::dyn_cast(user)) + { + slot = varOp.getResult(); + } + else if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getValue() == used) + { + slot = storeOp.getReference(); + } + + if (slot) + { + auto slotVar = slot.getDefiningOp(); + if (!slotVar || slotVar.getCaptured().value_or(false)) + { + kept = user; + return; + } + + for (auto *slotUser : slot.getUsers()) + { + if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + } + + return; + } + + uses.push_back(user); + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + kept = user; + } + + return; + } + + // `it.next()`: the field read, the bound function, its `this` + if (mlir::isa(user)) + { + for (auto partResult : user->getResults()) + { + values.push_back(partResult); + } + + return; + } + + if (!isBorrow(user, used)) + { + kept = user; + } + }); + } + + return kept; + } + // A generator's maker puts what the generator captures - its parameters, an outer local - into // a box, and the box into its state object's `.captured` field: the box of a closure that // escapes, with the state object, at that store. There is no closure value, so no use runs it @@ -535,6 +839,11 @@ class OwnershipInferencePass return; } + auto stateRef = mlir::cast(closure->escape).getReference().getDefiningOp(); + auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); + auto borrowing = borrowingFieldsOf(stateType); + closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); + for (auto &fill : closure->fills) { if (auto *retain = retainBefore(fill.second.getValue(), fill.second)) @@ -581,6 +890,12 @@ class OwnershipInferencePass ++reads; for (auto *readUser : loadOp->getUsers()) { + // rc's retain of the initial state, for fields that own (holdsConstantsOrMoves) + if (mlir::isa(readUser)) + { + continue; + } + auto fillOp = mlir::dyn_cast(readUser); if (!fillOp || fillOp.getValue() != loadOp.getResult() || !fillOp.getReference().getDefiningOp()) { @@ -946,7 +1261,7 @@ class OwnershipInferencePass auto name = varName(varOp); MLIRTypeHelper mth(&getContext(), CompileOptions{}); - if (!isOwningVariable(varOp) && + if (!isOwningVariable(varOp) && !closure.borrowsValues && mth.ownsHeapMemory(varOp.getLoc(), mlir::cast(value.getType()).getElementType())) { reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's"); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 53b589530..c3cbfad65 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -7,6 +7,7 @@ #include "TypeScript/TypeScriptOps.h" #include "TypeScript/Passes.h" #include "TypeScript/Defines.h" +#include "TypeScript/MLIRLogic/MLIRTypeHelper.h" #include "OwnershipFacts.h" @@ -85,6 +86,7 @@ class OwnershipSignaturePass : public mlir::PassWrappersetAttr(OWN_FRESH_RESULT_ATTR_NAME, mlir::UnitAttr::get(&getContext())); } + + // a generator whose state object borrows from its arguments + if (call.callees.funcs.size() == 1 && !call.callees.imported) + { + if (auto bounded = call.callees.funcs.front()->getAttrOfType(OWN_RESULT_BOUNDED_ATTR_NAME)) + { + call.op->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, bounded); + } + } } exportNoDrops(); @@ -1140,6 +1151,261 @@ class OwnershipSignaturePass : public mlir::PassWrapper bounded; + llvm::SetVector fields; + }; + + static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name) + { + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + if (auto id = mlir::dyn_cast_or_null(field.id); id && id.getValue() == name) + { + return static_cast(index); + } + } + + return -1; + } + + // The box the maker stores into the `.captured` field of the initial state it copies into + // `newOp`'s block: `ts.Store(box, ts.PropertyRef(state, captured))`, `state` a local whose only + // read fills the block. + static mlir::Value boxStoredIntoState(mlir_ts::NewOp newOp, int32_t captured) + { + mlir_ts::StoreOp fill; + for (auto *user : newOp->getUsers()) + { + if (auto storeOp = mlir::dyn_cast(user); storeOp && storeOp.getReference() == newOp.getResult()) + { + fill = storeOp; + } + } + + auto loadOp = fill ? fill.getValue().getDefiningOp() : mlir_ts::LoadOp(); + auto stateOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::VariableOp(); + if (!stateOp) + { + return {}; + } + + mlir::Value box; + for (auto *user : stateOp->getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp || propertyRefOp.getPosition() != captured) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || box) + { + return {}; + } + + box = storeOp.getValue(); + } + } + + auto boxOp = box ? box.getDefiningOp() : mlir_ts::VariableOp(); + return boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); + } + + // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that + // owns nothing (its value is the caller's). + static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) + { + auto argument = cellOp.getInitializer() ? mlir::dyn_cast(cellOp.getInitializer()) + : mlir::BlockArgument(); + if (!cellOp.getCaptured().value_or(false) || isOwningVariable(cellOp) || !argument || + !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + + bool ownsHeap(mlir::Location location, mlir::Type type) + { + MLIRTypeHelper mth(&getContext(), CompileOptions{}); + return mth.ownsHeapMemory(location, type); + } + + void findBorrowingStates(mlir::ModuleOp module) + { + llvm::MapVector states; + llvm::DenseMap made; + module.walk([&](mlir_ts::NewOp newOp) { + auto valueRefType = mlir::dyn_cast(newOp.getType()); + auto tupleType = valueRefType ? mlir::dyn_cast(valueRefType.getElementType()) + : mlir_ts::TupleType(); + if (!tupleType) + { + return; + } + + ++made[tupleType]; + auto captured = fieldIndex(tupleType, CAPTURED_NAME); + auto maker = newOp->getParentOfType(); + // an open maker has callers this pass cannot give the bound to + if (captured < 0 || !maker || open.contains(maker)) + { + return; + } + + auto box = boxStoredIntoState(newOp, captured); + if (!box) + { + return; + } + + BorrowingState state{maker, captured}; + for (auto *user : box.getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + auto cellOp = storeOp ? storeOp.getValue().getDefiningOp() : mlir_ts::VariableOp(); + int32_t index = -1; + if (!cellOp || !isParameterCell(cellOp, maker, index)) + { + return; // a box with anything but parameters' cells owns what it holds + } + + auto cellType = mlir::cast(cellOp.getType()); + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + { + state.bounded.push_back(index); + } + } + } + + if (!state.bounded.empty()) + { + states[tupleType] = state; + } + }); + + // made in one place only: the maker is the one function a bound can come from + states.remove_if([&](auto &entry) { return made[entry.first] != 1; }); + if (states.empty()) + { + return; + } + + // Each store into a field of a state type, by field: what the field may be given. + llvm::DenseMap, llvm::SmallVector> stored; + module.walk([&](mlir_ts::StoreOp storeOp) { + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + auto tupleType = propertyRefOp ? storageTupleOf(propertyRefOp->getOperand(0).getType()) : mlir_ts::TupleType(); + if (tupleType && states.count(tupleType)) + { + stored[{tupleType, propertyRefOp.getPosition()}].push_back(storeOp.getValue()); + } + }); + + for (auto &[type, state] : states) + { + auto tupleType = mlir::cast(type); + // least fixpoint: a field borrows once everything stored into it is a read of a borrowing + // cell or field + for (auto changed = true; changed;) + { + changed = false; + for (auto [index, field] : llvm::enumerate(tupleType.getFields())) + { + auto position = static_cast(index); + auto found = stored.find({type, position}); + if (position == state.captured || state.fields.contains(position) || found == stored.end() || + !ownsHeap(state.maker.getLoc(), field.type)) + { + continue; + } + + if (llvm::all_of(found->second, [&](mlir::Value value) { return readsBorrowed(value, tupleType, state); })) + { + state.fields.insert(position); + changed = true; + } + } + } + } + + auto *context = &getContext(); + llvm::SmallVector entries; + for (auto &[type, state] : states) + { + llvm::SmallVector fields{state.captured}; + fields.append(state.fields.begin(), state.fields.end()); + entries.push_back(mlir::ArrayAttr::get( + context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)})); + state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded)); + } + + module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries)); + } + + // Is `value` a read of what a state of `tupleType` borrows: the value in one of its box's cells + // (`ts.Load` of `ts.Load(ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i))`), or a + // borrowing field's (`ts.Load(ts.PropertyRef(state, p))`)? + bool readsBorrowed(mlir::Value value, mlir_ts::TupleType tupleType, const BorrowingState &state) + { + auto loadOp = rootOf(value).getDefiningOp(); + if (!loadOp) + { + return false; + } + + auto isStateField = [&](mlir::Value ref, int32_t &position) { + auto propertyRefOp = ref.getDefiningOp(); + if (!propertyRefOp || storageTupleOf(propertyRefOp->getOperand(0).getType()) != tupleType) + { + return false; + } + + position = propertyRefOp.getPosition(); + return true; + }; + + int32_t position = -1; + if (isStateField(loadOp.getReference(), position)) + { + return state.fields.contains(position); + } + + // the cell, read out of the box the state holds + auto cellLoad = loadOp.getReference().getDefiningOp(); + auto cellRef = cellLoad ? cellLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + } + // ---- Drops ---- // Least fixpoint: a function drops nothing until its body or a call it makes shows it may. diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 1bebfe032..10d457263 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2564,7 +2564,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_call_no_drops own_getter_borrow own_param_kept own_any_box own_string_length_borrow own_closure_borrow own_closure_escape own_generator own_interface_view - own_async own_record_return) + own_async own_record_return own_generator_borrow) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -2644,7 +2644,9 @@ set(own_error_cases "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_generator_outlives_arg|borrows .* but is used here after .* is released or overwritten" + "own_err_generator_bounded_escapes|borrows its generator's arguments and cannot be stored, returned or captured" + "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" "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" diff --git a/tslang/test/tester/own/own_err_generator_bounded_escapes.ts b/tslang/test/tester/own/own_err_generator_bounded_escapes.ts new file mode 100644 index 000000000..87607f7ae --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_bounded_escapes.ts @@ -0,0 +1,17 @@ +// -mm=own, phase 7b rejects: the generator borrows `a`, which dies with `make`'s frame. +function* each(a: number[]) { + for (const v of a) { + yield v; + } +} + +function make() { + const a: number[] = [1, 2]; + return each(a); +} + +function main() { + for (const v of make()) { + print(v); + } +} diff --git a/tslang/test/tester/own/own_err_generator_outlives_arg.ts b/tslang/test/tester/own/own_err_generator_outlives_arg.ts new file mode 100644 index 000000000..21d99abcf --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_outlives_arg.ts @@ -0,0 +1,13 @@ +// -mm=own, phase 7b rejects: `it` borrows `a`, and `a` is assigned while `it` is still used. +function* each(a: number[]) { + for (const v of a) { + yield v; + } +} + +function main() { + let a: number[] = [1, 2]; + const it = each(a); + a = [3.5]; + print(it.next().value); +} diff --git a/tslang/test/tester/own/own_err_generator_param_object.ts b/tslang/test/tester/own/own_err_generator_param_object.ts deleted file mode 100644 index 59a96d84f..000000000 --- a/tslang/test/tester/own/own_err_generator_param_object.ts +++ /dev/null @@ -1,16 +0,0 @@ -// -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_generator_yields_borrow.ts b/tslang/test/tester/own/own_err_generator_yields_borrow.ts new file mode 100644 index 000000000..4351857dd --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_yields_borrow.ts @@ -0,0 +1,11 @@ +// -mm=own, phase 7b rejects: `s` is the caller's, so the generator cannot hand it out as its own. +function* words(s: string) { + yield s; +} + +function main() { + const s = "w" + 1; + for (const w of words(s)) { + print(w); + } +} diff --git a/tslang/test/tester/own/own_generator_borrow.ts b/tslang/test/tester/own/own_generator_borrow.ts new file mode 100644 index 000000000..5353b916c --- /dev/null +++ b/tslang/test/tester/own/own_generator_borrow.ts @@ -0,0 +1,78 @@ +// -mm=own, phase 7b: a generator over a parameter that holds a block borrows it. The caller owns +// the generator, which may not outlive the argument; the generator's release gives back neither the +// argument nor what it copied out of it (the `for...of` over it). Each case reads through a churn() +// between yields, so an argument freed 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); + } +} + +// an array parameter, iterated +function* each(a: number[]) { + for (const v of a) { + yield v; + } +} + +// a class parameter, its fields yielded +function* fields(c: C) { + yield c.x; + yield c.v.length; +} + +// a string parameter: what is made from it is the generator's own +function* lengths(s: string) { + yield s.length; + yield (s + "!").length; +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n: number = i % 10; + const a: number[] = [n, n + 1, n + 2]; + for (const v of each(a)) { + churn(); + t += v; + } + + const c = new C(n); + c.v.push(n); + for (const v of fields(c)) { + churn(); + t += v; + } + + const s = "s" + n; + for (const v of lengths(s)) { + churn(); + t += v; + } + + // given up after the first value + const it = each(a); + t += it.next().value; + + // two generators over one argument at once + const g1 = each(a); + const g2 = each(a); + t += g1.next().value + g2.next().value; + churn(); + t += g1.next().value + g2.next().value; + + // the argument is still the caller's + t += a.length + c.x; + } + + assert(t == 1180000, "t"); + print("done."); +} From 92fc7c62a95ef0bd87220b33dd52b71fc9fd66c7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 15:05:46 +0100 Subject: [PATCH 2/5] -mm=own phase 7b: `.map` and `.filter` borrow their source array MLIRGen builds `a.map(f)` as a closure whose box holds a copy of `a` and `f`, called at once through the box: its body is the generator's maker, and it copies its box's fields by value into the generator's own box. - The signature pass takes a box copied from a parameter's fields as borrowing, bounded by that parameter, and a maker whose symbol is used only by direct calls and closures nothing but retains and releases as closed. In `next`, a by-value field of a borrowing box is a borrowing read too. - The inference: a store into a borrowing state box is a borrow; a read of a borrowing box's field is the caller's (no place read: only the maker fills the box, so no call overwrites it - before, a `filter` callback called in a loop "dropped" the field it was read from); a closure box that holds copies only, does not escape, and is given to such a maker borrows its copies, so two `.map`s of one array no longer move it twice. - At the caller, a bounding box ends where the closures on it are released and where what it was filled from ends (`own_err_generator_map_outlives`). The box is a `ref`, so it is checked before the test that skips arguments holding no block. - rc reads a place twice for one boxed copy and retains the first read: the second is the same value, both where a taker is looked for and where rc's retain for a box field is. `own_generator_borrow` gains `map` and `filter`. Corpus: 362 -> 364 of 591 (`00filter`, `00class_iterator_super`), none lost, each run under test-runner -mm=own, AOT and JIT. Found, not fixed (every model): `a.map(f).map(g)` fails to compile ("the condition has no value"). Co-Authored-By: Claude Opus 5.5 --- .../lib/TypeScript/OwnershipInferencePass.cpp | 208 +++++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 111 +++++++++- tslang/test/tester/CMakeLists.txt | 1 + .../own/own_err_generator_map_outlives.ts | 8 + .../test/tester/own/own_generator_borrow.ts | 13 +- 5 files changed, 318 insertions(+), 23 deletions(-) create mode 100644 tslang/test/tester/own/own_err_generator_map_outlives.ts diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index e056f38cf..94e387688 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -384,6 +384,9 @@ class OwnershipInferencePass // a generator's box whose state object borrows from the generator's arguments // (OWN_BORROWING_FIELDS_ATTR_NAME): a parameter's cell moves in, and the box frees the cell only bool borrowsValues = false; + // copies only, and given to a generator's maker that borrows from it (`.map(f)`): the box + // borrows its copies, as a box with cells would + bool borrowsCopies = false; }; llvm::SmallVector> closures; @@ -454,15 +457,23 @@ class OwnershipInferencePass closure->escape = escape; } - // copies only: the box owns them (phase 4); only an alias needs deciding + // copies only: the box owns them (phase 4); only an alias needs deciding - unless the box + // is given to a generator's maker whose result borrows from it if (!closure->hasCells) { - if (!closure->escape && closure->aliased) + closure->borrowsCopies = + !closure->escape && llvm::any_of(closure->boxUses, [](mlir::Operation *use) { + return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); + }); + if (!closure->borrowsCopies) { - closures.push_back(std::move(closure)); - } + if (!closure->escape && closure->aliased) + { + closures.push_back(std::move(closure)); + } - return; + return; + } } for (auto &fill : closure->fills) @@ -573,7 +584,15 @@ class OwnershipInferencePass bool isBorrowingCell(mlir::Value ref) { auto cellLoad = ref.getDefiningOp(); - auto cellRef = cellLoad ? cellLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + return cellLoad && isBorrowingBoxField(cellLoad.getReference()); + } + + // Is `ref` a field of the `.captured` box of a generator's state object that borrows: + // `ts.PropertyRef(ts.Load(ts.PropertyRef(state, captured)), i)`? A field copied in by value holds + // the caller's value itself, a cell field the caller's cell. + bool isBorrowingBoxField(mlir::Value ref) + { + auto cellRef = ref.getDefiningOp(); auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); auto boxRef = boxLoad ? boxLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); if (!boxRef) @@ -593,7 +612,7 @@ class OwnershipInferencePass { getFunction().walk([&](mlir_ts::LoadOp loadOp) { auto ref = loadOp.getReference(); - if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingField(ref))) + if (!ownsHeap(loadOp.getResult()) || !(isBorrowingCell(ref) || isBorrowingBoxField(ref) || isBorrowingField(ref))) { return; } @@ -636,13 +655,29 @@ class OwnershipInferencePass auto args = callArgs(call); for (auto index : bounded.asArrayRef()) { - if (static_cast(index) >= args.size() || holdsNoBlock(args[index]) || - isImmortalLiteral(args[index])) + if (static_cast(index) >= args.size()) { continue; } + // a closure's box (`.map(f)` calls its maker with it): it lives while the closures on + // it do, and what it holds while their owners do. A reference to storage, so first. auto root = rootOf(args[index]); + if (auto boxOp = root.getDefiningOp(); boxOp && boxOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) + { + if (!checkBoundedByBox(call, root, uses, name)) + { + return; + } + + continue; + } + + if (holdsNoBlock(args[index]) || isImmortalLiteral(args[index])) + { + continue; + } + if (borrowedParam(root) >= 0) { continue; // the caller's caller's: alive for all of this function @@ -699,6 +734,82 @@ class OwnershipInferencePass }); } + // The ends of a capture box given to a call with a bounded result: each release of a closure on + // it (the box goes with its last), and each end of what a field of it was filled from. Reports + // and returns false when a use of the result comes after one. + bool checkBoundedByBox(mlir::Operation *call, mlir::Value box, llvm::ArrayRef uses, + llvm::StringRef name) + { + llvm::SmallVector ends; + for (auto *user : box.getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user); boundOp && boundOp.getThisVal() == box) + { + llvm::SmallVector closureUses; + auto aliased = false; + if (followClosure(boundOp.getResult(), closureUses, ends, aliased)) + { + reportPlaceBorrowEscapes(call, name, "a closure that escapes"); // the box may go anywhere + return false; + } + + continue; + } + + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult() || holdsNoBlock(storeOp.getValue())) + { + continue; + } + + auto root = rootOf(storeOp.getValue()); + Chain chain; + if (auto slotLoad = slotLoadOf(root)) + { + chain.roots.push_back({slotLoad.getReference(), Chain::Slot}); + } + else if (borrowedParam(root) >= 0 || isImmortalLiteral(root)) + { + continue; + } + else if (isFresh(root)) + { + chain.roots.push_back({root, Chain::Owned}); + } + else + { + reportPlaceBorrowEscapes(call, name, "a value this function cannot bound"); + return false; + } + + auto rootEndsOf = rootEnds(chain); + ends.append(rootEndsOf.begin(), rootEndsOf.end()); + } + } + + for (auto *end : ends) + { + for (auto *use : uses) + { + if (end != call && use != end && reachableAfter(end, use, call)) + { + reportBorrowOutlives(use, end, name, "the generator's source"); + return false; + } + } + } + + return true; + } + // Every use of a bounded result, through its views, the locals that hold it, and the function // and object a call through it is made with. Answers the first use that keeps it - anything // that is not a read, a call, a local, or rc's bookkeeping - or null. @@ -843,6 +954,17 @@ class OwnershipInferencePass auto stateType = mlir::cast(stateRef->getOperand(0).getType()).getElementType(); auto borrowing = borrowingFieldsOf(stateType); closure->borrowsValues = !borrowing.empty() && borrowing.front() == stateRef.getPosition(); + // `.map`'s maker copies what its parameter box holds into the generator's box: a borrow + if (closure->borrowsValues) + { + for (auto &fill : closure->fills) + { + if (!mlir::isa(fill.second.getValue().getType())) + { + fill.second->setAttr(OWN_CAPTURE_BORROW_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + } + } for (auto &fill : closure->fills) { @@ -911,6 +1033,22 @@ class OwnershipInferencePass // fills the box, in the same block. mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) { + // rc's retain of an earlier read of the same place (twinReadAfter) + if (auto loadOp = value.getDefiningOp()) + { + for (auto *user : loadOp.getReference().getUsers()) + { + auto earlier = mlir::dyn_cast(user); + auto *retain = earlier && earlier.getResult().hasOneUse() ? *earlier->getUsers().begin() : nullptr; + if (retain && mlir::isa(retain) && !captureRetains.contains(retain) && + twinReadAfter(retain, earlier.getResult()) == value && retain->getBlock() == store->getBlock() && + retain->isBeforeInBlock(store)) + { + return retain; + } + } + } + mlir::Operation *found = nullptr; for (auto *user : value.getUsers()) { @@ -1101,7 +1239,7 @@ class OwnershipInferencePass } }; - if (!closure->hasCells) + if (!closure->hasCells && !closure->borrowsCopies) { keepAliasedReleases(); continue; @@ -1834,8 +1972,9 @@ class OwnershipInferencePass continue; } - // rc's reference for a borrowing box's copy goes with the closure (decideClosures) - if (captureRetains.contains(user)) + // rc's reference for a borrowing box's copy goes with the closure (decideClosures), and + // one for a borrowing field's with the field (classifyBorrowingFields) + if (captureRetains.contains(user) || fieldBorrowRetains.contains(user)) { continue; } @@ -1954,6 +2093,13 @@ class OwnershipInferencePass if (auto loadOp = mlir::dyn_cast(def); loadOp && isPlace(loadOp.getReference())) { + // a field of a generator's borrowing box: only its maker fills it, and nothing a call + // can reach overwrites it; it is the caller's, as a parameter is (checkBorrowedReads) + if (isBorrowingBoxField(loadOp.getReference())) + { + return nullptr; + } + return def; } @@ -3242,9 +3388,47 @@ class OwnershipInferencePass taker = user; }); + // rc retained this read and hands the receiver the next read of the same place + if (!taker && !several) + { + if (auto twin = twinReadAfter(retain, value)) + { + return takerOf(retain, twin); + } + } + return several ? nullptr : taker; // two takers of one read: not a move this phase proves } + // rc reads a place twice for one copy - `.map(f)` boxes its array so - and retains the first + // read, whose only use is that retain: the read of the same place that follows it in its block, + // with nothing stored into the place between, is the same value. Null when there is none. + static mlir::Value twinReadAfter(mlir::Operation *retain, mlir::Value value) + { + auto first = value.getDefiningOp(); + if (!first || !mlir::isa(retain) || !value.hasOneUse() || retain->getBlock() != first->getBlock()) + { + return {}; + } + + auto place = first.getReference(); + for (auto *op = retain->getNextNode(); op; op = op->getNextNode()) + { + if (auto loadOp = mlir::dyn_cast(op); loadOp && loadOp.getReference() == place) + { + return loadOp.getResult(); + } + + auto storeOp = mlir::dyn_cast(op); + if ((storeOp && storeOp.getReference() == place) || isCall(op)) + { + return {}; + } + } + + return {}; + } + // A move out of an owning local: `let b = a`, `h.c = a`, `return a`. rc read the slot and // retained what it read for the receiver; the move erases that retain and every release of // the slot the move reaches, which must be dominated by it. Any other use of the slot the diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index c3cbfad65..966dbc21e 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -1229,6 +1229,51 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr(CAPTURE_BOX_ATTR_NAME) ? box : mlir::Value(); } + // Is every caller of `funcOp` a direct call this pass sees? It is private, and each use of its + // symbol is a call, or a closure over it that is only retained and released - `.map(f)` builds + // one and calls its body straight through the box. + bool calledOnlyDirectly(mlir::ModuleOp module, mlir_ts::FuncOp funcOp) + { + if (!funcOp.isPrivate()) + { + return false; + } + + auto uses = mlir::SymbolTable::getSymbolUses(funcOp.getSymNameAttr(), &module.getBodyRegion()); + if (!uses) + { + return false; + } + + for (auto &use : *uses) + { + auto *user = use.getUser(); + if (mlir::isa(user)) + { + continue; + } + + auto symbolRefOp = mlir::dyn_cast(user); + if (!symbolRefOp) + { + return false; + } + + for (auto *refUser : symbolRefOp->getUsers()) + { + auto boundOp = mlir::dyn_cast(refUser); + if (!boundOp || !llvm::all_of(boundOp->getUsers(), [](mlir::Operation *boundUser) { + return mlir::isa(boundUser); + })) + { + return false; + } + } + } + + return true; + } + // A parameter's cell: a captured local declared from an argument of `maker`'s entry block, that // owns nothing (its value is the caller's). static bool isParameterCell(mlir_ts::VariableOp cellOp, mlir_ts::FuncOp maker, int32_t &index) @@ -1245,6 +1290,21 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp->getOperand(0)) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || argument.getOwner()->getParentOp() != maker.getOperation()) + { + return false; + } + + index = static_cast(argument.getArgNumber()); + return true; + } + bool ownsHeap(mlir::Location location, mlir::Type type) { MLIRTypeHelper mth(&getContext(), CompileOptions{}); @@ -1268,7 +1328,7 @@ class OwnershipSignaturePass : public mlir::PassWrappergetParentOfType(); // an open maker has callers this pass cannot give the bound to - if (captured < 0 || !maker || open.contains(maker)) + if (captured < 0 || !maker || !calledOnlyDirectly(module, maker)) { return; } @@ -1291,15 +1351,37 @@ class OwnershipSignaturePass : public mlir::PassWrappergetUsers()) { auto storeOp = mlir::dyn_cast(fieldUser); - auto cellOp = storeOp ? storeOp.getValue().getDefiningOp() : mlir_ts::VariableOp(); + if (!storeOp) + { + return; + } + + auto value = storeOp.getValue(); int32_t index = -1; - if (!cellOp || !isParameterCell(cellOp, maker, index)) + if (auto cellOp = value.getDefiningOp(); cellOp && isParameterCell(cellOp, maker, index)) { - return; // a box with anything but parameters' cells owns what it holds + auto cellType = mlir::cast(cellOp.getType()); + if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + { + state.bounded.push_back(index); + } + + continue; + } + + // a copy of what a parameter holds in a field: `.map`'s maker is a closure body + // that copies its own box's fields into the generator's + if (!ownsHeap(value.getLoc(), value.getType())) + { + continue; } - auto cellType = mlir::cast(cellOp.getType()); - if (ownsHeap(cellOp.getLoc(), cellType.getElementType())) + if (!readsParameterField(value, maker, index)) + { + return; // a box with anything but what the parameters hold owns what it holds + } + + if (!llvm::is_contained(state.bounded, index)) { state.bounded.push_back(index); } @@ -1399,11 +1481,20 @@ class OwnershipSignaturePass : public mlir::PassWrapper(); + auto boxLoad = fieldRef ? fieldRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + }; + + if (isBoxField(loadOp.getReference())) + { + return true; + } + auto cellLoad = loadOp.getReference().getDefiningOp(); - auto cellRef = cellLoad ? cellLoad.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); - auto boxLoad = cellRef ? cellRef->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); - return boxLoad && isStateField(boxLoad.getReference(), position) && position == state.captured; + return cellLoad && isBoxField(cellLoad.getReference()); } // ---- Drops ---- diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 10d457263..ca5e81a53 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2647,6 +2647,7 @@ set(own_error_cases "own_err_generator_outlives_arg|borrows .* but is used here after .* is released or overwritten" "own_err_generator_bounded_escapes|borrows its generator's arguments and cannot be stored, returned or captured" "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" + "own_err_generator_map_outlives|borrows .* but is used here after .* is released or overwritten" "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" diff --git a/tslang/test/tester/own/own_err_generator_map_outlives.ts b/tslang/test/tester/own/own_err_generator_map_outlives.ts new file mode 100644 index 000000000..5fe14ab89 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_map_outlives.ts @@ -0,0 +1,8 @@ +// -mm=own, phase 7b rejects: the generator `.map` builds borrows `a`, which is assigned while the +// generator is still used. +function main() { + let a: number[] = [1, 2, 3]; + const it = a.map((x) => x * 2); + a = [4.5]; + print(it.next().value); +} diff --git a/tslang/test/tester/own/own_generator_borrow.ts b/tslang/test/tester/own/own_generator_borrow.ts index 5353b916c..b4005a3cd 100644 --- a/tslang/test/tester/own/own_generator_borrow.ts +++ b/tslang/test/tester/own/own_generator_borrow.ts @@ -69,10 +69,21 @@ function main() { churn(); t += g1.next().value + g2.next().value; + // `.map` and `.filter` are generators over the array MLIRGen builds for them + for (const v of a.map((x) => x * 2)) { + churn(); + t += v; + } + + for (const v of a.filter((x) => x > n)) { + churn(); + t += v; + } + // the argument is still the caller's t += a.length + c.x; } - assert(t == 1180000, "t"); + assert(t == 2080000, "t"); print("done."); } From 12a7bd28af2ce6a932ea31cf83403b8295b9734d Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 15:16:27 +0100 Subject: [PATCH 3/5] -mm=own phase 7b: a box borrows its copies only when each has an owner of its own The previous commit let a copies-only closure box given to `.map`/`.filter`'s maker borrow everything it holds. A callback that captures (`a.filter((x) => x > n)`) is a closure made for that box, which rc hands its reference: borrowed, it was nobody's, a leak of 31 bytes a call that measure.ps1 showed (rc 4.3 MB, own 10.5 MB over 200000 calls). The box now borrows only when no copy it holds is fresh; otherwise it owns its copies as in phase 4, and the array moves into it. `own_generator_borrow` puts its capturing `filter` last, after the array's other uses, and makes its string in a helper (a temporary in front of a `yield` leaks under rc and own alike, spec 18.5). It measures flat: 4.3 MB at 20000 and at 200000 iterations, as rc. Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/OwnershipInferencePass.cpp | 11 +++++++++-- tslang/test/tester/own/own_generator_borrow.ts | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 94e387688..8dfe1aa4a 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -461,9 +461,16 @@ class OwnershipInferencePass // is given to a generator's maker whose result borrows from it if (!closure->hasCells) { + // ...and only when every copy has an owner of its own: the box frees itself alone, so a + // value made for it (a callback that captures, `a.filter((x) => x > n)`) would be + // nobody's closure->borrowsCopies = - !closure->escape && llvm::any_of(closure->boxUses, [](mlir::Operation *use) { - return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); + !closure->escape && + llvm::any_of(closure->boxUses, + [](mlir::Operation *use) { return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME); }) && + llvm::none_of(closure->fills, [&](auto &fill) { + auto value = fill.second.getValue(); + return !holdsNoBlock(value) && isFresh(rootOf(value)); }); if (!closure->borrowsCopies) { diff --git a/tslang/test/tester/own/own_generator_borrow.ts b/tslang/test/tester/own/own_generator_borrow.ts index b4005a3cd..0d815d12d 100644 --- a/tslang/test/tester/own/own_generator_borrow.ts +++ b/tslang/test/tester/own/own_generator_borrow.ts @@ -29,10 +29,16 @@ function* fields(c: C) { yield c.v.length; } +// a string made in another function: a temporary made in front of a `yield` in the generator's own +// body is never released (spec 18.5) +function bangLength(s: string) { + return (s + "!").length; +} + // a string parameter: what is made from it is the generator's own function* lengths(s: string) { yield s.length; - yield (s + "!").length; + yield bangLength(s); } function main() { @@ -75,13 +81,15 @@ function main() { t += v; } + // the argument is still the caller's + t += a.length + c.x; + + // a callback that captures is made for the box `.filter` builds, which then owns its copies: + // `a` moves in, so this is its last use for (const v of a.filter((x) => x > n)) { churn(); t += v; } - - // the argument is still the caller's - t += a.length + c.x; } assert(t == 2080000, "t"); From c040dd7b7e1905795ead9856032a96b72cf3b55a Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 15:19:29 +0100 Subject: [PATCH 4/5] -mm=own phase 7b: spec section 19 Results, teeth, measurements and limits of phase 7b. Section 18.5's await item is #440 now: an await of a function that returns nothing never awaits. Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-02-own-phase-7b.md | 24 ++-- .../2026-09-24-own-memory-model-design.md | 114 +++++++++++++++++- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md index 9219f515f..7e51a4edc 100644 --- a/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md +++ b/tslang/docs/superpowers/plans/2026-10-02-own-phase-7b.md @@ -96,26 +96,26 @@ As in phase 7a (plan `2026-10-02-own-phase-7.md`). Also: ### Task 1: the decision (signature pass) -- [ ] Find each generator state type and its maker. Decide the borrowing box and fields. Write the +- [x] Find each generator state type and its maker. Decide the borrowing box and fields. Write the module attribute `__own_borrowing_fields`: per state type, the borrowing field indices, with the `.captured` index meaning a borrowing box. -- [ ] Pin `__own_result_bounded` on the maker and on its calls. +- [x] Pin `__own_result_bounded` on the maker and on its calls. ### Task 2: the lowering -- [ ] `releaseFields`, under own: skip a borrowing field. For a borrowing `.captured`, call the box +- [x] `releaseFields`, under own: skip a borrowing field. For a borrowing `.captured`, call the box routine that frees cells only (`tsrelcbc_`). -- [ ] `ts.ReleaseSlot {__own_slot_borrows}` frees nothing. +- [x] `ts.ReleaseSlot {__own_slot_borrows}` frees nothing. ### Task 3: the maker and `next` (inference) -- [ ] The maker: a parameter's heap cell moves into a borrowing state box. There is no 5b error, +- [x] The maker: a parameter's heap cell moves into a borrowing state box. There is no 5b error, and the frame's `ts.ReleaseCell` goes. -- [ ] `next`: stores into borrowing fields, and their releases, as the rulings say. +- [x] `next`: stores into borrowing fields, and their releases, as the rulings say. ### Task 4: callers (inference) -- [ ] The bounded check. +- [x] The bounded check. **Tests:** positive `own_generator_borrow.ts`: - an array parameter iterated; @@ -127,18 +127,20 @@ As in phase 7a (plan `2026-10-02-own-phase-7.md`). Also: Negatives: - `own_err_generator_outlives_arg` (the argument is assigned while the generator is still used); - `own_err_generator_bounded_escapes` (returned from the function that owns the argument); -- `own_err_generator_bounded_stored` (pushed into a global). +- `own_err_generator_yields_borrow` (the parameter itself yielded), which replaced + `own_err_generator_bounded_stored`: a store is the same keep check as the return. ### Task 5: `.map` and `.filter` -- [ ] The same, through the closure `..iter` and its by-value box. The caller's box is a borrowing +- [x] The same, through the closure `..iter` and its by-value box. The caller's box is a borrowing closure (5a), so the copy is a borrow: confirm this before assuming it. -**Tests:** `own_generator_borrow.ts` gets `map`, `filter`, and a chained `map` read by `for...of`. +**Tests:** `own_generator_borrow.ts` gets `map`, `filter`, and a capturing `filter` (a chained `map` +fails to compile under every model). Negative: `own_err_generator_map_outlives`. ### Task 6: measure, teeth, corpus, spec §19, memory -- [ ] Teeth: +- [x] Teeth: - the owning box routine for a borrowing box (the caller's argument freed twice); - a borrowing field released (the same); - the bounded check skipped (the negatives compile). 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 dd01a288c..8ef603a3c 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,7 +6,8 @@ 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) on branch `own-phase-7` (results §18). Plans in `docs/superpowers/plans/`. +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/`. ## 1. Purpose @@ -1312,9 +1313,10 @@ Found, not fixed: - **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). +- **An `await` of a function that returns nothing never awaits** (#440, every model): MLIRGen + exits before building the `async.await` when the awaited call has no value, so `await f()` + continues at once and `f` may never run. That is what made the `for await` facts above + impossible to test at run time. ### 18.6 Known limits (the input to 7b) @@ -1325,3 +1327,107 @@ Found, not fixed: - `.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. + +## 19. Phase 7b results, 2026-10-02 + +Plan: `docs/superpowers/plans/2026-10-02-own-phase-7b.md`. Changed: `OwnershipSignaturePass.cpp`, +`OwnershipInferencePass.cpp`, `OwnershipFacts.h`, `Defines.h`, and `OwnershipRoutineLogic.h` +(the lowering). + +### 19.1 What phase 7b accepts + +A generator over a parameter that holds a block, `function* each(a: number[])`, borrows it: Rust's +`fn each<'a>(a: &'a [f64]) -> impl Iterator + 'a`. The caller owns the state object, which may not +outlive the argument, and whose release gives the argument back to nobody. + +- **The decision is the signature pass's.** The release routine is built once per type, and the + inference is per function and parallel, so neither can make it. A state type is unique to its + generator: its `next`'s type names a per-site `object_storage`. For a state type whose maker is + closed, and is the type's only `ts.New`: + - the `.captured` box borrows when every fill is a parameter's cell, or a copy read out of a + parameter's field (`.map`'s maker is a closure body that copies its box); + - a state field borrows when everything stored into it, in the whole module, is a read of a + borrowing cell or field (a least fixpoint). The `for...of` copy of the array is such a field. + + It writes `ts.own_borrowing_fields` on the module (per type, the box's index first), and pins + `__own_result_bounded`, the parameters whose blocks the state borrows, on the maker and its calls. + A maker is closed when it is private and its symbol is used only by direct calls, and by closures + that are only retained and released (`.map` builds one and calls its body through the box). +- **The lowering**, under own, skips a borrowing field in the state object's release routine. A + borrowing box gets `tsrelcbc_`, which frees each cell and none of what the cells or by-value + fields hold. +- **In the maker**, a parameter's cell moves into a borrowing box: no 5b error, and the frame's + `ts.ReleaseCell` goes. A copy stored into a borrowing box is a borrow. +- **In `next`**: + - a store into a borrowing field takes nothing: rc's retain goes, and so does its release when + the field is overwritten; + - what is read out of a borrowing cell, field or box field is the caller's, and may only be + borrowed. Yielding it is an error (`own_err_generator_yields_borrow`): it would give the + caller's value a second owner, and rc retains nothing there for anything else to see; + - a read of a borrowing box's field is not a place read: only the maker fills the box, so no call + overwrites it. +- **At the caller**, the generator is owned as any fresh value, and bounded: + - no use of it may come after an end of a bounding argument. Its uses are followed through the + locals that hold it and the `next` it is called through. An end is the argument's root's + releases, assignments and moves, or, for a read out of a place, what may destroy the place. + Its releases are not uses: the routine gives back nothing it borrows; + - it may not be stored, returned or captured (`own_err_generator_bounded_escapes`); + - a bounding capture box (`.map`) ends where the closures on it are released, and where what it + was filled from ends (`own_err_generator_map_outlives`). +- **`.map` and `.filter`.** The caller's closure box holds a copy of the array. A box of copies + that does not escape, is given to a bounded maker, and holds nothing fresh borrows its copies, so + two `.map`s of one array no longer move it twice. A callback that captures is a fresh closure + made for the box: the box then owns its copies, and the array moves in. rc reads the array twice + for one copy and retains the first read: the second is the same value, both where a taker is + looked for and where a box field's retain is. + +### 19.2 Measured + +AOT, `measure.ps1`, in MB: + +| test | gc | rc | none | own | +|---|---|---|---|---| +| `own_generator_borrow`, 20000 iterations | 6.1 | 4.3 | 1441.5 | 4.3 | +| the same, 200000 iterations | 6.1 | 4.3 | 14376.5 | 4.3 | + +The ten-times run is the leak check. It found one in this phase's own work: the first form of the +copies rule borrowed a fresh capturing callback, which nobody then freed (own 10.5 MB, rc 4.3). + +### 19.3 Teeth + +Each made with a temporary `getenv` switch, the JIT cache cleared between runs: + +- **The owning box routine for a borrowing box:** `own_generator_borrow` fails, AOT + (0xC0000374) and JIT. +- **A borrowing field released:** the same. +- **The bounded check skipped:** `own_err_generator_outlives_arg`, + `own_err_generator_bounded_escapes` and `own_err_generator_map_outlives` compile. +- **The borrowed-read check skipped:** `own_err_generator_yields_borrow` compiles. + +### 19.4 Tests and the corpus + +- **Positive:** `own_generator_borrow`. It covers an array, a class and a string parameter, a + generator given up early, two generators over one argument, `map`, `filter`, and a capturing + `filter` last. +- **Negatives:** `own_err_generator_outlives_arg`, `own_err_generator_bounded_escapes`, + `own_err_generator_yields_borrow`, `own_err_generator_map_outlives`. +- **Removed:** `own_err_generator_param_object` compiles now and is gone. + +Corpus: 359 of 591 before, 364 after, none lost: `00funcs_generic_iterator`, `01disposable`, +`02disposable` (an object literal whose methods capture a parameter is the same shape), +`00filter`, `00class_iterator_super`. Each runs under test-runner `-mm=own`, AOT and JIT. + +Found, not fixed (every model): `a.map(f).map(g)` fails to compile ("the condition has no value"). + +### 19.5 Known limits + +- **A yielded borrow** (an element of a parameter's array, the parameter itself) is an error. The + result record type `{value: C, done}` is shared between generators, so a fact per type cannot + say that a record borrows. +- **A capturing `.map`/`.filter` callback** moves the array into the closure's box: a later use of + the array is a use after move. +- **A bounded generator cannot be returned** past the argument it borrows, even when the + argument is the returning function's own parameter (the fact does not propagate). +- **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". From 087116af6047dac574763e0fa222818fb98a2291 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 15:24:13 +0100 Subject: [PATCH 5/5] -mm=own phase 7b: a bounding argument read out of a field; the twin read's barrier The bounded check's branch for an argument read out of a place had no test. `each(h.items)` now runs in `own_generator_borrow`, and `own_err_generator_place_overwritten` assigns `h.items` while the generator is used. twinReadAfter stopped at a store into the place or a call; it now stops at any op given the place (a push writes through it too). MLIRGen emits no such op there today; the rule runs on every move out of a slot. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 7 ++++--- .../lib/TypeScript/OwnershipInferencePass.cpp | 5 +++-- tslang/test/tester/CMakeLists.txt | 1 + .../own/own_err_generator_place_overwritten.ts | 18 ++++++++++++++++++ tslang/test/tester/own/own_generator_borrow.ts | 13 ++++++++++++- 5 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 tslang/test/tester/own/own_err_generator_place_overwritten.ts diff --git a/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md b/tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md index 8ef603a3c..3c7260ee3 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 @@ -1407,10 +1407,11 @@ Each made with a temporary `getenv` switch, the JIT cache cleared between runs: ### 19.4 Tests and the corpus - **Positive:** `own_generator_borrow`. It covers an array, a class and a string parameter, a - generator given up early, two generators over one argument, `map`, `filter`, and a capturing - `filter` last. + generator given up early, two generators over one argument, an argument read out of a field, + `map`, `filter`, and a capturing `filter` last. - **Negatives:** `own_err_generator_outlives_arg`, `own_err_generator_bounded_escapes`, - `own_err_generator_yields_borrow`, `own_err_generator_map_outlives`. + `own_err_generator_yields_borrow`, `own_err_generator_map_outlives`, + `own_err_generator_place_overwritten` (the field the argument was read from is assigned). - **Removed:** `own_err_generator_param_object` compiles now and is gone. Corpus: 359 of 591 before, 364 after, none lost: `00funcs_generic_iterator`, `01disposable`, diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 8dfe1aa4a..ad58058cd 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -3426,8 +3426,9 @@ class OwnershipInferencePass return loadOp.getResult(); } - auto storeOp = mlir::dyn_cast(op); - if ((storeOp && storeOp.getReference() == place) || isCall(op)) + // anything else given the place may write through it - a store, a push - and a call may + // write anything + if (llvm::is_contained(op->getOperands(), place) || isCall(op)) { return {}; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index ca5e81a53..b0095354c 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2648,6 +2648,7 @@ set(own_error_cases "own_err_generator_bounded_escapes|borrows its generator's arguments and cannot be stored, returned or captured" "own_err_generator_yields_borrow|borrows the generator's argument and cannot be stored, returned or captured" "own_err_generator_map_outlives|borrows .* but is used here after .* is released or overwritten" + "own_err_generator_place_overwritten|borrows a field but is used here after it may be released or overwritten" "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" diff --git a/tslang/test/tester/own/own_err_generator_place_overwritten.ts b/tslang/test/tester/own/own_err_generator_place_overwritten.ts new file mode 100644 index 000000000..90cd32157 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_place_overwritten.ts @@ -0,0 +1,18 @@ +// -mm=own, phase 7b rejects: the generator borrows the array `h.items` holds, and the field is +// assigned while the generator is still used. +class H { + items: number[] = [1, 2, 3]; +} + +function* each(a: number[]) { + for (const v of a) { + yield v; + } +} + +function main() { + const h = new H(); + const it = each(h.items); + h.items = [9]; + print(it.next().value); +} diff --git a/tslang/test/tester/own/own_generator_borrow.ts b/tslang/test/tester/own/own_generator_borrow.ts index 0d815d12d..bd99537c8 100644 --- a/tslang/test/tester/own/own_generator_borrow.ts +++ b/tslang/test/tester/own/own_generator_borrow.ts @@ -7,6 +7,10 @@ class C { constructor(public x: number) {} } +class H { + items: number[] = [1, 2, 3]; +} + function churn() { const junk: C[] = []; for (let i = 0; i < 64; i++) { @@ -64,6 +68,13 @@ function main() { t += v; } + // an argument read out of a field: nothing here overwrites it + const h = new H(); + for (const v of each(h.items)) { + churn(); + t += v; + } + // given up after the first value const it = each(a); t += it.next().value; @@ -92,6 +103,6 @@ function main() { } } - assert(t == 2080000, "t"); + assert(t == 2200000, "t"); print("done."); }