Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,8 +746,11 @@ a reference gives it back on every path out of a function.
elements with `[a[i], a[j]] = [a[j], a[i]]`, which moves nothing; swap two elements or two fields
the same way in your own code. A swap through a local (`let t = a[i]; a[i] = a[j]; a[j] = t`)
gives an object in `t` a second owner, which is an error. A callback your function only calls,
tests or hands on to another function (as `sort` hands its comparison to a helper) is known by
the functions its callers pass; one stored, captured or returned is not. `copyWithin` works on
tests (`compare && compare(a, b)`), hands on to another function (as `sort` hands its comparison
to a helper), names again (`const g = f`) or captures in a closure that calls it (`a.forEach((v)
=> f(v))`) is known by the functions its callers pass; one stored in a field, returned, or
captured in a closure that is itself kept in a local is not. A callback may assign the numbers
and strings it captures (`total += v.n`). `copyWithin` works on
numbers and strings (a string is copied where it moves); over class instances it would leave one
object in two elements, an error by design.
- Iterator helpers work, the lazy ones (`map`, `filter`, `take`, `drop`) taking their source over
Expand All @@ -763,8 +766,9 @@ a reference gives it back on every path out of a function.
- `reduce` and `reduceRight` over an array of arrays or objects are errors: with no initial value
they start from an element, and may return it as it is, a second owner of it.
- The `Array<T>` class holds values, strings or `Shared<T>` handles, as `Map` does: `new
Array<number>()` works, and `new Array<Node>()` is a compile error that points at `Shared<T>`.
`Array<string>` and `Array<Shared<T>>` do not compile yet.
Array<number>()`, `new Array<string>()` and `new Array<Shared<Node>>()` work, and `new
Array<Node>()` is a compile error that points at `Shared<T>`. A string stored again and again
(`fill`) is copied each time.
- A parameter with a default (`constructor(private items: T[] = [])`) is kept like any other
parameter, whether the caller passes it or not.
- `Shared<T>` cycles are never freed.
Expand Down
102 changes: 102 additions & 0 deletions tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -4907,3 +4907,105 @@ Not here: `array_reduceRight` over an array of arrays, which may start from an e
it as it is, a second owner, is marked "not own". `Array<string>` (`fill` keeps a defaulted string
parameter in a loop) and `Array<Shared<T>>` (the class's methods hand callbacks on through a vtable,
whose families stay open) still do not compile.

## 50. Strings copied where a store runs again, or where nothing retains them, 2026-10-27

`Array<string>` (the class) did not compile: `fill` stores its `value` parameter into each element in
a loop. A store into a place kept the parameter (`keeps`), the callers moved it in, and the second
store was `'value' is moved inside a loop but was made outside it`. And a generator that yields on
some paths only (`for (const v of this) if (func(v)) yield v` in `filter2`) reads `v` out of its
state's field into the record `next()` returns; the field is read for the test too, so it is no
field that only yields (§48), and the store into the record was `borrows a field and cannot be
stored`.

- **A store that may run again** (`mayRunAgain`: in a loop) of a string parameter is no keep: the
parameter stays a borrow, and each store takes a copy, as `copyForRetain` makes of rc's retain
for it.
- **A store into a place rc retains nothing for** (a field of the record a yield builds) of a
string or a handle read out of a place (`storesCopyOf`): the place takes a `ts.StringCopy`, or a
handle a count of its own (`ts.Retain`), made in the dry run's `callCopies` list.
- **A handle given to a call that may release it** (a callback, through `copiesInto`): the call gets
a count of its own, `ts.Retain` right before and `ts.Release` right after, unless it keeps it.
`isCountedHandleReceiver` takes such a call, and `handleReadUses` the release after it, for a
counted use of the read (§23.3).

`own_string_copies` fills arrays with a string parameter, directly and through a class, and keeps
the strings a generator yields on some paths, churning between steps, under own, rc, gc and none.

## 51. A walk over handles drops nothing, 2026-10-27

`Array<Shared<T>>` did not compile: every method that hands `this.data` to a function that walks it
(`join`, `every`, `some`, `find`, ...) was `borrows a field but is used here after it may be
released or overwritten`. A `for (const v of this)` over handles makes `v` a counted copy, and its
`ts.ReleaseSlot` touches a handle: `dropsInBody` took the walk for one that may destroy the last
reference to a payload, and with it anything.

- **A counted read local** (`isCountedReadSlot`): declared from a read (`ts.Load`), retained by rc
(`ts.RetainSlot`), never assigned. Where the read came from holds a count of its own while the
local lives - a store into it, a pop, a call that does either is a drop of its own - so the
local's release is never the last. Neither `dropsInBody` nor `fieldDropsInBody` counts it.
- **`ts.ArraySplice`'s items** are counted receivers of a handle read (`isCountedHandleReceiver`), as
`push`'s and `unshift`'s are.

`own_shared_counted_reads` walks an array of handles a class holds, by a function given a callback,
by a `for...of`, by a generator that yields some, and splices one in, churning between steps, under
own, rc, gc and none. DefaultLib: `array_class_shared` and `array_class_string` compile and run under
own.

## 52. A callback that assigns a captured number or string, 2026-10-27

`names.forEach((v) => { each += v; })`, with `forEach` handing the array a field holds on, was
`borrows a field but is used here after it may be released or overwritten`: an assignment through a
captured variable's cell, in the closure's body, was a drop of anything (`fieldDropsInBody`), and a
call given a closure was judged by none of its callees' place drops.

- **A cell that owns no block** (`t += v.n`, a number): its `ts.ReleaseSlot` destroys nothing
(`releasesNothing`). Not a drop in `dropsInBody`, `fieldDropsInBody`, or the inference pass's.
- **A cell that holds a string** (`each += v`): its assignment frees that string only, and a string
owns nothing else. A place drop of its own (`OWN_CAPTURED_STRING_POSITION`): it drops a borrow that
may be a string, and no other - not an array, a class instance, a tuple, an object or an interface.
- **A call given a closure it does not keep** is judged by its callees' place drops too: the closure
is a member of the callback parameter's family, whose bodies those drops count (§33, §43). A call
that keeps the closure is not.

`own_captured_string_callback` builds strings and counts through callbacks a class's methods give
on with the array a field holds, churning between steps, under own, rc, gc and none; own emits no
counting. Negative: `own_err_captured_string_drops`, a string read out of a field and borrowed
across a call that runs a callback assigning a captured string.

## 53. `f && f(x)` of an optional function is `undefined | T`, 2026-10-27

`compare && compare(a, b)` with `compare?: (x, y) => number` failed in every model: the `&&` was the
union of the right side and the left one's type without its optional (`invalid cast from (p0, p1) =>
s32 to s32`), and under own a `boolean | function` union holds the callback (§43). A function is
never falsy: an optional one is false only when it is not there, so the false branch is `undefined`,
as in TypeScript (`mlirGenAndOrLogic`).

`own_callback_and_call` sorts class instances through `compare && compare(a[j], a[j + 1])`, with a
comparison given and with none, churning between steps, under own, rc, gc and none; own emits no
counting.

## 54. A callback parameter captured by a closure that calls it, 2026-10-27

`a.forEach((v) => f(v))` captured `f`, which kept its family open (§43): a call through it was one
that may destroy anything, and so was every call that gave the closure on.

- **The parameter's cell** (`onlyCalledParam`): a captured parameter has a cell declared from it,
never assigned. Each of its uses is a read that is only called or given on (as a slot's is), the
cell's own `ts.RetainCell` and `ts.ReleaseCell`, or a store of it into a field of a closure's
capture box. The closure (`closureFunctionOf`) is bound only to boxes holding that cell in that
field, or called by name with one (the built-in `forEach`, inlined, calls it so), and its body
reads the field only to call what the cell holds, or give it on.
- **The calls in the closure** (`resolveCallback`): a read of the cell out of the closure's box
(`ts.Load(ts.Load(ts.PropertyRef(box, i)))`) is the parameter's (`capturedCallbacks`), and so is a
read of the cell where it is declared. Both resolve through the parameter's family.
- With §52, `const g = f; g(v)` resolves as well: the local is a view of the parameter's slot.

`own_callback_captured` sums, finds the largest and walks through callbacks given to a closure that
calls them and to a local that names them, from a class's methods over the array a field holds,
churning between steps, under own, rc, gc and none; own emits no counting. Negative:
`own_err_callback_captured_drops`, whose callback replaces a global array that the walk's element
may be part of.

Not here: a closure held in a local and called (`const call = (v) => f(v); call(v)`), and a
callback stored into a field or returned, keep their families open.
6 changes: 5 additions & 1 deletion tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -5713,7 +5713,11 @@ class MLIRGenImpl
resultFalse = leftExpressionValue;
if (auto optType = dyn_cast<mlir_ts::OptionalType>(resultFalse.getType()))
{
resultFalse = builder.create<mlir_ts::CastOp>(location, optType.getElementType(), resultFalse);
// a function is never falsy: an optional one is false only when it is not there, so
// `compare && compare(a, b)` is `undefined | number`, as in TypeScript
resultFalse = mth.isAnyFunctionType(optType.getElementType())
? builder.create<mlir_ts::UndefOp>(location, getUndefinedType()).getResult()
: builder.create<mlir_ts::CastOp>(location, optType.getElementType(), resultFalse).getResult();
}
}
else
Expand Down
11 changes: 11 additions & 0 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ namespace mlir_ts = mlir::typescript;
// The position a place reached through an interface is given: its position in the object is not
// known (spec 28)
constexpr int64_t OWN_INTERFACE_FIELD_POSITION = -3;
// The position of a captured variable's cell holding a string, assigned: the old string goes, and a
// string owns nothing else, so only a borrow that is a string may be it (spec 52)
constexpr int64_t OWN_CAPTURED_STRING_POSITION = -4;
// On a function whose body relies on a fact its callers cannot all know (a parameter it keeps, a
// result that borrows): why, for the error the body gets instead.
#define OWN_FACTS_LOST_ATTR_NAME "__own_facts_lost"
Expand Down Expand Up @@ -956,6 +959,14 @@ inline bool ownsHeap(mlir::Value value)
return ownsHeap(value.getLoc(), value.getType());
}

// The release of a slot whose type owns no block - a captured number assigned (`t += v.n` in a
// callback) - destroys nothing (spec 52)
inline bool releasesNothing(mlir_ts::ReleaseSlotOp releaseSlotOp)
{
auto refType = mlir::dyn_cast<mlir_ts::RefType>(releaseSlotOp.getSlot().getType());
return refType && !ownsHeap(releaseSlotOp.getLoc(), refType.getElementType());
}

// Holds no block: a number, `null` or `undefined` widened, an empty optional, a string literal.
// Also a view of a value whose type owns none: a union's number payload made into another union
// (`GetValueFromUnion`, then `CreateUnionInstance`) is a number all the way. A `value_ref` is not
Expand Down
31 changes: 30 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ namespace own_inference
// an overwrite of a field or an element, or an assignment to a captured variable or
// a global (spec 29: the global may hold what `this` or a parameter is)
auto slot = releaseSlotOp.getSlot();
if (isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot) || slot.getDefiningOp<mlir_ts::AddressOfOp>())
if (!releasesNothing(releaseSlotOp) &&
(isPlace(slot) || isCellVariable(slot) || isLoadedCell(slot) || slot.getDefiningOp<mlir_ts::AddressOfOp>()))
{
drops.push_back(op);
}
Expand Down Expand Up @@ -562,13 +563,41 @@ namespace own_inference
continue;
}

// the place owns the copy, or a count of a handle's of its own (spec 23.2)
if (auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(call))
{
mlir::OpBuilder builder(storeOp);
if (isHandle(value))
{
builder.create<mlir_ts::RetainOp>(storeOp->getLoc(), value);
continue;
}

auto copy = builder.create<mlir_ts::StringCopyOp>(storeOp->getLoc(), value.getType(), value);
storeOp.getValueMutable().assign(copy);
continue;
}

// a callee that keeps it takes the copy; else it is released after the call
auto args = callArgs(call);
auto kept = llvm::any_of(ownedParams(call), [&](int32_t index) {
return static_cast<size_t>(index) < args.size() && args[index] == value;
});

mlir::OpBuilder builder(call);
// a handle: a count of its own for the call, given back after it (spec 50)
if (isHandle(value))
{
builder.create<mlir_ts::RetainOp>(call->getLoc(), value);
if (!kept)
{
builder.setInsertionPointAfter(call);
builder.create<mlir_ts::ReleaseOp>(call->getLoc(), value);
}

continue;
}

auto copy = builder.create<mlir_ts::StringCopyOp>(call->getLoc(), value.getType(), value);
for (auto &operand : call->getOpOperands())
{
Expand Down
6 changes: 5 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferencePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ class OwnershipInferencePass
// rc's references for boxes, which the closures decide
llvm::DenseSet<mlir::Operation *> captureRetains;
// A string read out of a place, given to a call that may release it (the dry run's): the call
// gets a copy instead, released after it (spec 22), read -> call
// gets a copy instead, released after it (spec 22), read -> call; or stored into a place, which
// owns the copy (spec 50), read -> store
llvm::SmallVector<std::pair<mlir::Operation *, mlir::Operation *>> callCopies;
// A move out of a field of an owning record local, being checked (spec 48): its retain and its
// taker, which walkBorrowed takes for uses of the borrow and not for keeps
Expand Down Expand Up @@ -827,6 +828,9 @@ class OwnershipInferencePass
bool fieldTakeHolds(mlir::Operation *read, const FieldTake &take, llvm::ArrayRef<BorrowUse> uses);
// Can `call` be given a copy of `value`, a string read out of a place, in its place (callCopies)?
static bool copiesInto(mlir::Operation *call, mlir::Value value);
// Does `store` put `value`, a string read out of a place, into another place with no retain of
// rc's for it, so that the place can take a copy instead (callCopies)?
bool storesCopyOf(mlir::Operation *store, mlir::Value value);

// A local that holds a borrow through `user`, and the field path under it that holds it (empty:
// the local's value itself): a store of it into the local, or into a field of the local's value
Expand Down
Loading
Loading