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
40 changes: 34 additions & 6 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,35 @@ function main() {
second
```

`@keeps` also goes on a setter that stores its value (`@keeps("n") set rebase(n: Node)`).
`@keeps` also goes on a setter that stores its value (`@keeps("n") set rebase(n: Node)`), and on a
generator: its state then owns the arguments it holds, instead of borrowing them (see
[`@borrows`](#borrows)), so the generator can be stored and returned. The state owns every argument
it holds, so `@keeps` names each of them:

```typescript
@keeps("iter", "f")
function* mapped<T extends { next(): { value: any, done: boolean } }>(iter: T, f: (x: number) => number) {
for (const v of iter) yield f(v);
}

function* count(n: number) {
for (let i = 0; i < n; i++) yield i;
}

function make() {
return mapped(count(3), (x) => x * 10); // count(3) moves into the state
}

function main() {
let t = 0;
for (const v of make()) t += v;
print(t);
}
```

```text
30
```

### `@lends`

Expand Down Expand Up @@ -705,11 +733,11 @@ a reference gives it back on every path out of a function.
- Array methods that keep elements of the source in a new array (`filter` over class instances)
give each such element a second owner, which is an error by design. Use `Shared<T>` elements,
or build the result from copies.
- Iterator helpers that walk their source (`forEach`, `every`, `some`, `find`, `toArray`) work.
The lazy ones (`map`, `filter`, `take`, `drop`) give an iterator that borrows its source, so
`const seq = fibonacci().map(f)` is an error: use it in place (`for (const v of
fibonacci().map(f))`), or, in a function, name the source first (`const src = fibonacci();
const seq = src.map(f);`). A module-level `const` is a global, and a global cannot hold a borrow.
- Iterator helpers work, the lazy ones (`map`, `filter`, `take`, `drop`) taking their source over
as in JavaScript: `const seq = fibonacci().map(f)`. An iterator that borrows its container
(`s.values()` of a `Map` or a `Set`) cannot be taken over by one, so `s.values().take(2)` is an
error: walk it with `for...of` instead. A closure that captures, given to two of them, is moved
into the first.
- `Shared<T>` cycles are never freed.
- There are no lifetime annotations: a borrow that the compiler cannot follow is an error, even
when the program would be correct. The fix is usually a move, a fresh value or `Shared<T>`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4535,4 +4535,47 @@ under own: `iterator_every`, `iterator_find`, `iterator_forEach`, `iterator_some
Not here: the lazy helpers `map`, `filter`, `take` (generators whose state borrows the source) and
`drop` (which returns it) still give a result that borrows a temporary, so `const seq =
fibonacci().map(f)` is an error; that needs a generator state that owns a parameter. `reduce` with a
string accumulator starts its `let` from the `initial` parameter.
string accumulator starts its `let` from the `initial` parameter.
## 38. A generator that keeps its arguments (`@keeps` on a generator), 2026-10-27

A generator over a parameter that holds a block borrows it (§19): the caller owns the state, which may
not be stored, returned or captured past the argument. The default library's lazy iterator helpers
(`map`, `filter`, `take`, `drop`) are generators over their source, so `const seq =
fibonacci().map(f)` was "borrows its generator's arguments and cannot be stored" - in JavaScript a
helper takes its source over. `@keeps("p")` on a generator now says that: the state owns `p`.

- **The signature pass.** `findBorrowingStates` still takes a `@keeps` maker's state, marked `owns`:
no bound (`__own_result_bounded`), nothing in `borrowsFound`, and in `ts.own_borrowing_fields` the
box's position is written as -1, so the release routine frees the box like any escaping closure's -
its cells and what they hold - while the fields that hold only what is read out of it (a `for...of`
cursor over `iter`, §37) still borrow, from the box, which the state frees with it.
`localOwnedParams` counts, for a function that says `@keeps`, each parameter owning a block whose
cell the maker stores into the state's `.captured` box (`paramsCapturedIntoState`), so the callers
move the arguments in and `checkDeclared` holds the body to the decorator. A box owns every cell it
holds: `@keeps` on `map` has to name the callback too ("names 'iter', but its body keeps 'iter',
'f'"), and a capturing closure given to two helpers is used after its move (a plain function
holds no block, and is not moved).
- **The inference.** `decideEscaping` lets the cell of a parameter the function keeps move into an
escaping box, as an owning local's does (it was "a closure that escapes owns what it captures, but
-mm=own cannot move this variable into it: its value is the caller's").
- **The callers.** An argument a callee keeps moves in only when it is fresh or this function's to
give. Two shapes the helpers meet were neither: `isFresh` now takes a `ts.CallIndirect` of a
`ts.SymbolRef` to a function defined here (a generator called at a script's top level, an
extension call such as `g().take(2)`), as it took a direct call; and `holdsNoBlock` a plain function
cast to a hybrid one (`f(double)`, `f((x) => x * 2)`), whose `this` is null.

`own_generator_keeps` runs a class argument, chained iterator generators, a lambda and a named
callback, a generator returned past its argument's expression, one given up after a step, and a
module-level one, churning between steps, under own, rc, gc and none; own emits no counting. AOT under
own, 200000 iterations peak at 4.4 MB, as 1000 do (4.3 MB). Negatives: `own_err_generator_keeps_missing`
(the callback left out), `own_err_generator_keeps_used_after` (the argument read after it moved),
`own_err_generator_keeps_bounded` (a generator that borrows its argument given to one that keeps it).

DefaultLib under own: `iterator_map`, `iterator_filter` and `iterator_toArray` pass with `@keeps` on
`map`, `filter` and `take`, and `drop` made a generator.

Not here: a generator that borrows its container given to a keeper (`s.values().take(2)`, and
`new Set([1, 2, 3]).values().take(2)` in `iterator_take` and `iterator_drop`), whether the container is
a temporary or a named local: the keeper would outlive the borrow. A `for...of` at a script's top level over a
module-level generator whose `next` calls a callback: the call may assign the global, so the loop's
borrow of it ends. `reduce` with a string accumulator, as before.
17 changes: 17 additions & 0 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,16 @@ inline bool isFresh(mlir::Value value)
return callee && !callee.isDeclaration();
}

// the same call made through the function's symbol: a generator called at the top level of a
// script, an extension call (`g().take(2)`)
if (auto callIndirectOp = mlir::dyn_cast<mlir_ts::CallIndirectOp>(def))
{
auto symbolRefOp = callIndirectOp.getCallee().getDefiningOp<mlir_ts::SymbolRefOp>();
auto callee = symbolRefOp ? mlir::SymbolTable::lookupNearestSymbolFrom<mlir_ts::FuncOp>(def, symbolRefOp.getIdentifierAttr())
: mlir_ts::FuncOp();
return callee && !callee.isDeclaration();
}

if (mlir::isa<mlir_ts::CallOp, mlir_ts::CallIndirectOp, mlir_ts::CallInternalOp, mlir_ts::CallHybridInternalOp,
mlir_ts::InvokeOp, mlir_ts::InvokeHybridOp>(def))
{
Expand Down Expand Up @@ -949,6 +959,13 @@ inline bool holdsNoBlock(mlir::Value value)
return true;
}

// a function made a hybrid one (`f(double)`, `f((x) => x * 2)`): its `this` is null
if (auto castOp = mlir::dyn_cast<mlir_ts::CastOp>(def);
castOp && castOp.getIn().getDefiningOp<mlir_ts::SymbolRefOp>() && mlir::isa<mlir_ts::FunctionType>(castOp.getIn().getType()))
{
return true;
}

if (!isView(def))
{
return false;
Expand Down
9 changes: 8 additions & 1 deletion tslang/lib/TypeScript/OwnershipInferenceClosures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1041,8 +1041,15 @@ namespace own_inference

auto name = varName(varOp);
auto elementType = mlir::cast<mlir_ts::RefType>(value.getType()).getElementType();
// the cell of a parameter this function keeps: its callers gave the value up, so it moves
// in like an owning local's (a `@keeps` generator's state, spec 38)
auto init = varOp.getInitializer();
auto argument = init ? mlir::dyn_cast<mlir::BlockArgument>(init) : mlir::BlockArgument();
auto keptParam = argument && argument.getOwner()->isEntryBlock() &&
argument.getOwner()->getParentOp() == getFunction().getOperation() &&
llvm::is_contained(ownedParams(getFunction()), static_cast<int32_t>(argument.getArgNumber()));
// a handle's cell holds a count of its own, whoever's the value was (spec 23.2)
if (!isOwningVariable(varOp) && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) &&
if (!isOwningVariable(varOp) && !keptParam && !closure.borrowsValues && ownsHeap(varOp.getLoc(), elementType) &&
!MLIRTypeHelper::isSharedHandleType(elementType))
{
reportEscapingCellNotOwned(move, closure.escape, "its value is the caller's");
Expand Down
69 changes: 67 additions & 2 deletions tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,53 @@ namespace own_signature
return true;
}

llvm::SmallVector<int32_t> OwnershipSignaturePass::paramsCapturedIntoState(mlir_ts::FuncOp maker)
{
llvm::SmallVector<int32_t> captured;
maker.walk([&](mlir_ts::NewOp newOp) {
auto valueRefType = mlir::dyn_cast<mlir_ts::ValueRefType>(newOp.getType());
auto tupleType = valueRefType ? mlir::dyn_cast<mlir_ts::TupleType>(valueRefType.getElementType())
: mlir_ts::TupleType();
auto position = tupleType ? fieldIndex(tupleType, CAPTURED_NAME) : -1;
auto box = position >= 0 ? boxStoredIntoState(newOp, position) : mlir::Value();
if (!box)
{
return;
}

for (auto *user : box.getUsers())
{
auto propertyRefOp = mlir::dyn_cast<mlir_ts::PropertyRefOp>(user);
if (!propertyRefOp)
{
continue;
}

for (auto *fieldUser : propertyRefOp->getUsers())
{
auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(fieldUser);
auto cellOp = storeOp ? storeOp.getValue().getDefiningOp<mlir_ts::VariableOp>() : mlir_ts::VariableOp();
auto index = -1;
if (!cellOp || !isParameterCell(cellOp, maker, index))
{
continue;
}

// a handle's cell holds a count of its own (spec 23.2): its caller gives nothing up
auto elementType = mlir::cast<mlir_ts::RefType>(cellOp.getType()).getElementType();
if (own_facts::ownsHeap(cellOp.getLoc(), elementType) && !MLIRTypeHelper::isSharedHandleType(elementType) &&
!llvm::is_contained(captured, index))
{
captured.push_back(index);
}
}
}
});

llvm::sort(captured);
return captured;
}

bool OwnershipSignaturePass::readsParameterSlot(mlir::Value value, mlir_ts::FuncOp maker, int32_t &index)
{
auto loadOp = rootOf(value).getDefiningOp<mlir_ts::LoadOp>();
Expand Down Expand Up @@ -609,6 +656,15 @@ namespace own_signature
}

BorrowingState state{maker, captured};
// a maker that says `@keeps`: its callers move the arguments in, and the box owns them
// like any escaping closure's; the result is bounded by nothing (spec 38)
if (maker->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME))
{
state.owns = true;
states[tupleType] = state;
return;
}

for (auto *user : box.getUsers())
{
auto propertyRefOp = mlir::dyn_cast<mlir_ts::PropertyRefOp>(user);
Expand Down Expand Up @@ -692,6 +748,11 @@ namespace own_signature
states.remove_if([&](auto &entry) { return made[entry.first] != 1; });
for (auto &[type, state] : states)
{
if (state.owns)
{
continue;
}

auto bounded = state.bounded;
llvm::sort(bounded);
borrowsFound[state.maker] = bounded;
Expand Down Expand Up @@ -747,11 +808,15 @@ namespace own_signature
llvm::SmallVector<mlir::Attribute> entries;
for (auto &[type, state] : states)
{
llvm::SmallVector<int32_t> fields{state.captured};
// the box first, -1 for one that owns: its readers compare the box's position with it
llvm::SmallVector<int32_t> fields{state.owns ? -1 : state.captured};
fields.append(state.fields.begin(), state.fields.end());
entries.push_back(mlir::ArrayAttr::get(
context, {mlir::TypeAttr::get(type), mlir::DenseI32ArrayAttr::get(context, fields)}));
state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded));
if (!state.owns)
{
state.maker->setAttr(OWN_RESULT_BOUNDED_ATTR_NAME, mlir::DenseI32ArrayAttr::get(context, state.bounded));
}
}

module->setAttr(OWN_BORROWING_FIELDS_ATTR_NAME, mlir::ArrayAttr::get(context, entries));
Expand Down
13 changes: 13 additions & 0 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,19 @@ namespace own_signature
}
}

// a generator whose state owns what it holds of its arguments, as its `@keeps` says: the
// callers move them in, and the state's release frees them (spec 38)
if (funcOp->hasAttr(OWN_DECLARED_KEEPS_ATTR_NAME))
{
for (auto index : paramsCapturedIntoState(funcOp))
{
if (!llvm::is_contained(owned, index))
{
owned.insert(llvm::upper_bound(owned, index), index);
}
}
}

return owned;
}

Expand Down
10 changes: 9 additions & 1 deletion tslang/lib/TypeScript/OwnershipSignaturePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -936,9 +936,14 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
static bool keeps(mlir::Operation *user, mlir::Value used);

// The parameters the body keeps (spec 2.4, owned-by-callee): a read of the parameter's slot -
// never assigned - that something keeps.
// never assigned - that something keeps. A generator that says `@keeps` keeps too each parameter
// whose cell its state's box holds (paramsCapturedIntoState, spec 38).
static llvm::SmallVector<int32_t> localOwnedParams(mlir_ts::FuncOp funcOp);

// The parameters, owning a block, whose cell `maker` stores into the `.captured` box of the state
// object it makes: what a generator's state holds of its arguments (spec 38).
static llvm::SmallVector<int32_t> paramsCapturedIntoState(mlir_ts::FuncOp maker);

// ---- The facts callers must know ----

// Owned parameters and a result that borrows only hold where every caller knows them, so the
Expand Down Expand Up @@ -978,6 +983,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper<OwnershipSignaturePass,
llvm::SetVector<int32_t> fields;
// the stores of `this` into the box, which keep nothing once the state borrows
llvm::SmallVector<mlir::Operation *> thisStores;
// a `@keeps` maker's (spec 38): the box owns its arguments, and only the fields that hold
// what is read out of it borrow - from the box, which the state frees last
bool owns = false;
};

static int32_t fieldIndex(mlir_ts::TupleType tupleType, llvm::StringRef name);
Expand Down
14 changes: 12 additions & 2 deletions tslang/test/tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3937,7 +3937,7 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o
own_call_field_drops own_immortal_keepers
own_generator_method_this own_string_copy_call own_param_kept_some_paths own_method_callbacks
own_loop_read_again own_length_drops_elements own_tuple_param_kept
own_for_of_borrowed_iterator)
own_for_of_borrowed_iterator own_generator_keeps)
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.
Expand All @@ -3964,6 +3964,13 @@ foreach(for_of_mm rc gc none)
tslang_add_test(NAME test-compile-${for_of_mm}-own_for_of_borrowed_iterator COMMAND test-runner -mm=${for_of_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_for_of_borrowed_iterator.ts")
endforeach()

# A generator whose `@keeps` makes its state own its arguments (spec 38); the same program under every
# other model.
foreach(keeps_mm rc gc none)
tslang_add_test(NAME test-jit-${keeps_mm}-own_generator_keeps COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_generator_keeps.ts")
tslang_add_test(NAME test-compile-${keeps_mm}-own_generator_keeps COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/own_generator_keeps.ts")
endforeach()

# A value read out of an assigned global, lent to a call that assigns only other globals, outlives the
# call; own refuses a call that may assign that global or one it cannot name.
foreach(other_global_mm own rc gc none)
Expand Down Expand Up @@ -4628,7 +4635,10 @@ set(own_error_cases
"own_err_shared_elements_object|:13:21: error: -mm=own: 'Bag' cannot hold 'Node' [(]its 'T'[)]: an element has to be a value, a string or a Shared<T> handle. use 'Shared<Node>'"
"own_err_shared_elements_any|:8:21: error: -mm=own: 'Bag' cannot hold 'any' [(]its 'T'[)]: an element has to be a value, a string or a Shared<T> handle. give it one of those types"
"own_err_shared_elements_inferred|:12:17: error: -mm=own: 'Bag' cannot hold 'Node' [(]its 'T'[)]"
"own_err_keeper_popped|:14:9: error: argument '[^']*' is given to 'Registry[.]register', which keeps it, but -mm=own cannot move it here")
"own_err_keeper_popped|:14:9: error: argument '[^']*' is given to 'Registry[.]register', which keeps it, but -mm=own cannot move it here"
"own_err_generator_keeps_missing|:9:1: error: '@keeps' of 'mapped<[^']*' names 'iter', but its body keeps 'iter', 'f'"
"own_err_generator_keeps_used_after|:15:11: error: '[^']*' is used here after its value was moved"
"own_err_generator_keeps_bounded|:16:21: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured")
foreach(own_error_case ${own_error_cases})
string(FIND "${own_error_case}" "|" own_error_sep)
string(SUBSTRING "${own_error_case}" 0 ${own_error_sep} own_error_name)
Expand Down
17 changes: 17 additions & 0 deletions tslang/test/tester/own/own_err_generator_keeps_bounded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// -mm=own rejects: a generator that borrows its argument cannot be given to one whose state keeps
// what it is given - it would outlive what it borrows.
type It = { next(): { value: any, done: boolean } };

function* each(a: number[]) {
for (const v of a) yield v;
}

@keeps("iter")
function* plusOne<TIter extends It>(iter: TIter) {
for (const v of iter) yield v + 1;
}

function main() {
const a = [1, 2, 3];
for (const v of plusOne(each(a))) print(v);
}
16 changes: 16 additions & 0 deletions tslang/test/tester/own/own_err_generator_keeps_missing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -mm=own rejects: a generator's state that owns its arguments owns every one it holds, so `@keeps`
// has to name the callback too.
type It = { next(): { value: any, done: boolean } };

function* count(n: number) {
for (let i = 0; i < n; i++) yield i;
}

@keeps("iter")
function* mapped<TIter extends It>(iter: TIter, f: (x: number) => number) {
for (const v of iter) yield f(v);
}

function main() {
for (const v of mapped(count(3), (x) => x * 2)) print(v);
}
Loading
Loading