From 82517d83157c1cadef6390f48adef6c35a18b77f Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Tue, 27 Oct 2026 13:43:13 +0000 Subject: [PATCH] -mm=own: a generator that says @keeps owns the arguments its state holds A generator's state captured its parameters as borrows (spec 19), so a lazy helper such as `iter.map(f)` could not be stored or returned past the expression that built it. `@keeps` on a generator now names the parameters its state takes over: callers move them in, the state's release frees them, and the generator is not bounded by its arguments. Spec section 38. Co-Authored-By: Claude Opus 5.5 --- docs/own-memory-model.md | 40 ++++++++-- .../2026-09-24-own-memory-model-design.md | 45 ++++++++++- tslang/lib/TypeScript/OwnershipFacts.h | 17 ++++ .../TypeScript/OwnershipInferenceClosures.cpp | 9 ++- .../OwnershipSignatureGenerators.cpp | 69 +++++++++++++++- .../lib/TypeScript/OwnershipSignaturePass.cpp | 13 +++ .../lib/TypeScript/OwnershipSignaturePass.h | 10 ++- tslang/test/tester/CMakeLists.txt | 14 +++- .../own/own_err_generator_keeps_bounded.ts | 17 ++++ .../own/own_err_generator_keeps_missing.ts | 16 ++++ .../own/own_err_generator_keeps_used_after.ts | 17 ++++ tslang/test/tester/own/own_generator_keeps.ts | 79 +++++++++++++++++++ 12 files changed, 333 insertions(+), 13 deletions(-) create mode 100644 tslang/test/tester/own/own_err_generator_keeps_bounded.ts create mode 100644 tslang/test/tester/own/own_err_generator_keeps_missing.ts create mode 100644 tslang/test/tester/own/own_err_generator_keeps_used_after.ts create mode 100644 tslang/test/tester/own/own_generator_keeps.ts diff --git a/docs/own-memory-model.md b/docs/own-memory-model.md index c0f04253d..5a7dbea29 100644 --- a/docs/own-memory-model.md +++ b/docs/own-memory-model.md @@ -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(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` @@ -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` 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` 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 94a0f229a..5d1d1912a 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 @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index e62fc2331..6ba35f4d8 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -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(def)) + { + auto symbolRefOp = callIndirectOp.getCallee().getDefiningOp(); + auto callee = symbolRefOp ? mlir::SymbolTable::lookupNearestSymbolFrom(def, symbolRefOp.getIdentifierAttr()) + : mlir_ts::FuncOp(); + return callee && !callee.isDeclaration(); + } + if (mlir::isa(def)) { @@ -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(def); + castOp && castOp.getIn().getDefiningOp() && mlir::isa(castOp.getIn().getType())) + { + return true; + } + if (!isView(def)) { return false; diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index ab0faa147..a1db67018 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -1041,8 +1041,15 @@ namespace own_inference auto name = varName(varOp); auto elementType = mlir::cast(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(init) : mlir::BlockArgument(); + auto keptParam = argument && argument.getOwner()->isEntryBlock() && + argument.getOwner()->getParentOp() == getFunction().getOperation() && + llvm::is_contained(ownedParams(getFunction()), static_cast(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"); diff --git a/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp index ed88907a8..d331bec6b 100644 --- a/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp +++ b/tslang/lib/TypeScript/OwnershipSignatureGenerators.cpp @@ -550,6 +550,53 @@ namespace own_signature return true; } + llvm::SmallVector OwnershipSignaturePass::paramsCapturedIntoState(mlir_ts::FuncOp maker) + { + llvm::SmallVector captured; + maker.walk([&](mlir_ts::NewOp newOp) { + auto valueRefType = mlir::dyn_cast(newOp.getType()); + auto tupleType = valueRefType ? mlir::dyn_cast(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(user); + if (!propertyRefOp) + { + continue; + } + + for (auto *fieldUser : propertyRefOp->getUsers()) + { + auto storeOp = mlir::dyn_cast(fieldUser); + auto cellOp = storeOp ? storeOp.getValue().getDefiningOp() : 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(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(); @@ -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(user); @@ -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; @@ -747,11 +808,15 @@ namespace own_signature llvm::SmallVector entries; for (auto &[type, state] : states) { - llvm::SmallVector fields{state.captured}; + // the box first, -1 for one that owns: its readers compare the box's position with it + llvm::SmallVector 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)); diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index 7a73179ca..d7c2a24c8 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -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; } diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.h b/tslang/lib/TypeScript/OwnershipSignaturePass.h index 77813a559..bd11d8a03 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.h +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.h @@ -936,9 +936,14 @@ class OwnershipSignaturePass : public mlir::PassWrapper 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 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 @@ -978,6 +983,9 @@ class OwnershipSignaturePass : public mlir::PassWrapper fields; // the stores of `this` into the box, which keep nothing once the state borrows llvm::SmallVector 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); diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d8161560c..4c2c84529 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_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. @@ -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) @@ -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 handle. use 'Shared'" "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 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) diff --git a/tslang/test/tester/own/own_err_generator_keeps_bounded.ts b/tslang/test/tester/own/own_err_generator_keeps_bounded.ts new file mode 100644 index 000000000..d61aa5bbe --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_keeps_bounded.ts @@ -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(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); +} diff --git a/tslang/test/tester/own/own_err_generator_keeps_missing.ts b/tslang/test/tester/own/own_err_generator_keeps_missing.ts new file mode 100644 index 000000000..52734c3f6 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_keeps_missing.ts @@ -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(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); +} diff --git a/tslang/test/tester/own/own_err_generator_keeps_used_after.ts b/tslang/test/tester/own/own_err_generator_keeps_used_after.ts new file mode 100644 index 000000000..410c22880 --- /dev/null +++ b/tslang/test/tester/own/own_err_generator_keeps_used_after.ts @@ -0,0 +1,17 @@ +// -mm=own rejects: an argument a `@keeps` generator's state owns moved into it, and is read after. +class C { + constructor(public x: number) {} +} + +@keeps("c") +function* twice(c: C) { + yield c.x; + yield c.x * 2; +} + +function main() { + const c = new C(5); + const g = twice(c); + print(c.x); + for (const v of g) print(v); +} diff --git a/tslang/test/tester/own/own_generator_keeps.ts b/tslang/test/tester/own/own_generator_keeps.ts new file mode 100644 index 000000000..b815528b2 --- /dev/null +++ b/tslang/test/tester/own/own_generator_keeps.ts @@ -0,0 +1,79 @@ +// A generator that says `@keeps` owns the arguments its state holds (spec 38): its callers move them +// in, the state's release frees them, and the generator is bounded by nothing - it may be stored, +// returned, or given to another generator that keeps it. What the default library's lazy iterator +// helpers (`map`, `filter`, `take`, `drop`) are. Each case churns between steps, so an argument 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 } }; + +// a class argument +@keeps("c") +function* twice(c: C) { + yield c.x; + churn(); + yield c.x * 2; +} + +// an iterator argument +@keeps("iter") +function* plusOne(iter: TIter) { + for (const v of iter) yield v + 1; +} + +// an iterator and a callback +@keeps("iter", "f") +function* mapped(iter: TIter, 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 double(x: number) { + return x * 2; +} + +// returned past the argument's expression +function make() { + return twice(new C(5)); +} + +const top = twice(new C(3)); + +function main() { + let t = 0; + for (let i = 0; i < 1000; i++) { + const g = make(); + churn(); + for (const v of g) t += v; + + const h = plusOne(plusOne(count(3))); + churn(); + for (const v of h) t += v; + + for (const v of mapped(count(3), (x) => x * 10)) t += v; + for (const v of mapped(plusOne(count(2)), double)) t += v; + + // given up after one step + const k = twice(new C(1)); + t += k.next().value; + } + + t += top.next().value; + assert(t == 1000 * (15 + 9 + 30 + 6 + 1) + 3, "t"); + print("done."); +}