From d54f7520d5e175783ee322de8c2d6052349bcbd7 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 09:46:04 +0100 Subject: [PATCH 1/4] -mm=own phase 5a: a closure that does not escape borrows what it captures The frame keeps its cells, the box frees itself only (__own_borrows_captures), and nothing that may run the closure comes after anything it borrows is given back. A captured parameter's cell borrows its value (__own_cell_borrows) and cannot be assigned, in the frame or in a closure body (__own_assigns_captures, from the signature pass). An assignment to a captured variable is a drop, and a call given a closure may run it. A closure over a variable that escapes is still an error (5b). Co-Authored-By: Claude Opus 5.5 --- .../plans/2026-10-02-own-phase-5.md | 184 ++++++ tslang/include/TypeScript/Defines.h | 7 + .../TypeScript/LowerToLLVM/LLVMCodeHelper.h | 7 +- .../LowerToLLVM/OwnershipRoutineLogic.h | 31 +- tslang/lib/TypeScript/LowerToLLVM.cpp | 35 +- tslang/lib/TypeScript/OwnershipFacts.h | 51 +- .../lib/TypeScript/OwnershipInferencePass.cpp | 561 +++++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 128 +++- tslang/test/tester/CMakeLists.txt | 12 +- tslang/test/tester/own/own_closure_borrow.ts | 144 +++++ .../own/own_err_cell_assigned_in_closure.ts | 19 + .../own/own_err_cell_assigned_in_frame.ts | 17 + tslang/test/tester/own/own_err_closure.ts | 11 +- .../own_err_closure_assigns_captured_field.ts | 20 + .../own/own_err_closure_assigns_param.ts | 18 + .../tester/own/own_err_closure_copy_moved.ts | 14 + .../own/own_err_closure_outlives_cell.ts | 14 + .../own/own_err_closure_param_assigned.ts | 16 + 18 files changed, 1252 insertions(+), 37 deletions(-) create mode 100644 tslang/docs/superpowers/plans/2026-10-02-own-phase-5.md create mode 100644 tslang/test/tester/own/own_closure_borrow.ts create mode 100644 tslang/test/tester/own/own_err_cell_assigned_in_closure.ts create mode 100644 tslang/test/tester/own/own_err_cell_assigned_in_frame.ts create mode 100644 tslang/test/tester/own/own_err_closure_assigns_captured_field.ts create mode 100644 tslang/test/tester/own/own_err_closure_assigns_param.ts create mode 100644 tslang/test/tester/own/own_err_closure_copy_moved.ts create mode 100644 tslang/test/tester/own/own_err_closure_outlives_cell.ts create mode 100644 tslang/test/tester/own/own_err_closure_param_assigned.ts diff --git a/tslang/docs/superpowers/plans/2026-10-02-own-phase-5.md b/tslang/docs/superpowers/plans/2026-10-02-own-phase-5.md new file mode 100644 index 000000000..c5177a4da --- /dev/null +++ b/tslang/docs/superpowers/plans/2026-10-02-own-phase-5.md @@ -0,0 +1,184 @@ +# `-mm=own` Phase 5 Implementation Plan: closures + +> **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 closure that captures a variable compiles when its ownership can be +proven (spec §2.5, §4.2 step 6). Today every `ts.RetainCell` is the error "closures that capture a +variable are not supported by -mm=own yet" (25 corpus files stop there first), and a captured +parameter with a heap value is a lowering error. + +After this phase: + +- A closure that does not escape **borrows** what it captures. The frame keeps its cells, the box + owns nothing but itself, and the closure may not run after anything it borrows is released. +- A closure that escapes (returned, stored, pushed, kept) **owns** what it captures. Its cells + and by-value captures move into the box, and the frame may not use them after that. +- Everything else is an error, with the rule in the message. + +**Architecture:** inference-side only, plus two marks the lowering reads. MLIRGen does not change +(`test-own-mlirgen-matches-rc` keeps passing). + +1. **`OwnershipInferencePass`** classifies each `ts.CreateBoundFunction {__owns_capture}`: does its + value escape? Then it decides each capture: a cell (a captured `ts.Variable`, or a cell an + enclosing closure captured, read out of its box) or a by-value copy (`ts.Store` of a value into + a box field, after rc's `ts.Retain`). +2. **Two marks for the lowering**, own only: + - `__own_borrows_captures` on `ts.CreateBoundFunction`: the box's tag names a routine that + frees the box and nothing it holds. + - `__own_cell_borrows` on `ts.ReleaseCell`: the cell does not own its value (a captured + parameter, `this`), so its release frees the cell only. +3. **Drops** learn cells: an assignment to a captured variable, in the frame or in a closure body, + may destroy what a borrow read out of it. A call given a closure may run it. + +**Tech Stack:** C++17, MLIR, CMake/ctest, `test/tester/test-runner.cpp`, +`test/tester/tools/measure.ps1`. + +**Spec:** `docs/superpowers/specs/2026-09-24-own-memory-model-design.md`: §2.5 (closures), §4.2 +step 6, §15.7 (this phase's input). Results go into §16. + +## Global Constraints + +- Branch `own-phase-5` from `origin/main` (7c543604, with #434 merged). +- `gc`, `rc` and `none` output does not change: the new marks are set by an own-only pass and read + by the own branch of the lowering. +- Never branch on the memory model in MLIRGen (§4.3). +- `__tslang_inc_ref` and `__tslang_dec_ref` never appear in own output. +- Own tests run with `--no-default-lib`, under JIT and AOT, and read through a `churn()` after the + point where a wrong release would free. Expected values are checked under `gc` first. +- Build: `cmake --build __build/tslang/windows-msbuild-2026-release --config Release --target tslang test-runner` + from `I:/TypeScriptCompiler`. Test: `ctest -j 16 -C Release` from the build directory. Build the + branch in WSL (GCC) before pushing. +- Source files are CRLF. Commits are GPG-signed, each ending with the session's `Co-Authored-By:`. +- Check IR shapes with `--emit=mlir-affine --own-skip-inference` (the IR is on stderr) and, since + CSE runs first under `--opt`/JIT, with `--emit=jit --mlir-print-ir-after-all --mlir-disable-threading`. +- Baseline corpus on this tree: 310 of 591 compile under `-mm=own --no-default-lib`. + +## Shapes (measured on 7c543604) + +- **The closure** is `V = ts.CreateBoundFunction(B, @f) {__owned_result, __owns_capture}` followed by + rc's `ts.Retain(V)`: a fresh value, moved or released like any other. `B` is the box, + `ts.Variable {captured, __capture_box}`, filled by `ts.Store(x, ts.PropertyRef(B, i))`. +- **A call of a folded `const f`** is devirtualized to `ts.SymbolCallInternal(B) @f`: the box is + used without `V`. A call through a value is `ts.CallInternal(ts.GetMethod(V), ts.GetThis(V))`. A + closure passed on is `ts.Cast(V)` to `!ts.hybrid_func`: the same `{func, this, tag}` triple. +- **A cell** is `ts.Variable(init) {captured}`: `__owned`/`__owned_consumed` when the frame owns the + value, nothing for a parameter's (`this` included) or for a value that owns no heap. rc: + `ts.RetainCell(cell)` before the box store, `ts.ReleaseCell(cell)` at the frame's scope exit; the + box's release routine (`tsrelcb_`) releases every cell field (value, then cell) and every + by-value field. Under own, today's `tsrelcb_` destroys all of it. +- **A cell inherited by a nested closure** is `ts.Load(ts.PropertyRef(%arg0, i))` of type `!ts.ref` + in the enclosing closure's body, `ts.RetainCell`ed for the inner box. +- **A by-value capture** (a folded `const`) is `ts.Retain(x)` then `ts.Store(x, ts.PropertyRef(B, i))`. +- **An assignment to a captured variable** is `ts.ReleaseSlot(cell)` + `ts.Store(new, cell)` in + the frame, and `ts.ReleaseSlot(ts.Load(ts.PropertyRef(%arg0, i)))` + `ts.Store` in a closure body. + Neither is in the inference's `drops`, nor in the signature pass's `dropsInBody`. +- **A loop** re-creates the box each iteration, releases the closure at the iteration's end, and + releases the cells after the loop. + +## Rulings made while planning + +- **Escape is decided on `V`'s flow, before anything else.** `V` does not escape when every use, + through function-type casts and owning `let`s (their loads), is rc's bookkeeping, a borrow + (`isBorrow`: a call's argument the callee does not keep, `ts.GetThis`/`ts.GetMethod`), or a call. + Anything else escapes: a return (a store into the result slot), a store into a field, element, + global or another box, a push, a kept argument, a boxing cast. Cost if wrong: an escaping + closure read as borrowing would free its cells at the frame's exit, so anything unknown is an + escape. +- **A non-escaping box borrows everything; an escaping box owns everything.** Mixing per field + would need a routine per closure site. Cost: a non-escaping closure cannot take ownership of a + value it is given; none needs to. +- **The closure's uses** are every use of `V` (through casts and owning `let`s) and of `B` (other + than its own fill and `V`'s creation). A borrowing closure's uses may not run after an end of + anything it borrows: + - a cell this frame owns: its `ts.ReleaseCell`s, and the point it moves into an escaping box; + - a by-value fresh value: its releases and takers; + - a parameter's cell, an inherited cell, a parameter: nothing ends in this frame. + A by-value copy of a borrow (`const c = h.c; () => c.x`) is a store of a borrow: an error, as + today (`cannot be stored, returned or captured`). Recorded as a limit. +- **A captured parameter's cell borrows its value.** Its `ts.ReleaseCell` frees the cell only + (`__own_cell_borrows`), and nothing may assign it: not the frame, not any closure body that holds + the cell, followed through nested captures. The lowering's "a captured variable cannot own its + value under -mm=own yet" goes. Cost if wrong: a parameter freed under its caller. +- **Cells are places.** `ts.ReleaseSlot` of a cell is a drop of every chain whose root this + function does not own (a read of a cell is such a root). In the signature pass the same op makes + a function drop: the cell is reachable from its creator. +- **A call given a closure may run it.** A call with an operand that is a closure value, a capture + box, or derived from one, drops every chain when its callee may drop: the box may hold anything. + Cost: spurious errors where a closure that cannot drop is passed to an unknown callee. +- **An escaping capture is a move of each cell into the box** (5b). The `ts.RetainCell` is the + move: the frame's `ts.ReleaseCell`s it reaches must be dominated by it and are erased; a frame + use of the cell reachable after it is a use after move; a second capture reachable after it is + too; a capture in a loop of a cell declared outside is moved in a loop. A parameter's cell with + a heap value and an inherited cell cannot move: an error. A cell holding a number can. Spec + §2.5's "any use after the closure's creation" is this rule. +- **Phasing.** 5a: non-escaping closures, parameter cells, the drop rules; an escaping closure is + `a closure that escapes is not supported by -mm=own yet`. 5b: escaping closures. One PR each. + +## Review Focus + +1. Escape classification: any use of `V` the classifier does not recognise must be an escape. +2. A borrowing box's routine frees the box only; an owning box's routine is today's `tsrelcb_`. +3. Every `ts.ReleaseCell` of a cell whose value is a parameter's carries `__own_cell_borrows`, or the + compile fails. +4. The three drop holes: a frame assignment to a captured variable, a closure body's, and a call + that runs a closure assigning a field of a by-value capture. Each has a negative test. + +--- + +### Task 1 (5a): the lowering marks + +**Files:** `lib/TypeScript/LowerToLLVM.cpp` (`CreateBoundFunctionOpLowering::getCaptureTypeTag`, +`ReleaseCellOpLowering`, `VariableOpLowering`), `include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h` +(`getOrCreateCaptureBoxFreeRoutine`, `emitReleaseCell` cell-only form), +`include/TypeScript/LowerToLLVM/LLVMCodeHelper.h` (a descriptor keyed apart for the borrowing box), +`lib/TypeScript/OwnershipFacts.h` (the mark names). + +- [ ] Under own, `__own_borrows_captures` picks a descriptor whose release routine frees the box + only (`tsrelcbb_`), keyed apart from the owning one. +- [ ] Under own, `__own_cell_borrows` lowers `ts.ReleaseCell` to "free the cell if mortal". +- [ ] `VariableOpLowering`: under own, a captured parameter's cell takes nothing; the error goes. + +### Task 2 (5a): non-escaping closures + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp`. + +- [ ] Classify every `__owns_capture` closure; an escaping one is the 5a error. +- [ ] For a borrowing closure: erase each `ts.RetainCell`; treat each box-field store as a borrow; + check the closure's uses against the ends of everything it borrows; mark the closure. +- [ ] Mark each `ts.ReleaseCell` of a parameter's heap cell; reject an assignment to such a cell in + the frame or in any closure body holding it. +- [ ] Remove the blanket `ts.RetainCell` error. + +**Tests:** positives `own_closure_borrow.ts` (a local, a by-value `const`, a closure passed as an +argument, in a loop, nested, `this` and a parameter, the frame assigning a captured local between +calls), negatives `own_err_closure_outlives_cell.ts` (called after the cell's scope ended), +`own_err_closure_param_assigned.ts`, `own_err_closure_borrowed_by_value.ts`. + +### Task 3 (5a): drops through cells and closures + +**Files:** `lib/TypeScript/OwnershipInferencePass.cpp` (`drops`, `dropsChain`), +`lib/TypeScript/OwnershipSignaturePass.cpp` (`dropsInBody`). + +- [ ] `ts.ReleaseSlot` of a cell is a drop of chains rooted in this function's non-owned values. +- [ ] A closure body assigning a cell may drop. +- [ ] A call given a closure or a box drops every chain when its callee may drop. + +**Tests:** negatives `own_err_cell_assigned_in_frame.ts`, `own_err_cell_assigned_in_closure.ts`, +`own_err_closure_assigns_captured_field.ts`. + +### Task 4 (5b): escaping closures + +- [ ] Cells move into an escaping box; by-value captures are takers; the errors of the rulings. + +**Tests:** positives `own_closure_escape.ts` (returned over an object local, over a number +parameter, counter, pushed into an array), negatives `own_err_closure_escape_used.ts`, +`own_err_closure_escape_twice.ts`, `own_err_closure_escape_param.ts`, +`own_err_closure_escape_loop.ts`, `own_err_closure_escape_inherited.ts`. + +### Task 5: measure, teeth, corpus, spec + +- [ ] `measure.ps1` for the positives under all four models. +- [ ] Teeth: the owning routine for a borrowing box; the frame's `ts.ReleaseCell` kept on a move. +- [ ] Corpus before/after; spec §16; memory. diff --git a/tslang/include/TypeScript/Defines.h b/tslang/include/TypeScript/Defines.h index 49e173edf..f6a6c15a8 100644 --- a/tslang/include/TypeScript/Defines.h +++ b/tslang/include/TypeScript/Defines.h @@ -67,6 +67,13 @@ // without this marker it would be indistinguishable from the cell of a captured variable - and // so would be given a frame's reference it has no owner for. A box's owner is the closure. #define CAPTURE_BOX_ATTR_NAME "__capture_box" + +// -mm=own only, set by OwnershipInferencePass for the lowering. On a `ts.CreateBoundFunction`: the +// closure does not escape, so its box borrows what it captures - the box's routine frees the box +// and nothing it holds. On a `ts.ReleaseCell`: the cell borrows its value (a captured parameter's, +// `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" #define RETURN_VARIABLE_NAME ".return" #define CAPTURED_NAME ".captured" #define LABEL_ATTR_NAME "label" diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h index e89ff5ab0..3248087c9 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h @@ -349,8 +349,11 @@ class LLVMCodeHelper : public LLVMCodeHelperBase // pointer to its trailing name bytes. That pointer is the runtime type tag: it reads as // an ordinary NUL-terminated type name, and the record is at `tag - sizeof(record)`. // See TYPE_DESCR_* in Defines.h. + // `variant` keeps apart two records of one type whose routines differ (-mm=own's borrowing + // capture box); it is not part of the name the record carries. mlir::Value getOrCreateTypeDescriptorName(mlir::Type type, std::string name, int kind, - StringRef releaseRoutineName, StringRef retainRoutineName) + StringRef releaseRoutineName, StringRef retainRoutineName, + StringRef variant = "") { auto loc = op->getLoc(); auto parentModule = op->getParentOfType(); @@ -360,7 +363,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase // keyed by the concrete type rather than by the name: every class reports the name // "class", but each needs its own record, which is the point of having one at all std::stringstream varName; - varName << "td_" << (size_t)hash_value(type) << "_" << name; + varName << "td_" << (size_t)hash_value(type) << variant.str() << "_" << name; TypeConverterHelper tch(typeConverter); auto llvmIndexType = tch.convertType(th.getIndexType()); diff --git a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h index 1a6394613..82658145c 100644 --- a/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h +++ b/tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h @@ -261,11 +261,15 @@ class OwnershipRoutineLogic } // The value goes back before the block does, and in that order: releasing it reads the - // cell. - void emitReleaseCell(mlir::Type contentsType, mlir::Value cellPtr) + // cell. A cell that borrows its value (-mm=own, OWN_CELL_BORROWS_ATTR_NAME) frees itself only. + void emitReleaseCell(mlir::Type contentsType, mlir::Value cellPtr, bool borrowsContents = false) { emitIfLastReference(cellPtr, [&]() { - releaseSlot(contentsType, cellPtr); + if (!borrowsContents) + { + releaseSlot(contentsType, cellPtr); + } + emitFreeBlock(cellPtr); }); } @@ -286,6 +290,13 @@ class OwnershipRoutineLogic return buildCaptureBoxRoutine(captureRefType, "tsrelcb_", /*retaining=*/false); } + // -mm=own's box of a closure that does not escape (OWN_BORROWS_CAPTURES_ATTR_NAME): it borrows + // every cell and every value it holds from the frame that built it, so it frees itself only. + std::string getOrCreateCaptureBoxFreeRoutine(mlir_ts::RefType captureRefType) + { + return buildCaptureBoxRoutine(captureRefType, "tsfrecb_", /*retaining=*/false, /*holdsNothing=*/true); + } + // 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. std::string getOrCreateCaptureBoxRetainRoutine(mlir_ts::RefType captureRefType) @@ -789,9 +800,11 @@ class OwnershipRoutineLogic releaseViaTagBesideThis(type, slotPtr, INTERFACE_TYPE_INDEX, retaining); } - // Body of both capture-box routines: they take the storage holding the box pointer, like - // every other routine, so each begins by loading the box out of it. - std::string buildCaptureBoxRoutine(mlir_ts::RefType captureRefType, StringRef prefix, bool retaining) + // Body of the capture-box routines: they take the storage holding the box pointer, like + // 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) { std::stringstream nameStream; nameStream << prefix.str() << (size_t)hash_value(captureRefType); @@ -824,7 +837,11 @@ class OwnershipRoutineLogic else { emitIfLastReference(boxValue, [&]() { - releaseCapturedFields(captureRefType.getElementType(), boxValue); + if (!holdsNothing) + { + releaseCapturedFields(captureRefType.getElementType(), boxValue); + } + emitFreeBlock(boxValue); }); } diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index dabadc032..07da8cf6f 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -549,7 +549,9 @@ class ReleaseCellOpLowering : public TsLlvmPattern { OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); orl.emitReleaseCell(cast(op.getSlot().getType()).getElementType(), - transformed.getSlot()); + transformed.getSlot(), + tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && + op->hasAttr(OWN_CELL_BORROWS_ATTR_NAME)); } rewriter.eraseOp(op); @@ -2610,19 +2612,13 @@ struct VariableOpLowering : public TsLlvmPattern // (`isOwningSlot`). So the first value has to be taken too, unless the frame has // already taken it - which is what an owned local's mark says, and what a captured // parameter's storage is precisely missing, the argument being the caller's. - if (isCaptured && tsLlvmContext->compileOptions.tracksOwnership() && + // + // Not under own, which has no count to take: such a cell borrows the value, and its + // release frees the cell alone (OWN_CELL_BORROWS_ATTR_NAME, which ownership inference + // sets, or rejects the program). + if (isCaptured && tsLlvmContext->compileOptions.isRefCounted() && !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME) && !varOp->hasAttr(OWNED_LOCAL_ATTR_NAME)) { - // The value is someone else's - a parameter's is the caller's - and a cell that - // destroyed it would free it under them. Own has no count to take here: closures - // are phase 5. - if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && - MLIRTypeHelper(rewriter.getContext(), tsLlvmContext->compileOptions) - .ownsHeapMemory(location, referenceType.getElementType())) - { - return varOp.emitError("a captured variable cannot own its value under -mm=own yet"); - } - OwnershipRoutineLogic orl(varOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); orl.emitRetainSlot(referenceType.getElementType(), allocated); } @@ -6183,13 +6179,22 @@ struct CreateBoundFunctionOpLowering : public TsLlvmPattern>` for the same reason, so neither the routines nor the // descriptor can be reused for a plain object of the same shape. + // + // Under own, a closure that does not escape borrows what its box holds + // (OWN_BORROWS_CAPTURES_ATTR_NAME), so its routine frees the box and nothing else. OwnershipRoutineLogic orl(createBoundFunctionOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - auto releaseRoutineName = orl.getOrCreateCaptureBoxReleaseRoutine(captureRefType); - auto retainRoutineName = orl.getOrCreateCaptureBoxRetainRoutine(captureRefType); + auto borrows = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn && + createBoundFunctionOp->hasAttr(OWN_BORROWS_CAPTURES_ATTR_NAME); + auto releaseRoutineName = borrows ? orl.getOrCreateCaptureBoxFreeRoutine(captureRefType) + : orl.getOrCreateCaptureBoxReleaseRoutine(captureRefType); + // under own nothing retains: see TypeDescriptorOpLowering + auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn + ? std::string() + : orl.getOrCreateCaptureBoxRetainRoutine(captureRefType); LLVMCodeHelper ch(createBoundFunctionOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); return ch.getOrCreateTypeDescriptorName(captureRefType, name, TypeOfOpHelper::typeKindFromName(name), - releaseRoutineName, retainRoutineName); + releaseRoutineName, retainRoutineName, borrows ? "b" : ""); } LogicalResult matchAndRewrite(mlir_ts::CreateBoundFunctionOp createBoundFunctionOp, Adaptor transformed, diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index 0630acdca..9359cd276 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -39,6 +39,11 @@ namespace mlir_ts = mlir::typescript; // the function among its `__own_no_drops` - and only its blocks are ones this module may destroy: // a block made under rc still carries a count its own module holds. #define OWN_FRESH_RESULT_ATTR_NAME "__own_fresh_result" +// On a closure (`ts.CreateBoundFunction`, OWNS_CAPTURE_ATTR_NAME): the fields of its capture box +// whose captured variable its body assigns, itself or through a closure it builds over the same +// 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 of a call, without the callee value of an indirect one. inline mlir::OperandRange callArgs(mlir::Operation *op) @@ -100,7 +105,13 @@ inline bool isView(mlir::Operation *op) auto keeps = [](mlir::Type type) { return mlir::isa(type); }; - return keeps(castOp.getIn().getType()) && keeps(castOp.getType()); + // a bound function and a hybrid one are the same `{func, this, tag}`: a closure passed on as + // a function value is still its box + auto isClosure = [](mlir::Type type) { + return mlir::isa(type); + }; + return (keeps(castOp.getIn().getType()) && keeps(castOp.getType())) || + (isClosure(castOp.getIn().getType()) && isClosure(castOp.getType())); } // The block a value is a view of. @@ -293,6 +304,44 @@ inline bool holdsNoBlock(mlir::Value value) return !ownsHeap(value) || (def && isLiteral(def)); } +// 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 +// the closures over the same cell, hold borrows of. +inline bool isLoadedCell(mlir::Value value) +{ + auto loadOp = value.getDefiningOp(); + return loadOp && mlir::isa(loadOp.getType()); +} + +// A captured variable's cell, in the function that declared it. +inline bool isCellVariable(mlir::Value value) +{ + auto varOp = value.getDefiningOp(); + return varOp && varOp.getCaptured().value_or(false) && !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); +} + +// The capture box a closure was built over, if `value` is a capture box's own variable. +inline mlir_ts::CreateBoundFunctionOp closureOfBox(mlir::Value box) +{ + auto varOp = box.getDefiningOp(); + if (!varOp || !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME)) + { + return {}; + } + + for (auto *user : box.getUsers()) + { + if (auto boundOp = mlir::dyn_cast(user); + boundOp && boundOp.getThisVal() == box && boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME)) + { + return boundOp; + } + } + + return {}; +} + // Is this the slot of parameter `argument` - a local declared from it that owns nothing, is not // captured, and is never assigned, so every read of it is the argument? inline bool isParameterSlot(mlir_ts::VariableOp varOp, int &index) diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 2842e4adf..02829ef11 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -27,6 +27,10 @@ namespace using namespace own_facts; +// This pass's own mark, for its own reading: a store of a value into the capture box of a closure +// that borrows what it captures (phase 5). The store is a read, not a taker. +#define OWN_CAPTURE_BORROW_ATTR_NAME "__own_capture_borrow" + // Ownership inference for -mm=own, phases 0 and 1. See // docs/superpowers/specs/2026-09-24-own-memory-model-design.md, sections 4.2, 10.6, 11 and 12. // @@ -64,8 +68,14 @@ class OwnershipInferencePass : public mlir::PassWrapper keepingCalls; drops.clear(); derivedCache.clear(); + closures.clear(); + captureStores.clear(); + captureRetains.clear(); returnsBorrowOf = resultBorrows(f); + // first: whether a closure borrows what it captures changes what its box's stores are + classifyClosures(); + // A view of a block is that block: the candidate is always the root. f.walk([&](mlir::Operation *op) { if (auto retainOp = mlir::dyn_cast(op)) @@ -90,8 +100,12 @@ class OwnershipInferencePass : public mlir::PassWrapper(op)) { - op->emitError("closures that capture a variable are not supported by -mm=own yet"); - signalPassFailure(); + // a box taking a cell: a closure that borrows it (decideClosures) claimed it + if (!captureRetains.contains(op)) + { + op->emitError("closures that capture a variable are not supported by -mm=own here yet"); + signalPassFailure(); + } } else if (mlir::isa(op)) { @@ -107,8 +121,9 @@ class OwnershipInferencePass : public mlir::PassWrapper(op)) { - if (mlir::isa_and_nonnull( - releaseSlotOp.getSlot().getDefiningOp())) + // an overwrite of a field or an element, or an assignment to a captured variable + auto slot = releaseSlotOp.getSlot(); + if (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot)) { drops.push_back(op); } @@ -165,6 +180,11 @@ class OwnershipInferencePass : public mlir::PassWrapper> paramReceivers; for (auto *op : retains) { + if (captureRetains.contains(op)) + { + continue; // a borrowing box's copy of a value: decideClosures decides it + } + auto value = retainedValue(op); if (value && proven.contains(value)) { @@ -255,6 +275,9 @@ class OwnershipInferencePass : public mlir::PassWrappererase(); @@ -264,7 +287,8 @@ class OwnershipInferencePass : public mlir::PassWrapperremoveAttr(name); } @@ -290,6 +314,471 @@ class OwnershipInferencePass : public mlir::PassWrapper returnedParams; + // ---- Closures (spec 2.5, phase 5) ---- + // + // A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`; its box holds a cell for each + // variable it captures by reference and a copy of each it captures by value. One that does not + // escape borrows all of it: rc's references for the box go, the box frees itself only + // (OWN_BORROWS_CAPTURES_ATTR_NAME), and nothing that may run the closure may come after anything + // it borrows is given back. One that escapes would own all of it, which is not done yet. + struct Closure + { + mlir_ts::CreateBoundFunctionOp op; + mlir::Value box; + // each store that fills the box, with the field it fills + llvm::SmallVector> fills; + bool hasCells = false; + // the first use that takes the closure out of this function's hands + mlir::Operation *escape = nullptr; + // what may run the closure: calls given it, called through it, or through its box + llvm::SmallVector uses; + // the calls through the box itself, made without the closure value (a folded `const f`) + llvm::SmallVector boxUses; + // where the closure value is given back: its releases, and those of the `let`s holding it + llvm::SmallVector ends; + // rc's references for the box: `ts.RetainCell` of each cell, `ts.Retain` of each copy + llvm::SmallVector retains; + // 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; + }; + + llvm::SmallVector> closures; + // a copy stored into a borrowing box -> its closure + llvm::DenseMap captureStores; + // rc's references for boxes, which the closures decide + llvm::DenseSet captureRetains; + + // 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 + // with it - its box owns them, and each copy moves in. + void classifyClosures() + { + getFunction().walk([&](mlir_ts::CreateBoundFunctionOp boundOp) { + if (!boundOp->hasAttr(OWNS_CAPTURE_ATTR_NAME) || closureOfBox(boundOp.getThisVal()) != boundOp) + { + return; + } + + auto closure = std::make_shared(); + closure->op = boundOp; + closure->box = boundOp.getThisVal(); + for (auto *user : closure->box.getUsers()) + { + if (user == boundOp.getOperation()) + { + continue; + } + + if (auto propertyRefOp = mlir::dyn_cast(user)) + { + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + if (!storeOp || storeOp.getReference() != propertyRefOp.getResult()) + { + closure->escape = closure->escape ? closure->escape : fieldUser; + continue; + } + + closure->fills.push_back({propertyRefOp.getPosition(), storeOp}); + closure->hasCells = closure->hasCells || mlir::isa(storeOp.getValue().getType()); + } + + continue; + } + + if (isCall(user)) + { + closure->uses.push_back(user); + closure->boxUses.push_back(user); + continue; + } + + closure->escape = closure->escape ? closure->escape : user; + } + + if (!closure->hasCells) + { + return; + } + + if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); + escape && !closure->escape) + { + closure->escape = escape; + } + + for (auto &fill : closure->fills) + { + auto storeOp = fill.second; + if (auto *retain = retainBefore(storeOp.getValue(), storeOp)) + { + closure->retains.push_back(retain); + captureRetains.insert(retain); + } + + if (!closure->escape && !mlir::isa(storeOp.getValue().getType())) + { + storeOp->setAttr(OWN_CAPTURE_BORROW_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + captureStores[storeOp] = closure.get(); + } + } + + if (!closure->escape) + { + boundOp->setAttr(OWN_BORROWS_CAPTURES_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + } + + closures.push_back(std::move(closure)); + }); + } + + // rc's reference for the box's copy of `value`: the retain of it nearest before the store that + // fills the box, in the same block. + mlir::Operation *retainBefore(mlir::Value value, mlir::Operation *store) + { + mlir::Operation *found = nullptr; + for (auto *user : value.getUsers()) + { + if (!mlir::isa(user) || user->getBlock() != store->getBlock() || + !user->isBeforeInBlock(store) || captureRetains.contains(user)) + { + continue; + } + + if (!found || found->isBeforeInBlock(user)) + { + found = user; + } + } + + return found; + } + + // Where a closure value goes: through its views and the owning `let`s that hold it, each use + // that may run it joins `uses`, and each release `ends`. Answers the first use that takes it + // anywhere else - returned, stored, kept by a callee, captured - or null. Anything this does + // not know is such a use. + mlir::Operation *followClosure(mlir::Value closure, llvm::SmallVectorImpl &uses, + llvm::SmallVectorImpl &ends, bool &aliased) + { + llvm::SmallVector values{closure}; + llvm::SmallPtrSet seen; + mlir::Operation *escape = nullptr; + auto followSlot = [&](mlir::Value slot) { + for (auto *slotUser : slot.getUsers()) + { + if (mlir::isa(slotUser)) + { + continue; + } + + if (mlir::isa(slotUser)) + { + ends.push_back(slotUser); + } + else if (auto loadOp = mlir::dyn_cast(slotUser)) + { + values.push_back(loadOp.getResult()); + } + else if (auto storeOp = mlir::dyn_cast(slotUser); !storeOp || storeOp.getReference() != slot) + { + escape = escape ? escape : slotUser; + } + } + }; + + while (!values.empty() && !escape) + { + auto value = values.pop_back_val(); + if (!seen.insert(value).second) + { + continue; + } + + forEachUse(value, [&](mlir::Operation *user, mlir::Value used) { + if (escape || mlir::isa(user)) + { + return; + } + + if (mlir::isa(user)) + { + ends.push_back(user); + return; + } + + if (isCall(user)) + { + if (isKeptArgument(user, used)) + { + escape = user; + } + else + { + uses.push_back(user); + } + + return; + } + + // the function and the box a call through the closure is made with + if (mlir::isa(user)) + { + for (auto *partUser : user->getResult(0).getUsers()) + { + if (isCall(partUser) && !isKeptArgument(partUser, user->getResult(0))) + { + uses.push_back(partUser); + } + else + { + escape = escape ? escape : partUser; + } + } + + 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(); + } + + // a local, owning or not (`let f: () => number;` owns nothing): its reads are the + // closure; a function's result slot is a local too, and its read is returned + auto slotVar = slot ? slot.getDefiningOp() : mlir_ts::VariableOp(); + if (slotVar && !slotVar.getCaptured().value_or(false)) + { + aliased = aliased || !isOwningVariable(slotVar); + followSlot(slot); + return; + } + + escape = user; + }); + } + + return escape; + } + + // Does a call given this value give it a closure, which may run and touch what its box holds? + static bool holdsCaptures(mlir::Value value) + { + if (mlir::isa(value.getType())) + { + return true; + } + + auto root = rootOf(value); + if (auto varOp = root.getDefiningOp()) + { + return varOp->hasAttr(CAPTURE_BOX_ATTR_NAME); + } + + // a closure's `this` is its box, a reference; a bound method's is the object + auto getThisOp = root.getDefiningOp(); + return getThisOp && mlir::isa(getThisOp.getType()); + } + + // The verdicts on the closures over cells: an escape is an error; a borrow must not be run after + // anything it borrows is given back - a cell this function owns at its releases, a copy at its + // owner's ends. A cell an enclosing closure holds and a parameter end nowhere here; a copy of a + // read out of a container is bounded by the read's own check, which sees the closure's uses + // (walkBorrowed). + void decideClosures(llvm::SetVector &toErase) + { + for (auto &closure : closures) + { + auto *boundOp = closure->op.getOperation(); + auto name = ownerName(closure->op.getResult()); + if (closure->escape) + { + reportClosureEscapes(closure->escape, boundOp); + continue; + } + + auto ok = true; + for (auto &fill : closure->fills) + { + auto value = fill.second.getValue(); + llvm::SmallVector ends; + llvm::StringRef captured = "this value"; + if (mlir::isa(value.getType())) + { + if (isCellVariable(value)) + { + captured = varName(value.getDefiningOp()); + for (auto *user : value.getUsers()) + { + if (mlir::isa(user)) + { + ends.push_back(user); + } + } + } + else if (!isLoadedCell(value)) + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + } + else + { + auto root = rootOf(value); + captured = ownerName(root); + Chain chain; + if (holdsNoBlock(value) || placeReadOf(root) || borrowedParam(root) >= 0) + { + // nothing to own, a read the read's check bounds, or a parameter + } + 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 + { + reportCaptureNotOwned(fill.second, captured); + ok = false; + continue; + } + + ends = rootEnds(chain); + } + + for (auto *end : ends) + { + if (captureRetains.contains(end) || captureStores.count(end)) + { + continue; // a box's own copy, this one's or another borrowing closure's + } + + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, captured); + ok = false; + } + } + } + } + + // Held by a local that owns nothing: rc's birth reference reads to the move logic as the + // store's, which would leave the box with no owner. The closure keeps its releases, and + // nothing may run it after them. + if (closure->aliased) + { + for (auto *end : closure->ends) + { + if (!mlir::isa(end)) + { + continue; + } + + toErase.remove(end); + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + } + + // a call through the box, after the closure value that owns the box is given back + for (auto *end : closure->ends) + { + for (auto *use : closure->boxUses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + + toErase.insert(closure->retains.begin(), closure->retains.end()); + } + } + + // A captured variable whose value this function does not own - a parameter, `this` - has a cell + // that borrows the value: the cell's release frees the cell only (OWN_CELL_BORROWS_ATTR_NAME), + // and nothing may assign the variable, here or in a closure over it, since the old value is the + // caller's. A variable whose value nobody can be said to own is an error. + void decideCells() + { + getFunction().walk([&](mlir_ts::VariableOp varOp) { + auto cell = varOp.getResult(); + if (!isCellVariable(cell) || isOwningVariable(varOp)) + { + return; + } + + MLIRTypeHelper mth(&getContext(), CompileOptions{}); + if (!mth.ownsHeapMemory(varOp.getLoc(), mlir::cast(cell.getType()).getElementType())) + { + return; + } + + auto name = varName(varOp); + auto init = varOp.getInitializer(); + auto argument = init ? mlir::dyn_cast(init) : mlir::BlockArgument(); + if (!argument || !argument.getOwner()->isEntryBlock() || + !mlir::isa(argument.getOwner()->getParentOp())) + { + reportCaptureNotOwned(varOp, name); + return; + } + + for (auto *user : cell.getUsers()) + { + if (mlir::isa(user)) + { + user->setAttr(OWN_CELL_BORROWS_ATTR_NAME, mlir::UnitAttr::get(&getContext())); + continue; + } + + auto storeOp = mlir::dyn_cast(user); + if (mlir::isa(user) || (storeOp && storeOp.getReference() == cell)) + { + reportCapturedParamAssigned(user, name); + return; + } + + // captured: the closure's body must not assign it either + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp) + { + continue; + } + + auto assigned = boundOp->getAttrOfType(OWN_ASSIGNS_CAPTURES_ATTR_NAME); + if (assigned && llvm::is_contained(assigned.asArrayRef(), static_cast(propertyRefOp.getPosition()))) + { + reportCapturedParamAssigned(boundOp, name); + return; + } + } + }); + } + struct SlotReceiver { mlir::Operation *retain; @@ -688,7 +1177,22 @@ class OwnershipInferencePass : public mlir::PassWrappersecond->uses) + { + uses.push_back({closureUse, pending.kills}); + } + } + if (pending.borrowed && keeping.returns && mlir::isa(user)) { continue; @@ -1090,7 +1594,14 @@ class OwnershipInferencePass : public mlir::PassWrapper(drop)) { + // An assignment to a captured variable destroys its old value, which is reached only + // through a read of the variable's cell: a root this function does not own. auto slot = releaseSlotOp.getSlot(); + if (isCellVariable(slot) || isLoadedCell(slot)) + { + return !chain.owned(); + } + int64_t position = -1; if (auto propertyRefOp = slot.getDefiningOp()) { @@ -1109,7 +1620,8 @@ class OwnershipInferencePass : public mlir::PassWrappergetOperands(), holdsCaptures)) { return true; } @@ -1500,6 +2012,13 @@ class OwnershipInferencePass : public mlir::PassWrapper(user) && user->hasAttr(OWN_CAPTURE_BORROW_ATTR_NAME)) + { + return true; + } + return false; } @@ -2227,6 +2746,36 @@ class OwnershipInferencePass : public mlir::PassWrapperemitError("a closure that captures a variable and escapes here is not supported by -mm=own yet"); + diag.attachNote(closure->getLoc()) << "the closure is created here"; + signalPassFailure(); + } + + void reportClosureOutlives(mlir::Operation *use, mlir::Operation *end, llvm::StringRef closure, + llvm::StringRef captured) + { + auto diag = use->emitError("'") << closure << "' borrows '" << captured << "', which it captures, but runs here after '" + << captured << "' is released"; + diag.attachNote(end->getLoc()) << "'" << captured << "' is released here"; + signalPassFailure(); + } + + void reportCaptureNotOwned(mlir::Operation *op, llvm::StringRef captured) + { + op->emitError("'") << captured << "' is captured by a closure, but -mm=own cannot tell who owns its value here"; + signalPassFailure(); + } + + void reportCapturedParamAssigned(mlir::Operation *op, llvm::StringRef name) + { + op->emitError("'") << name + << "' is a captured parameter and cannot be assigned under -mm=own: its value is the " + "caller's"; + signalPassFailure(); + } + void reportMovedOnSomePaths(mlir::Operation *move, mlir::Operation *release, llvm::StringRef name) { if (quiet) diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index bf70a1e34..b5d4993ae 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -11,6 +11,7 @@ #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSet.h" @@ -104,9 +105,131 @@ class OwnershipSignaturePass : public mlir::PassWrapperhasAttr(OWNS_CAPTURE_ATTR_NAME)) + { + return; + } + + auto assigned = assignedCaptures(closureBody(boundOp)); + if (!assigned.empty()) + { + boundOp->setAttr(OWN_ASSIGNS_CAPTURES_ATTR_NAME, + mlir::DenseI32ArrayAttr::get(&getContext(), assigned.getArrayRef())); + } + }); } private: + // The function a closure runs, when it names one defined here. + mlir_ts::FuncOp closureBody(mlir_ts::CreateBoundFunctionOp boundOp) + { + auto symbolRefOp = boundOp.getFunc().getDefiningOp(); + if (!symbolRefOp) + { + return {}; + } + + auto found = functions.find(symbolRefOp.getIdentifier()); + return found != functions.end() && !found->second.isDeclaration() ? found->second : mlir_ts::FuncOp(); + } + + // The fields of a closure body's capture box whose cell it assigns (OWN_ASSIGNS_CAPTURES_ATTR_NAME), + // directly or through a closure it builds over the cell. Every field, when the body is unknown or + // a cell goes anywhere this does not follow. + llvm::SetVector assignedCaptures(mlir_ts::FuncOp funcOp, int depth = 0) + { + llvm::SetVector assigned; + auto boxType = funcOp && funcOp.getNumArguments() > 0 && !funcOp.getBody().empty() + ? mlir::dyn_cast(funcOp.getArgument(0).getType()) + : mlir_ts::RefType(); + auto tupleType = boxType ? mlir::dyn_cast(boxType.getElementType()) : mlir_ts::TupleType(); + if (!tupleType || depth > 16) + { + // unknown: a box of any shape may have had any of its cells assigned + for (int32_t index = 0; index < 64; ++index) + { + assigned.insert(index); + } + + return assigned; + } + + if (auto found = assignedCache.find(funcOp); found != assignedCache.end()) + { + return found->second; + } + + for (auto *user : funcOp.getArgument(0).getUsers()) + { + auto propertyRefOp = mlir::dyn_cast(user); + if (!propertyRefOp) + { + continue; + } + + auto field = propertyRefOp.getPosition(); + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto loadOp = mlir::dyn_cast(fieldUser); + if (!loadOp || !isLoadedCell(loadOp.getResult())) + { + continue; // a field captured by value: a copy, not a variable + } + + if (cellAssigned(loadOp.getResult(), depth)) + { + assigned.insert(field); + } + } + } + + assignedCache[funcOp] = assigned; + return assigned; + } + + // Is the variable behind this cell assigned - here, or by a closure built over it? + bool cellAssigned(mlir::Value cell, int depth) + { + for (auto &use : cell.getUses()) + { + auto *user = use.getOwner(); + if (mlir::isa(user)) + { + continue; + } + + if (mlir::isa(user)) + { + return true; + } + + auto storeOp = mlir::dyn_cast(user); + if (!storeOp) + { + return true; // the cell goes somewhere this does not follow + } + + if (storeOp.getReference() == cell) + { + return true; + } + + // captured again, by a closure built here: the field it fills in that closure's box + auto propertyRefOp = storeOp.getReference().getDefiningOp(); + auto boundOp = propertyRefOp ? closureOfBox(propertyRefOp.getObjectRef()) : mlir_ts::CreateBoundFunctionOp(); + if (!boundOp || assignedCaptures(closureBody(boundOp), depth + 1).contains(propertyRefOp.getPosition())) + { + return true; + } + } + + return false; + } + + llvm::DenseMap> assignedCache; + // What a call may reach: known, when every candidate is a function defined in this module (or // the call goes through the `.instanceOf` slot, or reaches a function another module says // destroys nothing); else anything. @@ -1003,8 +1126,11 @@ class OwnershipSignaturePass : public mlir::PassWrapper(op)) { + // a captured variable assigned through its cell, by a closure's body: the variable + // is its declaring function's auto slot = releaseSlotOp.getSlot(); - drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)); + drops = slot.getDefiningOp() || (isPlace(slot) && reachesOutside(slot, funcOp)) || + isLoadedCell(slot); } else if (mlir::isa(op)) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index e47889917..3556f6378 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2562,7 +2562,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_optional_access own_borrow_merge own_field_let_borrow own_element_let_borrow own_borrow_merge_after_call own_call_no_drops own_getter_borrow own_param_kept own_any_box - own_string_length_borrow) + own_string_length_borrow + own_closure_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. @@ -2625,7 +2626,14 @@ set(own_error_cases "own_err_const_alias_move|borrows .* and cannot be assigned" "own_err_const_alias_field|is used here after its value was moved" "own_err_field_alias|is used here after its value was moved" - "own_err_closure|closures that capture a variable are not supported" + "own_err_closure|a closure that captures a variable and escapes here is not supported by -mm=own yet" + "own_err_closure_outlives_cell|borrows .*, which it captures, but runs here after .* is released" + "own_err_closure_copy_moved|borrows .*, which it captures, but runs here after .* is released" + "own_err_closure_param_assigned|is a captured parameter and cannot be assigned under -mm=own" + "own_err_closure_assigns_param|is a captured parameter and cannot be assigned under -mm=own" + "own_err_cell_assigned_in_frame|borrows .* but is used here after it may be released or overwritten" + "own_err_cell_assigned_in_closure|borrows .* but is used here after it may be released or overwritten" + "own_err_closure_assigns_captured_field|borrows .* but is used here after it may be released or overwritten" "own_err_unknown_producer|takes a second reference" "own_err_two_owners|is used here after its value was moved" "own_err_push_let|is used here after its value was moved" diff --git a/tslang/test/tester/own/own_closure_borrow.ts b/tslang/test/tester/own/own_closure_borrow.ts new file mode 100644 index 000000000..d7a37212f --- /dev/null +++ b/tslang/test/tester/own/own_closure_borrow.ts @@ -0,0 +1,144 @@ +// -mm=own, phase 5: a closure that does not escape borrows what it captures. The frame keeps the +// cells of its variables, the box frees itself only, and a captured parameter's cell frees the +// cell and not the caller's value. Each case reads through a churn() after the point where a wrong +// release would free. +class C { + v: number[] = []; + constructor(public x: number) {} +} + +function churn() { + const junk: C[] = []; + for (let i = 0; i < 64; i++) { + const c = new C(999); + c.v.push(99); + junk.push(c); + } +} + +function apply(f: () => number) { + churn(); + return f(); +} + +// a local, called through a folded `const`; the frame still owns it afterwards +function local(n: number) { + let a = new C(n); + const f = () => a.x + a.v.length; + churn(); + const r = f(); + churn(); + return r + a.x; +} + +// a copy of a `const` beside a cell: the box borrows it too +function byValue(n: number) { + const h = new C(n); + let k = 0; + const f = () => h.x + k; + churn(); + return f() + h.x; +} + +// passed as an argument +function asArgument(n: number) { + let a = new C(n); + const r = apply(() => a.x); + churn(); + return r + a.x; +} + +// a box per iteration; the cells go after the loop +function inLoop(n: number) { + let a = new C(n); + let t = 0; + for (let k = 0; k < 3; k++) { + const f = () => a.x + k; + t += f(); + } + + churn(); + return t + a.x; +} + +// the inner closure borrows the cell the outer one borrows +function nested(n: number) { + let a = new C(n); + const g = () => { + const h = () => a.x; + churn(); + return h(); + }; + const r = g(); + churn(); + return r + a.x; +} + +// `this` and a parameter: cells that borrow their values from the caller +class D { + constructor(public c: C) {} + + read() { + const f = () => this.c.x; + churn(); + return f(); + } +} + +function param(c: C) { + const f = () => c.x; + churn(); + return f() + c.x; +} + +// the frame assigns the captured local between calls, and the closure sees the new value +function frameAssigns(n: number) { + let a = new C(n); + const f = () => a.x; + const first = f(); + a = new C(n + 1); + churn(); + return first + f(); +} + +// the closure assigns the captured local +function closureAssigns(n: number) { + let a = new C(n); + const f = () => { + a = new C(n + 2); + }; + f(); + churn(); + return a.x; +} + +// a `let` holds the closure +function letHolds(n: number) { + let a = new C(n); + let f = () => a.x; + churn(); + return f(); +} + +// a `let` with no initializer owns nothing: it is an alias, and the closure keeps its release +function letAliases(n: number) { + let a = new C(n); + let f: () => number; + f = () => a.x; + churn(); + return f(); +} + +function main() { + let t = 0; + for (let i = 0; i < 20000; i++) { + const n = i % 10; + const d = new D(new C(n)); + const c = new C(n); + t += local(n) + byValue(n) + asArgument(n) + inLoop(n) + nested(n) + d.read() + param(c) + + frameAssigns(n) + closureAssigns(n) + letHolds(n) + letAliases(n); + } + + assert(t == 1920000); + print("done."); +} diff --git a/tslang/test/tester/own/own_err_cell_assigned_in_closure.ts b/tslang/test/tester/own/own_err_cell_assigned_in_closure.ts new file mode 100644 index 000000000..7b4a4b545 --- /dev/null +++ b/tslang/test/tester/own/own_err_cell_assigned_in_closure.ts @@ -0,0 +1,19 @@ +// -mm=own, phase 5 rejects: `y` borrows a field of the value in `a`'s cell, and the closure +// assigns `a`. +class C { + constructor(public x: number) {} +} + +class H { + constructor(public c: C) {} +} + +function main() { + let a = new H(new C(1)); + const f = () => { + a = new H(new C(2)); + }; + const y = a.c; + f(); + print(y.x); +} diff --git a/tslang/test/tester/own/own_err_cell_assigned_in_frame.ts b/tslang/test/tester/own/own_err_cell_assigned_in_frame.ts new file mode 100644 index 000000000..b5e79ff1f --- /dev/null +++ b/tslang/test/tester/own/own_err_cell_assigned_in_frame.ts @@ -0,0 +1,17 @@ +// -mm=own, phase 5 rejects: `y` borrows a field of the value in `a`'s cell, and assigning `a` +// destroys that value. +class C { + constructor(public x: number) {} +} + +class H { + constructor(public c: C) {} +} + +function main() { + let a = new H(new C(1)); + const f = () => a.c.x; + const y = a.c; + a = new H(new C(2)); + print(y.x, f()); +} diff --git a/tslang/test/tester/own/own_err_closure.ts b/tslang/test/tester/own/own_err_closure.ts index 3e77e1f71..1080c8877 100644 --- a/tslang/test/tester/own/own_err_closure.ts +++ b/tslang/test/tester/own/own_err_closure.ts @@ -1,7 +1,12 @@ -// -mm=own, phase 0 rejects: a closure capturing a heap value (closures are phase 5). +// -mm=own, phase 5 rejects: a closure over a variable that escapes (returned). Its box would own +// the cell, which phase 5b does. +function makeCounter() { + let n = 0; + return () => ++n; +} + function main() { - const a: number[] = [1]; - const f = () => a.length; + const f = makeCounter(); print(f()); print("done."); } diff --git a/tslang/test/tester/own/own_err_closure_assigns_captured_field.ts b/tslang/test/tester/own/own_err_closure_assigns_captured_field.ts new file mode 100644 index 000000000..8a96b3852 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_assigns_captured_field.ts @@ -0,0 +1,20 @@ +// -mm=own, phase 5 rejects: `y` borrows `h.c`, and the closure, which holds `h`, overwrites it. +class C { + constructor(public x: number) {} +} + +class H { + constructor(public c: C) {} +} + +function main() { + const h = new H(new C(1)); + let k = 0; + const f = () => { + h.c = new C(2); + k++; + }; + const y = h.c; + f(); + print(y.x, k); +} diff --git a/tslang/test/tester/own/own_err_closure_assigns_param.ts b/tslang/test/tester/own/own_err_closure_assigns_param.ts new file mode 100644 index 000000000..e6a71a4cc --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_assigns_param.ts @@ -0,0 +1,18 @@ +// -mm=own, phase 5 rejects: the closure assigns the captured parameter `c`, whose value is the +// caller's. +class C { + constructor(public x: number) {} +} + +function reset(c: C) { + const g = () => { + c = new C(2); + }; + g(); + return c.x; +} + +function main() { + const c = new C(1); + print(reset(c), c.x); +} diff --git a/tslang/test/tester/own/own_err_closure_copy_moved.ts b/tslang/test/tester/own/own_err_closure_copy_moved.ts new file mode 100644 index 000000000..229360a39 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_copy_moved.ts @@ -0,0 +1,14 @@ +// -mm=own, phase 5 rejects: the closure borrows its copy of `h`, and is called after `h` moved +// into the array. +class C { + constructor(public x: number) {} +} + +function main() { + const keep: C[] = []; + const h = new C(1); + let k = 0; + const f = () => h.x + k; + keep.push(h); + print(f(), keep.length); +} diff --git a/tslang/test/tester/own/own_err_closure_outlives_cell.ts b/tslang/test/tester/own/own_err_closure_outlives_cell.ts new file mode 100644 index 000000000..0ff6eb91b --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_outlives_cell.ts @@ -0,0 +1,14 @@ +// -mm=own, phase 5 rejects: the closure borrows `a`, and is called after `a`'s block gave it back. +class C { + constructor(public x: number) {} +} + +function main() { + let f: () => number; + { + let a = new C(1); + f = () => a.x; + } + + print(f()); +} diff --git a/tslang/test/tester/own/own_err_closure_param_assigned.ts b/tslang/test/tester/own/own_err_closure_param_assigned.ts new file mode 100644 index 000000000..285ff2496 --- /dev/null +++ b/tslang/test/tester/own/own_err_closure_param_assigned.ts @@ -0,0 +1,16 @@ +// -mm=own, phase 5 rejects: `c` is captured, so its cell borrows the caller's value, and assigning +// it would free that value. +class C { + constructor(public x: number) {} +} + +function read(c: C) { + const g = () => c.x; + c = new C(2); + return g(); +} + +function main() { + const c = new C(1); + print(read(c), c.x); +} From 14d4ea1732f8ec0b1147d6c3f5bfd29bfb0fce93 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 09:53:36 +0100 Subject: [PATCH 2/4] -mm=own phase 5a: own_closure_borrow reads after the points a wrong free would hit Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/own/own_closure_borrow.ts | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tslang/test/tester/own/own_closure_borrow.ts b/tslang/test/tester/own/own_closure_borrow.ts index d7a37212f..e4269bdf9 100644 --- a/tslang/test/tester/own/own_closure_borrow.ts +++ b/tslang/test/tester/own/own_closure_borrow.ts @@ -31,6 +31,21 @@ function local(n: number) { return r + a.x; } +// the closure goes at the end of its block, before the frame's last reads: a box that destroyed +// what it borrows would leave `a` freed here +function innerBlock(n: number) { + let a = new C(n); + a.v.push(n); + let r = 0; + { + let f = () => a.x; + r = f(); + } + + churn(); + return r + a.x + a.v.length - 1; +} + // a copy of a `const` beside a cell: the box borrows it too function byValue(n: number) { const h = new C(n); @@ -135,10 +150,13 @@ function main() { const n = i % 10; const d = new D(new C(n)); const c = new C(n); - t += local(n) + byValue(n) + asArgument(n) + inLoop(n) + nested(n) + d.read() + param(c) + + t += innerBlock(n) + local(n) + byValue(n) + asArgument(n) + inLoop(n) + nested(n) + d.read() + param(c) + frameAssigns(n) + closureAssigns(n) + letHolds(n) + letAliases(n); + // the captured parameters' values are still the caller's + churn(); + t += c.x + d.c.x - 2 * n; } - assert(t == 1920000); + assert(t == 2100000); print("done."); } From cdd3f0bc43a60a6adae804f8c0d6a3519b7661a2 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 09:54:50 +0100 Subject: [PATCH 3/4] -mm=own phase 5a: spec section 16 Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) 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 8f05f5789..474603733 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 @@ -4,7 +4,8 @@ Date: 2026-09-24. Status: design approved in conversation; written review 2026-0 amendments folded in. Phase 0 merged as #399 (results §11); phase 1 (moves by reachability) merged as #401 (results §12); phase 2 (borrows for locals) merged as #402 (results §13); phase 3 (containers and unions) merged as #403 (results §14); phase 4 (function -signatures and `any`) on branch `own-phase-4` (results §15). Plans in `docs/superpowers/plans/`. +signatures and `any`) merged as #406-#410 (results §15); phase 5a (closures that do not escape) +on branch `own-phase-5` (results §16). Plans in `docs/superpowers/plans/`. ## 1. Purpose @@ -1007,3 +1008,97 @@ checks that the program is rejected against the rc library. - From §14.6: a read reached through an interface, borrow chains through assigned borrowers, moves after the last use of every borrower, and a temporary taken by a `let` and consumed elsewhere. + +## 16. Phase 5a results, 2026-10-02 + +Plan: `docs/superpowers/plans/2026-10-02-own-phase-5.md`. Changed: `OwnershipInferencePass.cpp`, +`OwnershipSignaturePass.cpp`, `OwnershipFacts.h`, and in the lowering the capture box's +descriptor and `ts.ReleaseCell`. MLIRGen does not change. + +### 16.1 What phase 5a accepts + +A closure is `ts.CreateBoundFunction(box, @f) {__owns_capture}`. Its box holds a cell for each +variable captured by reference and a copy of each `const` captured by value. The pass decides each +closure over at least one cell before anything else, because the verdict changes what the box's +stores are. + +- **Escape.** The closure value is followed through its views (a bound function and a hybrid one are + the same `{func, this, tag}`, so the cast between them is now a view) and through the locals that + hold it. It does not escape when every use is rc's bookkeeping, a call given it (not kept), or a + call through it (`ts.GetThis`/`ts.GetMethod`, or the devirtualized `ts.SymbolCallInternal(box)` of + a folded `const f`). Anything else is an escape: a return, a store into a field, element, global or + another box, a kept argument. An escape is `a closure that captures a variable and escapes here is + not supported by -mm=own yet` (5b). +- **A closure that does not escape borrows everything its box holds.** rc's `ts.RetainCell` of each + cell and `ts.Retain` of each copy go. The box's stores of copies are reads, not takers + (`__own_capture_borrow`, this pass's own mark). The closure carries `__own_borrows_captures`, and + its tag names a routine that frees the box and nothing else (`tsfrecb_`). +- **The closure's uses** are every call given it, called through it or through its box. None may run + after an end of what it borrows: the `ts.ReleaseCell`s of a cell this function owns, and the + releases and takers of a copy's owner (rootEnds of a fresh value or an owning local). A cell an + enclosing closure holds, and a parameter, end nowhere here. A copy of a read out of a container is + bounded by the read's own check: walkBorrowed adds the closure's uses to the read's. +- **A local that owns nothing** (`let f: () => number; f = () => ...`) is an alias of the closure. + rc's birth reference reads to the move logic as the store's, which erased the closure's only + release. Now the closure keeps its releases, and nothing may run it after them. +- **A captured parameter's cell** (`this` included) borrows the caller's value. Its `ts.ReleaseCell` + carries `__own_cell_borrows` and frees the cell only. Nothing may assign the variable: not the + frame, and not a closure body. The signature pass pins `__own_assigns_captures` on each closure: + the box fields whose cell its body assigns, followed through the closures it builds over the same + cell. The inference reads it, because a per-function pass may not look into another function. A + non-owning cell with a heap value that is not a parameter's is + `'x' is captured by a closure, but -mm=own cannot tell who owns its value here`. +- **Cells are places.** `ts.ReleaseSlot` of a cell, a captured `ts.Variable` in the frame or a cell + read out of a box in a closure body, is a drop of every chain whose root this function does not own. + A read of a cell is such a root. In the signature pass the closure-body form makes a function drop. +- **A call given a closure may run it.** A call whose operand is a closure value, a capture box, or a + closure's `this` drops every chain when its callee may drop: the box may hold anything. Before, a + call given only the box was not "given" what the box held (`derivedFrom` follows results, and a + store has none). + +### 16.2 Measured + +AOT, `measure.ps1`, in MB: `own_closure_borrow` (eleven shapes, 20000 rounds) reads gc 6.0, rc 4.3, +none 1769.5, own 4.3. + +### 16.3 Teeth + +- With the owning routine for a borrowing box, `own_closure_borrow` fails under AOT (0xC0000005): the + closure in `innerBlock` is a `let` released at its block's end, and its box then destroys the cell + the frame still reads. +- With `__own_cell_borrows` ignored, it fails under AOT (0xC0000374): the cell of `param`'s `c` and of + `D.read`'s `this` free the caller's objects, which `main` reads after a churn. +- Under the JIT both pass: LLVM folds the reads of freed memory. The AOT runs are the evidence. +- Each new negative compiles with its rule switched off. `own_err_closure_param_assigned` then + crashes (0xC0000374). + +### 16.4 Tests and the corpus + +- **Positive:** `own_closure_borrow` (a local, a copy beside a cell, an argument, a loop, nested, an + inner block, `this`, a parameter, the frame and the closure assigning the variable, a `let` holding + the closure, a `let` aliasing it), under JIT, AOT and the no-counting check. +- **Negatives:** `own_err_closure` (now: escapes), `own_err_closure_outlives_cell`, + `own_err_closure_copy_moved`, `own_err_closure_param_assigned`, `own_err_closure_assigns_param`, + `own_err_cell_assigned_in_frame`, `own_err_cell_assigned_in_closure`, + `own_err_closure_assigns_captured_field`. +- The capture box's descriptor has no retain routine under own, as no other descriptor does: it would + bring `__tslang_inc_ref` in. + +Corpus under `-mm=own --no-default-lib`: 310 of 591 before, 319 after, none lost. The nine new ones +(`00arrow_generic`, `00capture_in_new_arguments`, `00funcs_expression_generic`, +`00funcs_generic_arrow`, `00funcs_nesting_capture`, `00stack_test`, `22lambdas`, `241arrayforeach`, +`25lamdacapture`) pass under test-runner `-mm=own`, AOT and JIT. Of the 25 files the old error +stopped first: 9 compile, 4 escape (5b), 8 still meet a cell no closure claims (generators, which +are phase 7, and object literals whose methods capture), 4 stop at an earlier limit. + +### 16.5 Known limits (the input to phase 5b) + +- A closure over a cell that escapes (§2.5's second half): 5b. A closure over copies only already + owns them, as in phase 4, escaping or not. +- A copy of a borrowed value captured by a closure that does not escape works only when the borrow's + own check passes; a copy of a borrow into an escaping closure is an error. +- A captured local cannot be borrowed by a `let` or moved out of (slotLoadOf and borrowerOf exclude + captured variables). +- A non-escaping closure always borrows: one that could own its captures instead (called after the + cell's block ends, the variable not used again) is rejected. +- A call given any function value drops every chain when its callee may drop. From 4271d1330845f70118d093f7beea99d3c2b98543 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Fri, 2 Oct 2026 10:03:24 +0100 Subject: [PATCH 4/4] -mm=own phase 5a: a closure over copies aliased by a local keeps its release too Co-Authored-By: Claude Opus 5.5 --- .../2026-09-24-own-memory-model-design.md | 10 +-- .../lib/TypeScript/OwnershipInferencePass.cpp | 68 ++++++++++++------- tslang/test/tester/own/own_closure_borrow.ts | 14 +++- 3 files changed, 60 insertions(+), 32 deletions(-) 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 474603733..e82eab237 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 @@ -1040,7 +1040,8 @@ stores are. bounded by the read's own check: walkBorrowed adds the closure's uses to the read's. - **A local that owns nothing** (`let f: () => number; f = () => ...`) is an alias of the closure. rc's birth reference reads to the move logic as the store's, which erased the closure's only - release. Now the closure keeps its releases, and nothing may run it after them. + release. Now the closure keeps its releases, and nothing may run it after them. That holds for a + closure over copies only too, whose box otherwise owns its copies as in phase 4. - **A captured parameter's cell** (`this` included) borrows the caller's value. Its `ts.ReleaseCell` carries `__own_cell_borrows` and frees the cell only. Nothing may assign the variable: not the frame, and not a closure body. The signature pass pins `__own_assigns_captures` on each closure: @@ -1058,8 +1059,8 @@ stores are. ### 16.2 Measured -AOT, `measure.ps1`, in MB: `own_closure_borrow` (eleven shapes, 20000 rounds) reads gc 6.0, rc 4.3, -none 1769.5, own 4.3. +AOT, `measure.ps1`, in MB: `own_closure_borrow` (thirteen shapes, 20000 rounds) reads gc 6.1, rc 4.3, +none 1879.7, own 4.3. ### 16.3 Teeth @@ -1076,7 +1077,8 @@ none 1769.5, own 4.3. - **Positive:** `own_closure_borrow` (a local, a copy beside a cell, an argument, a loop, nested, an inner block, `this`, a parameter, the frame and the closure assigning the variable, a `let` holding - the closure, a `let` aliasing it), under JIT, AOT and the no-counting check. + the closure, a `let` aliasing it, a `let` aliasing a closure over copies), under JIT, AOT and the + no-counting check. - **Negatives:** `own_err_closure` (now: escapes), `own_err_closure_outlives_cell`, `own_err_closure_copy_moved`, `own_err_closure_param_assigned`, `own_err_closure_assigns_param`, `own_err_cell_assigned_in_frame`, `own_err_cell_assigned_in_closure`, diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 02829ef11..3dfa3b5bd 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -398,17 +398,23 @@ class OwnershipInferencePass : public mlir::PassWrapperescape = closure->escape ? closure->escape : user; } - if (!closure->hasCells) - { - return; - } - if (auto *escape = followClosure(boundOp.getResult(), closure->uses, closure->ends, closure->aliased); escape && !closure->escape) { closure->escape = escape; } + // copies only: the box owns them (phase 4); only an alias needs deciding + if (!closure->hasCells) + { + if (!closure->escape && closure->aliased) + { + closures.push_back(std::move(closure)); + } + + return; + } + for (auto &fill : closure->fills) { auto storeOp = fill.second; @@ -598,13 +604,42 @@ class OwnershipInferencePass : public mlir::PassWrapperop.getOperation(); auto name = ownerName(closure->op.getResult()); + auto ok = true; + // Held by a local that owns nothing: rc's birth reference reads to the move logic as the + // store's, which would leave the box with no owner. The closure keeps its releases, and + // nothing may run it after them. + auto keepAliasedReleases = [&]() { + for (auto *end : closure->ends) + { + if (!mlir::isa(end)) + { + continue; + } + + toErase.remove(end); + for (auto *use : closure->uses) + { + if (ok && reachableAfter(end, use, boundOp)) + { + reportClosureOutlives(use, end, name, name); + ok = false; + } + } + } + }; + + if (!closure->hasCells) + { + keepAliasedReleases(); + continue; + } + if (closure->escape) { reportClosureEscapes(closure->escape, boundOp); continue; } - auto ok = true; for (auto &fill : closure->fills) { auto value = fill.second.getValue(); @@ -675,28 +710,9 @@ class OwnershipInferencePass : public mlir::PassWrapperaliased) { - for (auto *end : closure->ends) - { - if (!mlir::isa(end)) - { - continue; - } - - toErase.remove(end); - for (auto *use : closure->uses) - { - if (ok && reachableAfter(end, use, boundOp)) - { - reportClosureOutlives(use, end, name, name); - ok = false; - } - } - } + keepAliasedReleases(); } // a call through the box, after the closure value that owns the box is given back diff --git a/tslang/test/tester/own/own_closure_borrow.ts b/tslang/test/tester/own/own_closure_borrow.ts index e4269bdf9..d0d89ebe9 100644 --- a/tslang/test/tester/own/own_closure_borrow.ts +++ b/tslang/test/tester/own/own_closure_borrow.ts @@ -144,6 +144,16 @@ function letAliases(n: number) { return f(); } +// a closure over copies only owns them, as before phase 5; aliased the same way, it keeps its +// release too +function copiesAliased(n: number) { + const h = new C(n); + let f: () => number; + f = () => h.x; + churn(); + return f(); +} + function main() { let t = 0; for (let i = 0; i < 20000; i++) { @@ -151,12 +161,12 @@ function main() { const d = new D(new C(n)); const c = new C(n); t += innerBlock(n) + local(n) + byValue(n) + asArgument(n) + inLoop(n) + nested(n) + d.read() + param(c) + - frameAssigns(n) + closureAssigns(n) + letHolds(n) + letAliases(n); + frameAssigns(n) + closureAssigns(n) + letHolds(n) + letAliases(n) + copiesAliased(n); // the captured parameters' values are still the caller's churn(); t += c.x + d.c.x - 2 * n; } - assert(t == 2100000); + assert(t == 2190000); print("done."); }