From 9014a0489c07d10224f6a6e39ca2329f4568664e Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 15:27:11 +0000 Subject: [PATCH 1/2] -mm=own: a @keeps generator given a bounded generator is bounded in turn `new Set(a).values().take(2)` was "borrows its generator's arguments and cannot be stored": moving a bounded result (a generator that borrows its argument, spec 19) into a `@keeps` generator's kept argument (spec 38) counted as keeping it. The keeper's state owns the bounded state, so it is bounded by the same arguments: `boundedUses` now follows the keeper's result as the bounded one's, so it can be walked in place but not returned, stored or used after its source ends. Spec section 39. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 13 ++- .../2026-09-24-own-memory-model-design.md | 40 +++++++++- .../TypeScript/OwnershipInferenceClosures.cpp | 15 ++++ .../lib/TypeScript/OwnershipInferencePass.h | 4 + tslang/test/tester/CMakeLists.txt | 15 ++-- .../own/own_err_generator_keeps_bounded.ts | 13 ++- ...wn_err_generator_keeps_bounded_outlives.ts | 21 +++++ .../tester/own/own_generator_keeps_bounded.ts | 79 +++++++++++++++++++ 8 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 tslang/test/tester/own/own_err_generator_keeps_bounded_outlives.ts create mode 100644 tslang/test/tester/own/own_generator_keeps_bounded.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index 5a7dbea29..c5284395a 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -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 @@ -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` 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` 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`. 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 5d1d1912a..9b92341b5 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 @@ -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. \ No newline at end of file +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 (0 under +rc, anything under own), so it took nothing in every model. `reduce`'s string accumulator, listed in +§38 as open, is DefaultLib #45 (its accumulator typed `V`, not the `any` that `?? iter.next().value` +gives). diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index a1db67018..60a9d109c 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -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(); @@ -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; } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.h b/tslang/lib/TypeScript/OwnershipInferencePass.h index 76e0f77b5..80f43c43d 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.h +++ b/tslang/lib/TypeScript/OwnershipInferencePass.h @@ -245,6 +245,10 @@ class OwnershipInferencePass // (OWN_BORROWING_FIELDS_ATTR_NAME, the signature pass's), the `.captured` box's first. llvm::SmallVector 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); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 4c2c84529..e4c275c23 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -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. @@ -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 @@ -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) diff --git a/tslang/test/tester/own/own_err_generator_keeps_bounded.ts b/tslang/test/tester/own/own_err_generator_keeps_bounded.ts index d61aa5bbe..bfb8b8f1a 100644 --- a/tslang/test/tester/own/own_err_generator_keeps_bounded.ts +++ b/tslang/test/tester/own/own_err_generator_keeps_bounded.ts @@ -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[]) { @@ -11,7 +11,12 @@ function* plusOne(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); } diff --git a/tslang/test/tester/own/own_err_generator_keeps_bounded_outlives.ts b/tslang/test/tester/own/own_err_generator_keeps_bounded_outlives.ts new file mode 100644 index 000000000..8543dbd18 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_keeps_bounded_outlives.ts @@ -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(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); +} diff --git a/tslang/test/tester/own/own_generator_keeps_bounded.ts b/tslang/test/tester/own/own_generator_keeps_bounded.ts new file mode 100644 index 000000000..7bc394b4f --- /dev/null +++ b/tslang/test/tester/own/own_generator_keeps_bounded.ts @@ -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(iter: TIter, limit: number) { + let count = 0; + for (const v of iter) { + if (count++ >= limit) break; + yield v; + } +} + +@keeps("iter") +function* drop(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."); +} From 0783639fa9ed33aaf1388326fcca6d49eebaf93b Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 15:27:33 +0000 Subject: [PATCH 2/2] Spec 39: what take(Infinity) did, per model Co-Authored-By: Claude Opus 5.5 --- .../superpowers/specs/2026-09-24-own-memory-model-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 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 9b92341b5..55894690c 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 @@ -4607,7 +4607,7 @@ walked in place, is accepted) and `own_err_generator_keeps_bounded_outlives` (us 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 (0 under -rc, anything under own), so it took nothing in every model. `reduce`'s string accumulator, listed 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).