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
13 changes: 9 additions & 4 deletions docs/own-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ function main() {
30
```

Given an iterator that borrows its container (`s.values()` of a `Set`, a generator over a
parameter), such a generator owns that iterator but is bounded by the container in turn: it can be
walked in place while the container lives (`for (const v of s.values().take(2))`), but not stored,
returned or captured past it.

### `@lends`

`@lends("p")`: the result is a borrow of parameter `p` (or of `"this"`): the caller owns
Expand Down Expand Up @@ -734,10 +739,10 @@ a reference gives it back on every path out of a function.
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 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.
as in JavaScript: `const seq = fibonacci().map(f)`. Over an iterator that borrows its container
(`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`.
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 @@ -4574,8 +4574,40 @@ own, 200000 iterations peak at 4.4 MB, as 1000 do (4.3 MB). Negatives: `own_err_
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
Not here: a generator that borrows its container given to a keeper (`s.values().take(2)`) - §39. 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.
borrow of it ends. `reduce` with a string accumulator, as before.

## 39. A keeper given a bounded generator is bounded in turn, 2026-10-27

`new Set([1, 2, 3]).values().take(2)` (DefaultLib `iterator_take`, `iterator_drop`): `values()` is a
generator whose state borrows the set (§19, bounded by it), and `take` says `@keeps` (§38), so its
state owns the iterator it is given. Moving a bounded result into a kept argument was "borrows its
generator's arguments and cannot be stored, returned or captured" - the keeper might outlive the set.

It need not: the keeper's state owns the bounded state, and so is bounded by the same arguments. When
`boundedUses` meets the bounded result as a kept argument of a call whose result is a keeper's state
(`ownsKeptState`: -1 in place of the box in `ts.own_borrowing_fields`), it follows that result as it
follows a local holding the bounded one: its uses - a `for...of` over it, a `next()` in the
statement, a local holding it, another keeper given it (`.drop(1).take(2)`) - are the bounded
result's, checked against the ends of its arguments as before, and returning, storing or capturing
it is still refused. The release needs nothing new: the keeper's state frees the bounded state, whose
release gives back nothing it borrows, so the order against the container's release does not matter.

Only a bounded *result* is followed so: what `boundedUses` walks is the call's result and the locals
holding it, each this function's to move. A keeper given a borrow of something else (an element, a
parameter) is the argument move's business, unchanged.

`own_generator_keeps_bounded` walks keepers over a generator that borrows a temporary, two keepers
deep, one step in the statement that made it, and a chain over a named source held in a local, churning
between steps, under own, rc, gc and none; AOT under own, 200000 iterations peak at 4.4 MB, as 1000
do. Negatives: `own_err_generator_keeps_bounded` (now: returned past its source - the §38 shape,
walked in place, is accepted) and `own_err_generator_keeps_bounded_outlives` (used after the source
is overwritten).

DefaultLib: `iterator_take` and `iterator_drop` pass under own. Their `limit` is now a number, as in
JavaScript: `take(Infinity)` converted `Infinity` to an `index`, which is no particular count: the same
line took all three under rc and none under own. `reduce`'s string accumulator, listed in
§38 as open, is DefaultLib #45 (its accumulator typed `V`, not the `any` that `?? iter.next().value`
gives).
15 changes: 15 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceClosures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ namespace own_inference
return fields;
}

bool OwnershipInferencePass::ownsKeptState(mlir::Type recordType)
{
auto fields = borrowingFieldsOf(recordType);
return !fields.empty() && fields.front() < 0;
}

bool OwnershipInferencePass::isYieldedField(mlir::Value ref)
{
auto propertyRefOp = ref.getDefiningOp<mlir_ts::PropertyRefOp>();
Expand Down Expand Up @@ -503,6 +509,15 @@ namespace own_inference
{
if (isKeptArgument(user, used))
{
// given to a generator that says `@keeps` (`s.values().take(2)`): its state
// owns the result, so is bounded by the same arguments - its uses are this
// result's (spec 38)
if (user->getNumResults() == 1 && ownsKeptState(user->getResult(0).getType()))
{
values.push_back(user->getResult(0));
return;
}

kept = user;
}

Expand Down
4 changes: 4 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferencePass.h
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ class OwnershipInferencePass
// (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first.
llvm::SmallVector<int32_t> borrowingFieldsOf(mlir::Type recordType);

// Is this the state object of a generator that says `@keeps` - one that owns its arguments
// (spec 38; -1 in place of the box in borrowingFieldsOf)?
bool ownsKeptState(mlir::Type recordType);

// Is `ref` a field of a generator's state object that only yields (OWN_YIELDED_FIELDS_ATTR_NAME)?
bool isYieldedField(mlir::Value ref);

Expand Down
15 changes: 9 additions & 6 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_generator_keeps)
own_for_of_borrowed_iterator own_generator_keeps own_generator_keeps_bounded)
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,11 +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.
# A generator whose `@keeps` makes its state own its arguments (spec 38), and one given a generator
# that borrows; the same programs 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")
foreach(keeps_test own_generator_keeps own_generator_keeps_bounded)
tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts")
tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts")
endforeach()
endforeach()

# A value read out of an assigned global, lent to a call that assigns only other globals, outlives the
Expand Down Expand Up @@ -4638,7 +4640,8 @@ set(own_error_cases
"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")
"own_err_generator_keeps_bounded|:14:17: error: '[^']*' borrows its generator's arguments and cannot be stored, returned or captured"
"own_err_generator_keeps_bounded_outlives|:20:11: error: '[^']*' borrows '[^']*' but is used here after '[^']*' is released or overwritten")
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
13 changes: 9 additions & 4 deletions tslang/test/tester/own/own_err_generator_keeps_bounded.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// -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.
// -mm=own rejects: a generator that says `@keeps`, given one that borrows its argument, is bounded
// by that argument too, so it cannot be returned past it.
type It = { next(): { value: any, done: boolean } };

function* each(a: number[]) {
Expand All @@ -11,7 +11,12 @@ function* plusOne<TIter extends It>(iter: TIter) {
for (const v of iter) yield v + 1;
}

function make() {
const a: number[] = [];
a.push(1);
return plusOne(each(a));
}

function main() {
const a = [1, 2, 3];
for (const v of plusOne(each(a))) print(v);
for (const v of make()) print(v);
}
21 changes: 21 additions & 0 deletions tslang/test/tester/own/own_err_generator_keeps_bounded_outlives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// -mm=own rejects: a generator that says `@keeps`, given one that borrows its argument, used after
// that argument is overwritten.
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() {
let a: number[] = [];
a.push(1, 2);
const g = plusOne(plusOne(each(a)));
print(g.next().value);
a = [];
print(g.next().value);
}
79 changes: 79 additions & 0 deletions tslang/test/tester/own/own_generator_keeps_bounded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// A generator that says `@keeps`, given one that borrows its argument, owns that generator and is
// bounded by the same argument (spec 38): walked in place - a for...of, a call in the statement -
// while the argument lives, as `new Set(a).values().take(2)` is. Each case churns between steps, so
// a source freed early reads garbage.
class C {
v: number[] = [];
constructor(public x: number) {}
}

function churn() {
const junk: C[] = [];
for (let i = 0; i < 64; i++) {
const c = new C(999);
c.v.push(99);
junk.push(c);
}
}

type It = { next(): { value: any, done: boolean } };

class Bag {
items: C[] = [];
constructor(n: number) {
for (let i = 1; i <= n; i++) this.items.push(new C(i));
}
}

// borrows its argument: its result is bounded by `b`. It reads through `b` at each step, as a
// Set's `values()` reads through the set.
function* each(b: Bag) {
for (let i = 0; i < b.items.length; i++) yield b.items[i].x;
}

@keeps("iter")
function* take<TIter extends It>(iter: TIter, limit: number) {
let count = 0;
for (const v of iter) {
if (count++ >= limit) break;
yield v;
}
}

@keeps("iter")
function* drop<TIter extends It>(iter: TIter, limit: number) {
let count = 0;
for (const v of iter) {
if (count++ < limit) continue;
yield v;
}
}

function main() {
let t = 0;
for (let i = 0; i < 1000; i++) {
// a temporary source, in a for...of header
for (const v of take(each(new Bag(3)), 2)) {
churn();
t += v;
}

// two makers deep
for (const v of take(drop(each(new Bag(4)), 1), 2)) {
churn();
t += v;
}

// one step, in the statement that made it
t += drop(each(new Bag(2)), 1).next().value;

// a named source, the chain held in a local
const b = new Bag(3);
const g = drop(take(each(b), 3), 2);
churn();
t += g.next().value;
}

assert(t == 1000 * (3 + 5 + 2 + 3), "t");
print("done.");
}
Loading