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
10 changes: 7 additions & 3 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,13 @@ a reference gives it back on every path out of a function.
(`s.values()` of a `Map` or a `Set`) a helper is bounded by the container too:
`for (const v of s.values().take(2))` works, but the result cannot be returned or kept past `s`.
An array's `values()`, `keys()`, `entries()` and `[Symbol.iterator]()` borrow it in the same
way. A closure that captures, given to two of them, is moved into the first. A `for...of` over a
generator, or a spread of one (`[...s.matchAll(re)]`), only borrows what each step yields: keeping
a yielded object or array (pushing it into another array) is an error for now.
way. A closure that captures, given to two of them, is moved into the first. What a generator
yields is the caller's: `for (const v of g()) a.push(v)` and `[...s.matchAll(re)]` take each value
over, as long as nothing reads it after it is pushed.
- Arguments given to a rest parameter (`a.concat(b)`) are borrowed for the call, so `b` is still
yours after it.
- `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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4849,3 +4849,61 @@ not a read for a keep: `'.a' borrows an element and cannot be stored`. It now sk
`isBorrow` already skips a `ts.DebugVariable` of a value. `own_for_of_nested_di` (debug build, JIT)
sums the lengths of the strings of a `string[][]`. DefaultLib: `array_concat` and `owning_copies`,
which failed under own in the debug pass only, compile and run.

## 48. What a generator yields is taken out of the record `next()` returns, 2026-10-27

`for (const c of g()) kept.push(c)` and `[...g()]` (a spread lowers to the same loop and push) were
`borrows a field and cannot be stored`: the loop's variable borrows the `value` field of the local
the record `next()` returns is kept in, which is released and given the next record every turn.

- **The take** (`fieldTakeOf`, `fieldTakeHolds`, checkPlaceRead). A read of a field of a local that
holds a record (a tuple), kept by one move - rc's retain and a push, a store into a place or a
global, a kept argument, or a store into an owning local declared without a value - is a move out
of the field when the taker runs once for each value read, nothing uses the borrow after it, and
nothing reads the field or the record before the local is released or given another value. The
walk is made again with the retain and the taker taken for uses, not keeps (`takenOps`); rc's
retain goes, and after the taker a `ts.Default` of the field's type is stored into the field, so
the local's release frees nothing of what moved. A value not taken (a push on some paths only)
stays in the field and goes with the record. Not a string, which is copied where it is kept.
- **The local** owns what it holds, or holds only results of calls that do not borrow: MLIRGen makes
no owner of a local at a script's top level or in a `try`, and nothing releases it there.
- **A local declared in a `try`** is declared without a value, where the cleanup can release it, and
given its value by a store right before rc's `ts.RetainSlot`. That store is now its declaration
(`slotDeclarationStore`, `declarationRetainOf`): the retained value is the stored one, the store
takes it as a declaration would (`takerAcquires`, `acquirePoint`), and a built array read
(`isBuiltArrayRead`) may be given that way. `try { const r = mk(); }` was `takes a second
reference`.
- **A global given its value once in the program's `main`** that nothing calls (a script's top
level) is a candidate immortal as one a static constructor gives it, and a class's `..new` (a
regular expression literal) makes its value as `ts.New` does. `const regexp = /a/g` at the top
level is immortal, so a call that may assign any global is no end of a borrow of it.
- **A built array stored into a global** (`const all = [...g()]` at the top level) moves in, as one
stored into a field (`isBuiltArrayReturned`).

`own_generator_take` keeps what generators yield by push and by spread, on all paths and on some, in
and out of a `try`, churning between steps, under own, rc, gc and none; own emits no counting.
Negative: `own_err_generator_take_used_after` reads the value after pushing it. DefaultLib:
`string_matchAll` and `string_match_lastIndex` compile and run under own.

## 49. The array a rest parameter is given borrows its elements, 2026-10-27

`a.concat(b)` moved `b` into the array `...other` is given (`b` was `used here after its value was
moved` after the call), and a callback's parameter given on that way (`(acc, cur) =>
acc.concat(cur)`) was `a parameter, kept here, but -mm=own cannot make the callers ... give it up`.
MLIRGen releases no such array under own, so the elements moved into it leaked with it.

`classifyArgumentArrays` takes a `ts.CreateArray` whose one use is an argument of a call that does
not keep it, and whose elements are each not fresh, or fresh and released by their owner: rc's
references for the elements go (`fieldBorrowRetains`), the array reads its elements without taking
them (`isBorrow`, `OWN_BORROWING_ARRAY_ATTR_NAME`), and a `ts.Release` marked
`OWN_SHALLOW_RELEASE_ATTR_NAME` after the call frees the array's data block and header and not the
elements (`emitReleaseArrayShallow`). Not after an invoke, a terminator.

`own_rest_array_borrows` gives locals, the same one twice, and a callback's parameter to rest
parameters and uses them after, churning between steps, under own, rc, gc and none; own emits no
counting. DefaultLib: `a.concat(b, c)` and then `b` compile under own.

Not here: `array_reduceRight` over an array of arrays, which may start from an element and return
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.
5 changes: 5 additions & 0 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
// retain nobody performed and free live memory.
#define OWNED_RESULT_ATTR_NAME "__owned_result"

// -mm=own only, set by OwnershipInferencePass for the lowering. On a `ts.Release` of an array: the
// array borrowed its elements (the array a rest parameter is given), so its release frees the
// array and not them.
#define OWN_SHALLOW_RELEASE_ATTR_NAME "__own_shallow_release"

// Marks an owned local that took its reference by consuming an OWNED_RESULT_ATTR_NAME value
// instead of by retaining. The slot still releases at every scope exit - that release is what
// gives the consumed reference back - so the pair is still balanced, but there is no
Expand Down
15 changes: 15 additions & 0 deletions tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,21 @@ class OwnershipRoutineLogic
FlatSymbolRefAttr::get(rewriter.getContext(), wrapperName), ValueRange{value});
}

// An array given back without what it holds (-mm=own, OWN_SHALLOW_RELEASE_ATTR_NAME): its data
// block and its header are freed, its elements are not, since it only borrowed them - the array a
// rest parameter is given. A null or static header is skipped as for any release.
void emitReleaseArrayShallow(mlir_ts::ArrayType arrayType, mlir::Value header)
{
TypeHelper th(rewriter);
ArrayLayout layout(op, rewriter, typeConverter, compileOptions);
emitIfLastReference(header, [&]() {
auto dataSlot = layout.dataAddress(arrayType, header);
auto dataValue = rewriter.create<LLVM::LoadOp>(op->getLoc(), th.getPtrType(), dataSlot);
emitFreeBlock(dataValue);
emitFreeBlock(header);
});
}

// What destroying a block of `storageType` at `payloadPtr` releases, without freeing the block:
// a promoted object's fields when its frame is done with it (ts.ReleaseContents). Each field
// given back is nulled, so that the block never holds a reference it no longer owns: a second
Expand Down
11 changes: 10 additions & 1 deletion tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,16 @@ class ReleaseOpLowering : public TsLlvmPattern<mlir_ts::ReleaseOp>
if (tsLlvmContext->compileOptions.tracksOwnership())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitReleaseValue(op.getReference().getType(), transformed.getReference());
// an array that only borrowed its elements (-mm=own's classifyArgumentArrays)
auto arrayType = dyn_cast<mlir_ts::ArrayType>(op.getReference().getType());
if (arrayType && op->hasAttr(OWN_SHALLOW_RELEASE_ATTR_NAME))
{
orl.emitReleaseArrayShallow(arrayType, transformed.getReference());
}
else
{
orl.emitReleaseValue(op.getReference().getType(), transformed.getReference());
}
}

rewriter.eraseOp(op);
Expand Down
7 changes: 7 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceBorrowers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ namespace own_inference
}

uses.push_back({user, pending.kills, isCall(user) && isCalleeOperand(use)});
// a move out of a record's field, its retain and its taker, which checkPlaceRead
// decides (spec 48): a use of the borrow until it is taken
if (takenOps.contains(user))
{
continue;
}

// copied into a borrowing box: whatever runs the closure reads it
if (auto found = captureStores.find(user); found != captureStores.end())
{
Expand Down
68 changes: 68 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceClosures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,74 @@ namespace own_inference
});
}

void OwnershipInferencePass::classifyArgumentArrays()
{
getFunction().walk([&](mlir_ts::CreateArrayOp arrayOp) {
// the array a rest parameter is given (`a.concat(b)`), made of values that are not its
// own to take, and given to one call that borrows it (spec 49)
auto result = arrayOp.getResult();
if (arrayOp->getNumOperands() == 0 || !result.hasOneUse())
{
return;
}

auto *call = *result.getUsers().begin();
if (!isCall(call) || call->hasTrait<mlir::OpTrait::IsTerminator>() || isKeptArgument(call, result) ||
!llvm::is_contained(callArgs(call), result))
{
return;
}

llvm::SmallVector<mlir::Operation *> retains;
for (auto element : arrayOp->getOperands())
{
if (holdsNoBlock(element))
{
continue;
}

// a fresh value nothing else releases would be the array's alone, which a release of
// the array alone leaks; a named one (`const a = new C()`) has its own release
auto root = rootOf(element);
auto releasedElsewhere = false;
forEachUse(root, [&](mlir::Operation *user, mlir::Value) {
releasedElsewhere = releasedElsewhere || mlir::isa<mlir_ts::ReleaseOp>(user);
});
auto *retain = isFresh(root) && !releasedElsewhere ? nullptr : retainBefore(element, arrayOp);
if (!retain)
{
return;
}

retains.push_back(retain);
}

// nothing borrowed (an array of numbers), or a result that borrows the array and outlives
// the call: the array stays the plain value it is
auto args = callArgs(call);
auto resultBorrowsArray = llvm::any_of(resultBorrowsAll(call), [&](int index) {
return static_cast<size_t>(index) < args.size() && args[index] == result;
});
if (retains.empty() || resultBorrowsArray || (call->getNumResults() == 1 && borrowingCall(call->getResult(0))))
{
return;
}

// rc's references for the elements go: the array borrows them, for as long as the call
setAttrTracked(arrayOp, OWN_BORROWING_ARRAY_ATTR_NAME);
fieldBorrowRetains.insert(retains.begin(), retains.end());

// and the array itself is given back after the call, without what it holds
if (!dryRun)
{
mlir::OpBuilder builder(call->getContext());
builder.setInsertionPointAfter(call);
auto releaseOp = builder.create<mlir_ts::ReleaseOp>(call->getLoc(), result);
releaseOp->setAttr(OWN_SHALLOW_RELEASE_ATTR_NAME, mlir::UnitAttr::get(&getContext()));
}
});
}

bool OwnershipInferencePass::isBorrowingCell(mlir::Value ref)
{
auto cellLoad = ref.getDefiningOp<mlir_ts::LoadOp>();
Expand Down
48 changes: 45 additions & 3 deletions tslang/lib/TypeScript/OwnershipInferenceMoves.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,37 @@ namespace own_inference
}

auto varOp = mlir::cast<mlir_ts::RetainSlotOp>(op).getSlot().getDefiningOp<mlir_ts::VariableOp>();
return varOp && varOp.getInitializer() ? rootOf(varOp.getInitializer()) : mlir::Value();
if (varOp && varOp.getInitializer())
{
return rootOf(varOp.getInitializer());
}

// a local declared in a `try` is declared without a value where the cleanup can release it,
// and given its value by a store right before rc's `ts.RetainSlot`
auto storeOp = varOp ? slotDeclarationStore(op) : mlir_ts::StoreOp();
return storeOp ? rootOf(storeOp.getValue()) : mlir::Value();
}

mlir::Operation *OwnershipInferencePass::declarationRetainOf(mlir::Operation *store)
{
auto *next = mlir::isa<mlir_ts::StoreOp>(store) ? store->getNextNode() : nullptr;
return mlir::isa_and_nonnull<mlir_ts::RetainSlotOp>(next) && slotDeclarationStore(next).getOperation() == store
? next
: nullptr;
}

mlir_ts::StoreOp OwnershipInferencePass::slotDeclarationStore(mlir::Operation *retainSlot)
{
auto slot = mlir::cast<mlir_ts::RetainSlotOp>(retainSlot).getSlot();
auto varOp = slot.getDefiningOp<mlir_ts::VariableOp>();
auto storeOp = mlir::dyn_cast_or_null<mlir_ts::StoreOp>(retainSlot->getPrevNode());
if (!varOp || varOp.getInitializer() || !isOwningVariable(varOp) || !storeOp || storeOp.getReference() != slot)
{
return {};
}

// the local's one acquisition: an assignment after it is rc's retain, release and store
return onlyRetainSlotOf(varOp) == retainSlot ? storeOp : mlir_ts::StoreOp();
}

bool OwnershipInferencePass::ownsHeap(mlir::Value value)
Expand Down Expand Up @@ -200,9 +230,13 @@ namespace own_inference
auto read = value.getDefiningOp<mlir_ts::LoadOp>();
auto builder = read ? read.getReference().getDefiningOp<mlir_ts::VariableOp>() : mlir_ts::VariableOp();
auto owner = mlir::cast<mlir_ts::RetainSlotOp>(retain).getSlot().getDefiningOp<mlir_ts::VariableOp>();
// the owner declared with the read, or (in a `try`) given it by the store before the retain
auto declarationStore = slotDeclarationStore(retain);
auto ownerGets = read && owner && (owner.getInitializer() == read.getResult() ||
(declarationStore && declarationStore.getValue() == read.getResult()));
if (!builder || isOwningVariable(builder) || builder.getCaptured().value_or(false) || !builder.getInitializer() ||
!mlir::isa_and_nonnull<mlir_ts::CreateArrayOp, mlir_ts::NewArrayOp>(builder.getInitializer().getDefiningOp()) ||
!owner || owner.getInitializer() != read.getResult() || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain)
!ownerGets || !read->hasOneUse() || onlyRetainSlotOf(owner) != retain)
{
return false;
}
Expand Down Expand Up @@ -243,8 +277,10 @@ namespace own_inference
auto resultSlot = storeOp && storeOp.getValue() == read.getResult()
? storeOp.getReference().getDefiningOp<mlir_ts::VariableOp>()
: mlir_ts::VariableOp();
// ...or a field, or a global (`const all = [...g()]` at a script's top level)
auto intoField = storeOp && storeOp.getValue() == read.getResult() &&
storeOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>();
(storeOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>() ||
storeOp.getReference().getDefiningOp<mlir_ts::AddressOfOp>());
if (!((resultSlot && isResultSlot(resultSlot)) || intoField) || returned)
{
return false;
Expand Down Expand Up @@ -366,6 +402,12 @@ namespace own_inference
return true;
}

// an array a rest parameter is given that borrows its elements (classifyArgumentArrays)
if (mlir::isa<mlir_ts::CreateArrayOp>(user) && user->hasAttr(OWN_BORROWING_ARRAY_ATTR_NAME))
{
return true;
}

// `assert(c, message)` prints the message when it fails, and keeps it either way
if (mlir::isa<mlir_ts::AssertOp>(user))
{
Expand Down
4 changes: 3 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferencePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ namespace own_inference
classifyBorrowingFields();
// ...and whether an object literal borrows what its fields hold (spec 32)
classifyRecords();
// ...and whether an array given to a rest parameter takes its elements (spec 49)
classifyArgumentArrays();
// a kept parameter the body may not move on every path is released at its exits (spec 33)
releaseKeptParamsAtExits();
auto exitReleasesGone = llvm::make_scope_exit([&]() {
Expand Down Expand Up @@ -502,7 +504,7 @@ namespace own_inference
OWN_FRESH_RESULT_ATTR_NAME,
OWN_ASSIGNS_CAPTURES_ATTR_NAME,
OWN_RESULT_BOUNDED_ATTR_NAME, OWN_STATE_BORROW_ATTR_NAME,
OWN_CAPTURE_BORROW_ATTR_NAME})
OWN_CAPTURE_BORROW_ATTR_NAME, OWN_BORROWING_ARRAY_ATTR_NAME})
{
op->removeAttr(name);
}
Expand Down
Loading
Loading