From 2f40bcb8d4237764aaa9c43bf06b632161c8ad4b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:46:31 -0500 Subject: [PATCH] Lower iterator helpers on generators and Map and Set iterators map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every and find compiled only as one chain from arr.values() or arr.entries() that ended in a consuming method. Every other form was refused, and in JavaScript the refusal was thrown at run time. IteratorObject now maps to the generator type. A consuming method over a chain of stages fuses into one loop over the source: a generator driven like for-of, a Map or Set walked live, or an array by index. A stage on its own builds a helper object: a generator over its source that supports next(), for...of, spreads, Array.from and yield*. A for...of whose head is a chain of stages without flatMap runs the stages inside the loop. The source is closed where Node closes it: when take reaches its count, when some, every or find has its answer, when a callback or the loop body throws, when the loop exits early, and before the RangeError of an invalid take or drop budget. It is not closed when it is exhausted or its own next() throws. return() on a helper object before its first next() closes the source through a new runtime hook, scr_gen_set_unstarted_return. When closing an iterator throws while another error propagates, the first error reaches the caller, as in Node. This applies to for...of over a generator too, where the error from the close used to replace the body's error. The try statement takes a dropFinallyErrors flag, whose cleanup handler calls the new scr_exc_keep_first. throw() lowers only on values typed as generators. Iterator helpers and the built-in iterators have no throw method, and a value typed as an iterator may hold one. Callbacks in JavaScript bind their unannotated parameters to the element type, and flatMap callbacks may return generators. A generator whose record yields share checked storage with its return channel has each value converted to the declared element type as it is pulled. --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/iterator-helpers.mdx | 1 + .../limitations/builtin-apis/map-and-set.mdx | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 47 +- packages/compiler/src/backend/mangle.ts | 5 + .../compiler/src/coverage/surface-manifest.ts | 4 +- .../frontend/lowering/containers/for-of.ts | 2 +- .../lowering/containers/iterator-helpers.ts | 2025 +++++++++++++---- .../src/frontend/lowering/lower-calls.ts | 26 +- .../src/frontend/lowering/lower-generators.ts | 33 +- .../src/frontend/lowering/lower-stmts.ts | 7 + packages/compiler/src/frontend/type-mapper.ts | 5 +- packages/compiler/src/ir/ir.ts | 15 +- packages/compiler/surface-manifest.json | 12 +- .../5908-iterator-helper-objects.ts.json | 6 + .../5909-iterator-helper-objects-js.js.json | 6 + packages/runtime/src/scr_async.c | 11 +- packages/runtime/src/scr_exception.c | 11 + packages/runtime/src/scr_exception.h | 4 + packages/runtime/src/scr_generator.h | 3 + tests/corpus/5908-iterator-helper-objects.ts | 214 ++ .../corpus/5909-iterator-helper-objects-js.js | 58 + .../collection-iterator-helpers.ts | 17 +- .../collection-iterator-helpers.ts.txt | 42 +- .../__snapshots__/es2025-fences.ts.txt | 11 +- .../string-iterator-helpers.ts.txt | 11 +- tests/harness/corpus-costs.json | 4 + 27 files changed, 2003 insertions(+), 581 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/iterator-helpers.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json create mode 100644 tests/corpus/5908-iterator-helper-objects.ts create mode 100644 tests/corpus/5909-iterator-helper-objects-js.js diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 6f21e2ce3..c15ff67cc 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -120,6 +120,8 @@ title: "Limitations" ../limitations/builtin-apis/synchronous-iteration.mdx +../limitations/builtin-apis/iterator-helpers.mdx + ../limitations/builtin-apis/regular-expressions.mdx ../limitations/builtin-apis/object-freezing.mdx diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx new file mode 100644 index 000000000..01f779cb6 --- /dev/null +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -0,0 +1 @@ +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported. diff --git a/docs/content/limitations/builtin-apis/map-and-set.mdx b/docs/content/limitations/builtin-apis/map-and-set.mdx index 04ebcbfca..12be44e06 100644 --- a/docs/content/limitations/builtin-apis/map-and-set.mdx +++ b/docs/content/limitations/builtin-apis/map-and-set.mdx @@ -1 +1 @@ -- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. +- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map` and `Set`, and promises requiring payload adapters, cannot pass into these slots. `Promise` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported. diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 84614b6cb..bac2818be 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -165,6 +165,7 @@ import { mangleFnClosure, mangleFunction, mangleGenDrop, + mangleGenSourceClose, mangleGenSpawn, mangleGlobal, mangleLocal, @@ -4623,6 +4624,25 @@ export class LlEmitter { dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``); for (const line of dr) out.push(line); + // An iterator helper's return() before its first next() closes the + // source generator in its first argument slot. + const closesSource = fn.generator.closesSource === true; + if (closesSource) { + const field = lifted ? 1 : 0; + this.declare(`declare void @scr_gen_close(ptr)`); + this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`); + out.push( + `define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`, + `entry:`, + ` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`, + ` %sv = load ptr, ptr %sp`, + ` call void @scr_gen_close(ptr %sv)`, + ` ret void`, + `}`, + ``, + ); + } + // Spawn wrapper: pack the args (+1 moves in), allocate the // SUSPENDED fiber — nothing runs until the first .next(). if (lifted) { @@ -4639,6 +4659,11 @@ export class LlEmitter { settleAsync === null ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + ...(closesSource + ? [ + ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, + ] + : []), ` ret ptr %gg`, `}`, ``, @@ -9517,14 +9542,15 @@ export class LlEmitter { B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`); B.line(`store ptr ${stash}, ptr ${stashSlot}`); this.scopes.push([{ slot: stashSlot, type: CAUGHT }]); - const suppressHandler = s.suppressFinallyErrors - ? { - label: B.newLabel("try.fs"), - used: false, - frameDepth: this.frames.length, - scopeDepth: this.scopes.length, - } - : null; + const suppressHandler = + s.suppressFinallyErrors || s.dropFinallyErrors + ? { + label: B.newLabel("try.fs"), + used: false, + frameDepth: this.frames.length, + scopeDepth: this.scopes.length, + } + : null; if (suppressHandler) this.tryStack.push(suppressHandler); this.emitBlock(s.finallyBody!); if (suppressHandler) this.tryStack.pop(); @@ -9534,8 +9560,9 @@ export class LlEmitter { this.emitUnwind(); if (suppressHandler?.used) { B.startBlock(suppressHandler.label); - this.declare(`declare void @scr_exc_suppress(ptr)`); - B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`); + const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress"; + this.declare(`declare void @${cleanup}(ptr)`); + B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`); this.emitUnwind(); } } diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 0bc428964..43c9cea71 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string { export function mangleGenDrop(fnName: string): string { return `sc_gd_${sanitize(fnName)}`; } +/** The never-started return() hook of a generator that closes its source + * argument (an iterator helper). */ +export function mangleGenSourceClose(fnName: string): string { + return `sc_gc_${sanitize(fnName)}`; +} /** Emitted ref-kind resolve thunk, interned per inner-type key. */ export function mangleResolveThunk(n: number): string { return `sc_rslv_${n}`; diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 45814cb8b..3477e1847 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes status: "static", ...(COLLECTION_ITERATOR_METHODS.has(name) ? { - note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused", + note: "live native iterators support next(), for...of, drains, and the iterator helper methods", } : {}), }); @@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes status: "static", ...(COLLECTION_ITERATOR_METHODS.has(name) ? { - note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused", + note: "live native iterators support next(), for...of, drains, and the iterator helper methods", } : {}), }); diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 4aa24b0b3..6aa838dea 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -747,7 +747,7 @@ function lowerForOfMapOrSet( * Recurses through every nested statement list; nested functions are * lifted elsewhere and never appear here, so every return found exits * THIS function through the active iteration. */ -function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] { +export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] { const walk = (list: IrStmt[]): IrStmt[] => list.flatMap((s): IrStmt[] => { switch (s.kind) { diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index f0928d2a1..7cb3753ac 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -4,7 +4,10 @@ import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { BOOL, + CAUGHT, F64, + type IrMapIntrinsicMethod, + type IrSetIntrinsicMethod, type IrExpr, type IrFunction, type IrLocal, @@ -16,11 +19,22 @@ import { UNDEFINED_T, VOID, arrayOf, + isUnitType, typeEquals, } from "../../../ir/ir.js"; import { locOf } from "../../program.js"; -import { typeKey } from "../../type-mapper.js"; +import { genResultRecord, typeKey } from "../../type-mapper.js"; import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; +import { + type GenType, + generatorLoopBinding, + genStep, + genValue, + stepDrivable, +} from "../lower-generators.js"; +import { exitBeforeReturns } from "./for-of.js"; +import { iteratorSlotAdapter } from "../iterator-adapters.js"; +import { lowerArrayCallback } from "./callback-arguments.js"; /* ── iterator helpers (ES2025) ─────────────────────────────────────────── */ @@ -37,9 +51,9 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; * raw value in the message) through an interned checker call inserted * at the argument's evaluation position. The source array is iterated * LIVE by index against a re-read length, exactly the array iterator's - * contract. Everything else — iterator objects stored in bindings, - * generator/Set/Map receivers, Iterator.from — keeps the lib fences. - * Null when the callee isn't a terminal over such a chain. */ + * contract. Chains over generators and other iterator objects lower in + * lowerGeneratorHelperCall. Null when the callee isn't a terminal over + * such a chain. */ export function lowerIteratorHelperCall( lowerer: Lowerer, call: ts.CallExpression, @@ -51,7 +65,7 @@ export function lowerIteratorHelperCall( // Walk receiver: stage* ← values()/entries() ← array-typed source // (entries seeds the chain with [index, element] pairs — the // checker's own tuple type). - const stages: { name: string; argNode: ts.Expression }[] = []; + const stages: StageNode[] = []; let cur: ts.Expression = access.expression; let source: ts.Expression | null = null; let srcProj: "values" | "entries" = "values"; @@ -73,16 +87,14 @@ export function lowerIteratorHelperCall( ts.isSpreadElement(cur.arguments[0]!) ) return null; - stages.push({ name, argNode: cur.arguments[0]! }); + stages.push({ name, argNode: cur.arguments[0]!, access: cur.expression }); cur = cur.expression.expression; } stages.reverse(); // source order for (const s of stages) { - const acc = findStageAccess(call, s); - if (acc !== null && !lowerer.isStdlibMember(acc)) return null; + if (!lowerer.isStdlibMember(s.access)) return null; } if (!lowerer.isStdlibMember(access)) return null; - const loc = locOf(call); // Lower everything in SOURCE ORDER (the chain's own evaluation // order): the source array, each stage argument, the terminal's. const items = lowerer.lowerExpr(source); @@ -90,81 +102,502 @@ export function lowerIteratorHelperCall( // The entries() seed: the interned [number, T] tuple — the same shape // the checker's own [number, T] maps to, so stage callbacks typed // against the lib's pairs intern equal. - let elem: IrType = - srcProj === "entries" - ? { - kind: "record", - shapeId: lowerer.shapes.intern( - [ - { name: "0", type: F64 }, - { name: "1", type: items.type.elem }, - ], - true, - ), - } - : items.type.elem; - type StageIr = - | { - kind: "map" | "filter" | "flatMap"; - fn: IrExpr & { type: IrType & { kind: "func" } }; - out: IrType; - } - | { kind: "take" | "drop"; budget: IrExpr }; - const stageIrs: StageIr[] = []; - for (const s of stages) { - if (s.name === "take" || s.name === "drop") { - const n = lowerer.lowerExpr(s.argNode); - if (n.type.kind !== "f64") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - // Node validates at the take()/drop() call — the checked budget - // rides an interned helper call AT this argument position. - stageIrs.push({ + const elem: IrType = + srcProj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + return lowerFusedChain( + lowerer, + call, + terminal, + stages, + { kind: "array", type: items.type, proj: srcProj }, + items, + elem, + ); +} + +/** Iterator-helper calls whose receiver is an iterator object: a + * generator, a stored array iterator, a helper result (IteratorObject), + * an `Iterator` value, or a Map/Set iterator (adapted into a + * generator over the live collection). + * + * - A terminal (toArray/forEach/reduce/some/every/find) fuses the chain + * of helper stages before it into one loop over the source generator, + * with the helpers' pull order and closing: the source is closed (its + * `return()` runs) when take's budget is spent, a short-circuit + * terminal hits, or a callback throws, and is not closed when it is + * exhausted or its own `next()` throws. + * - A stage (map/filter/take/drop/flatMap) outside such a chain builds + * an iterator helper object: a generator over its source that pulls + * lazily, closes the source on early exit, and closes it on a + * `return()` before the first `next()` (`closesSource`). + * + * Budgets and callbacks validate at the call, as in Node. Null when the + * call is not such a helper call. */ +export function lowerGeneratorHelperCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (call.questionDotToken || access.questionDotToken) return null; + const name = access.name.text; + const isTerminal = ITER_TERMINALS.has(name); + if (!isTerminal && !ITER_STAGES.has(name)) return null; + if (!isHelperReceiver(lowerer, access.expression)) return null; + if (!lowerer.isStdlibMember(access)) return null; + const loc = locOf(call); + if (!isTerminal) { + const argNode = call.arguments[0]; + if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); + } + const src = lowerHelperSource(lowerer, access.expression); + if (name !== "take" && name !== "drop") { + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc); + return lowerHelperObject(lowerer, call, src.gen, src.elem, stage, out, loc); + } + // The budget check closes the source, so the source is bound first. + const bound = bindSource(lowerer, src.gen, loc); + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc, bound.ref); + const helper = lowerHelperObject(lowerer, call, bound.ref, src.elem, stage, out, loc); + return { kind: "seqExpr", stmts: [bound.decl], result: helper, type: helper.type, loc }; + } + // The stages before the terminal, back to the first receiver that is + // not itself a helper stage. + const stages: StageNode[] = []; + let cur: ts.Expression = access.expression; + while (isStageCall(lowerer, cur)) { + const stageAccess = cur.expression as ts.PropertyAccessExpression; + stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + cur = stageAccess.expression; + } + stages.reverse(); // source order + // A chain from m.keys()/values()/entries() (or a Set's) walks the + // collection itself, live, as for-of does. + const collection = collectionIteratorCall(lowerer, cur); + if (collection !== null) { + const items = lowerer.lowerExpr(collection.receiver); + if (items.type.kind !== "map" && items.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + const source: ChainSource = { kind: "collection", type: items.type, proj: collection.proj }; + return lowerFusedChain( + lowerer, + call, + name, + stages, + source, + items, + collectionSeed(lowerer, source), + ); + } + const src = lowerHelperSource(lowerer, cur); + return lowerFusedChain( + lowerer, + call, + name, + stages, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.gen, + src.elem, + ); +} + +const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); +const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); +/** Native iterators that ride checked runtime values and adapt into + * generators for the helpers. */ +const ADAPTED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]); + +/** `for (x of )` where the head is a run of map/filter/take/drop + * stages over an array iterator, a Map or Set iterator, or a generator: + * one loop over the source with the stages nested in it and the loop body + * innermost, instead of a helper object per stage. The pull order and the + * closing are the helpers': `break`, `return` and throws from the body + * close a generator source (run its `return()`), and take closes it on + * the pass after its last delivery. Chains with flatMap keep the helper + * objects (a `break` there leaves an inner loop). Null when the head is + * not such a chain. */ +export function lowerForOfHelperChain( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + labels: string[] | undefined, +): IrStmt | null { + if (stmt.awaitModifier) return null; + if ( + ts.isVariableDeclarationList(stmt.initializer) && + (stmt.initializer.flags & ts.NodeFlags.Using) !== 0 + ) + return null; + let head: ts.Expression = stmt.expression; + while (ts.isParenthesizedExpression(head)) head = head.expression; + const stages: StageNode[] = []; + let cur: ts.Expression = head; + while (isStageCall(lowerer, cur)) { + const stageAccess = cur.expression as ts.PropertyAccessExpression; + if (stageAccess.name.text === "flatMap") return null; + stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + cur = stageAccess.expression; + } + if (stages.length === 0) return null; + stages.reverse(); // source order + const arrayRoot = arrayIteratorCall(lowerer, cur); + const collection = arrayRoot === null ? collectionIteratorCall(lowerer, cur) : null; + if (arrayRoot === null && collection === null) { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(cur)); + if (mapped?.kind !== "generator" || !stepDrivable(mapped)) return null; + } + const loc = locOf(stmt); + lowerer.scopes.push(new Map()); + try { + const frame = new HostFrame(lowerer, loc); + let items: IrExpr; + let source: ChainSource; + let elem: IrType; + if (arrayRoot !== null) { + items = lowerer.lowerExpr(arrayRoot.receiver); + if (items.type.kind !== "array") + lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver)); + source = { kind: "array", type: items.type, proj: arrayRoot.proj }; + elem = + arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + } else if (collection !== null) { + items = lowerer.lowerExpr(collection.receiver); + if (items.type.kind !== "map" && items.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + source = { kind: "collection", type: items.type, proj: collection.proj }; + elem = collectionSeed(lowerer, source); + } else { + const src = lowerHelperSource(lowerer, cur); + items = src.gen; + source = { kind: "generator", type: src.gen.type, elem: src.elem }; + elem = src.elem; + } + const itemsId = frame.local("items", items.type, false); + const out: IrStmt[] = [{ kind: "varDecl", localId: itemsId, init: items, loc }]; + // Stage arguments evaluate in chain order, before the first pull. + const slots: StageSlot[] = []; + for (const s of stages) { + const { stage, out: next } = lowerStage( + lowerer, + s, + elem, + loc, + source.kind === "generator" + ? (varRef(itemsId, source.type, loc) as IrExpr & { type: GenType }) + : null, + ); + const arg = "budget" in stage ? stage.budget : stage.fn; + const argId = frame.local("budget" in stage ? "n" : "f", arg.type, false); + out.push({ kind: "varDecl", localId: argId, init: arg, loc }); + slots.push(stageSlot(frame, stage, argId)); + elem = next; + } + const doneId = frame.local("done", BOOL, true); + out.push(...stagePrologue(slots, doneId, loc)); + // Each delivered value lands in a hidden local the loop binding reads. + const valueId = frame.local("hv", elem, false); + const binds = generatorLoopBinding(lowerer, stmt, varRef(valueId, elem, loc), elem); + let body = lowerer.inCtl( + "loop", + () => lowerer.lowerScopedBlock(stmt.statement), + source.kind === "collection" ? undefined : labels, + ); + if (source.kind === "collection") { + // A return from the body ends the live walk first, as for-of does. + const t = source.type; + body = exitBeforeReturns(body, () => ({ + kind: "exprStmt", + expr: + t.kind === "map" + ? { + kind: "mapIntrinsic", + method: "iterExit", + receiver: varRef(itemsId, t, loc), + args: [], + type: VOID, + loc, + } + : { + kind: "setIntrinsic", + method: "iterExit", + receiver: varRef(itemsId, t, loc), + args: [], + type: VOID, + loc, + }, + loc, + })); + } + const emit = foldStages( + lowerer, + frame, + slots, + (value) => [{ kind: "varDecl", localId: valueId, init: value, loc }, ...binds, ...body], + doneId, + loc, + ); + out.push( + ...sourceLoop( + lowerer, + frame, + itemsId, + source, + doneId, + emit, + loc, + source.kind === "collection" ? undefined : labels, + ), + ); + return { kind: "block", body: out, loc }; + } finally { + lowerer.scopes.pop(); + } +} + +/** `arr.values()` or `arr.entries()` on an array value, as the root of a + * helper chain. */ +function arrayIteratorCall( + lowerer: Lowerer, + expr: ts.Expression, +): { receiver: ts.Expression; proj: "values" | "entries" } | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + return null; + const proj = expr.expression.name.text; + if (proj !== "values" && proj !== "entries") return null; + if (lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind !== "array") return null; + if (!lowerer.isStdlibMember(expr.expression)) return null; + return { receiver: expr.expression.expression, proj }; +} + +/** One helper stage call in a chain, in source order. */ +interface StageNode { + name: string; + argNode: ts.Expression; + access: ts.PropertyAccessExpression; +} + +/** A lowered stage: its callback, or its validated budget. */ +type StageIr = + | { + kind: "map" | "filter" | "flatMap"; + fn: IrExpr & { type: IrType & { kind: "func" } }; + } + | { kind: "take" | "drop"; budget: IrExpr }; + +/** Where a fused chain's elements come from: a live array (by index), or + * a synchronous generator driven like for-of. */ +type ChainSource = + | { kind: "array"; type: IrType & { kind: "array" }; proj: "values" | "entries" } + | { kind: "generator"; type: GenType; elem: IrType } + | { + kind: "collection"; + type: (IrType & { kind: "map" }) | (IrType & { kind: "set" }); + proj: "keys" | "values" | "entries"; + }; + +/** `m.keys()`, `m.values()` or `m.entries()` (or a Set's) on a Map or + * Set value, as the root of a helper chain. */ +function collectionIteratorCall( + lowerer: Lowerer, + expr: ts.Expression, +): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + return null; + const proj = expr.expression.name.text; + if (proj !== "keys" && proj !== "values" && proj !== "entries") return null; + const kind = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind; + if (kind !== "map" && kind !== "set") return null; + if (!lowerer.isStdlibMember(expr.expression)) return null; + return { receiver: expr.expression.expression, proj }; +} + +/** The element a collection chain starts from: a key (a Set's element), + * a Map's value, or the interned [key, value] pair (a Set's [v, v]). */ +function collectionSeed(lowerer: Lowerer, source: ChainSource & { kind: "collection" }): IrType { + const t = source.type; + const keyT = t.kind === "map" ? t.key : t.elem; + if (source.proj === "keys" || (t.kind === "set" && source.proj === "values")) return keyT; + if (source.proj === "values") return (t as IrType & { kind: "map" }).value; + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: keyT }, + { name: "1", type: t.kind === "map" ? t.value : keyT }, + ], + true, + ), + }; +} + +/** The interned [number, T] pair an array's entries() yields. */ +function entryPairType(lowerer: Lowerer, elem: IrType): IrType { + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: F64 }, + { name: "1", type: elem }, + ], + true, + ), + }; +} + +/** Whether `expr` is an iterator object the helpers lower over: a + * synchronous generator-typed value, or a Map/Set iterator. */ +function isHelperReceiver(lowerer: Lowerer, expr: ts.Expression): boolean { + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (mapped?.kind === "generator") return mapped.async !== true; + const symbol = lowerer.typeOf(expr).getSymbol(); + return ( + mapped?.kind === "dyn" && + symbol !== undefined && + ADAPTED_ITERATOR_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} + +/** Whether `expr` is a stage call (`src.map(f)`) on a helper receiver. */ +function isStageCall(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return false; + if (expr.questionDotToken || expr.expression.questionDotToken) return false; + if (!ITER_STAGES.has(expr.expression.name.text)) return false; + if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return false; + return ( + isHelperReceiver(lowerer, expr.expression.expression) && lowerer.isStdlibMember(expr.expression) + ); +} + +/** The source of a helper call as a synchronous generator — the receiver + * itself, or a Map/Set iterator adapted into a generator over the live + * collection — and its element type. A generator whose yield channel is + * checked-dynamic beside a declared element type (`Generator

` boxes a + * record yield next to its `any` return channel) has each value converted + * to the declared type as it is pulled. */ +function lowerHelperSource( + lowerer: Lowerer, + expr: ts.Expression, +): { gen: IrExpr & { type: GenType }; elem: IrType } { + const tsType = lowerer.typeOf(expr); + const mapped = lowerer.mapTypeOf(tsType); + const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0]; + if (mapped?.kind === "generator") { + const gen = lowerer.lowerExpr(expr); + if (gen.type.kind !== "generator" || gen.type.async) lowerer.badType(expr, tsType); + const genT = gen.type; + const declared = genT.yieldT.kind === "dyn" && arg ? lowerer.mapTypeOf(arg) : null; + const elem = + declared !== null && + declared.kind !== "dyn" && + declared.kind !== "void" && + !isUnitType(declared) + ? declared + : genT.yieldT; + return { gen: gen as IrExpr & { type: GenType }, elem }; + } + const yieldT = arg ? lowerer.mapTypeOf(arg) : null; + if ( + !yieldT || + yieldT.kind === "void" || + !genResultRecord(yieldT, VOID, lowerer.shapes, lowerer.unions) + ) + lowerer.badType(expr, tsType); + const target: GenType = { kind: "generator", yieldT, retT: VOID, nextT: UNDEFINED_T }; + const adapted = iteratorSlotAdapter(lowerer, lowerer.lowerExpr(expr), target); + if (adapted === null || adapted.type.kind !== "generator") lowerer.badType(expr, tsType); + return { gen: adapted as IrExpr & { type: GenType }, elem: yieldT }; +} + +/** Lower one stage's argument against its input element type: the + * callback (value) or (value, counter), or the take/drop budget with + * Node's eager RangeError check at the argument's position. `out` is + * the stage's output element type. */ +function lowerStage( + lowerer: Lowerer, + s: StageNode, + elem: IrType, + loc: SrcLoc, + source: (IrExpr & { type: GenType }) | null = null, +): { stage: StageIr; out: IrType } { + if (s.name === "take" || s.name === "drop") { + const n = lowerer.lowerExpr(s.argNode); + if (n.type.kind !== "f64") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + // Node validates at the take()/drop() call — the checked budget + // rides an interned helper call AT this argument position. A + // generator source is closed before the RangeError, as Node closes + // the iterator the helper was called on. + return { + stage: { kind: s.name, budget: { kind: "call", - callee: internBudgetChecker(lowerer, loc), - args: [n], + callee: internBudgetChecker(lowerer, loc, source?.type ?? null), + args: source === null ? [n] : [n, source], type: F64, loc, }, - }); - continue; - } - const f = lowerer.lowerExpr(s.argNode); - if ( - f.type.kind !== "func" || - f.type.params.length > 2 || - (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || - (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") - ) { - lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - } - const fn = f as IrExpr & { type: IrType & { kind: "func" } }; - if (s.name === "map") { - stageIrs.push({ kind: "map", fn, out: fn.type.ret }); - elem = fn.type.ret; - } else if (s.name === "filter") { - if (fn.type.ret.kind !== "bool") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); - stageIrs.push({ kind: "filter", fn, out: elem }); - } else { - if (fn.type.ret.kind !== "array") { - lowerer.noLowering( - `.flatMap over a callback returning '${lowerer.fmt(fn.type.ret)}'`, - s.argNode, - "an array-returning callback is the supported form (iterators/strings have no lowering here)", - ); - } - stageIrs.push({ kind: "flatMap", fn, out: fn.type.ret.elem }); - elem = fn.type.ret.elem; - } + }, + out: elem, + }; } - // The terminal's own arguments. + // A flatMap callback returns its own array or generator type (an + // unannotated JavaScript arrow would otherwise return checked values). + let expectedReturn: IrType | undefined; + if (s.name === "flatMap") { + const sig = lowerer.checker.getCallSignatures(lowerer.typeOf(s.argNode))[0]; + const ret = sig ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(sig)) : null; + if (ret?.kind === "array" || ret?.kind === "generator") expectedReturn = ret; + } + // Unannotated JavaScript parameters bind the element type. + const f = lowerArrayCallback( + lowerer, + s.argNode, + [elem], + UNDEFINED_T, + false, + expectedReturn, + ).fnArg; + if ( + f.type.kind !== "func" || + f.type.params.length > 2 || + (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || + (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") + ) { + lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + } + const fn = f as IrExpr & { type: IrType & { kind: "func" } }; + if (s.name === "map") return { stage: { kind: "map", fn }, out: fn.type.ret }; + if (s.name === "filter") { + if (fn.type.ret.kind !== "bool") lowerer.badType(s.argNode, lowerer.typeOf(s.argNode)); + return { stage: { kind: "filter", fn }, out: elem }; + } + const ret = fn.type.ret; + if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem }; + if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret)) + return { stage: { kind: "flatMap", fn }, out: ret.yieldT }; + lowerer.noLowering( + `.flatMap over a callback returning '${lowerer.fmt(ret)}'`, + s.argNode, + "callbacks returning an array or a generator are the supported forms", + ); +} + +/** The terminal's arguments and result type over elements of `elem`. */ +function lowerTerminal( + lowerer: Lowerer, + call: ts.CallExpression, + terminal: string, + elem: IrType, +): { termArgs: IrExpr[]; resultT: IrType } { const termArgs: IrExpr[] = []; - let resultT: IrType; if (terminal === "toArray") { if (call.arguments.length !== 0) lowerer.noLowering(`.toArray with ${call.arguments.length} arguments`, call); - resultT = arrayOf(elem); - } else if ( + return { termArgs, resultT: arrayOf(elem) }; + } + if ( terminal === "forEach" || terminal === "some" || terminal === "every" || @@ -174,7 +607,7 @@ export function lowerIteratorHelperCall( if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering(`.${terminal} with ${call.arguments.length} arguments`, call); } - const f = lowerer.lowerExpr(argNode); + const f = lowerArrayCallback(lowerer, argNode, [elem], UNDEFINED_T).fnArg; if ( f.type.kind !== "func" || f.type.params.length > 2 || @@ -185,43 +618,84 @@ export function lowerIteratorHelperCall( lowerer.badType(argNode, lowerer.typeOf(argNode)); } termArgs.push(f); - resultT = - terminal === "forEach" ? VOID : terminal === "find" ? lowerer.withUndefinedArm(elem) : BOOL; - } else { - // reduce(f[, initial]) - const fNode = call.arguments[0]; - if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - fNode === undefined || - call.arguments.some((a) => ts.isSpreadElement(a)) - ) { - lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); - } - const f = lowerer.lowerExpr(fNode); - if ( - f.type.kind !== "func" || - f.type.params.length < 2 || - f.type.params.length > 3 || - !typeEquals(f.type.params[1]!, elem) || - (f.type.params.length === 3 && f.type.params[2]!.kind !== "f64") || - !typeEquals(f.type.params[0]!, f.type.ret) - ) { - lowerer.badType(fNode, lowerer.typeOf(fNode)); - } - const accT = (f.type as IrType & { kind: "func" }).ret; - if (call.arguments.length === 1 && !typeEquals(accT, elem)) { - lowerer.noLowering( - ".reduce without an initial value where the accumulator's type differs from the elements'", - call, - "pass the initial value: .reduce(f, init)", - ); - } - termArgs.push(f); - if (call.arguments[1] !== undefined) - termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); - resultT = accT; + return { + termArgs, + resultT: + terminal === "forEach" ? VOID : terminal === "find" ? lowerer.withUndefinedArm(elem) : BOOL, + }; } + // reduce(f[, initial]) + const fNode = call.arguments[0]; + if ( + call.arguments.length < 1 || + call.arguments.length > 2 || + fNode === undefined || + call.arguments.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); + } + // The accumulator binds the checker's result type. + const accLead = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? elem; + const f = lowerArrayCallback(lowerer, fNode, [accLead, elem], UNDEFINED_T).fnArg; + if ( + f.type.kind !== "func" || + f.type.params.length < 2 || + f.type.params.length > 3 || + !typeEquals(f.type.params[1]!, elem) || + (f.type.params.length === 3 && f.type.params[2]!.kind !== "f64") || + !typeEquals(f.type.params[0]!, f.type.ret) + ) { + lowerer.badType(fNode, lowerer.typeOf(fNode)); + } + const accT = (f.type as IrType & { kind: "func" }).ret; + if (call.arguments.length === 1 && !typeEquals(accT, elem)) { + lowerer.noLowering( + ".reduce without an initial value where the accumulator's type differs from the elements'", + call, + "pass the initial value: .reduce(f, init)", + ); + } + termArgs.push(f); + if (call.arguments[1] !== undefined) + termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); + return { termArgs, resultT: accT }; +} + +/** A terminal over a chain of stages from `source`: one interned fused + * function, called with the source, each stage's budget or callback and + * the terminal's arguments (lowered in that, the chain's own, order). */ +function lowerFusedChain( + lowerer: Lowerer, + call: ts.CallExpression, + terminal: string, + stages: StageNode[], + source: ChainSource, + items: IrExpr, + seed: IrType, +): IrExpr { + const loc = locOf(call); + if (source.kind === "generator" && !stepDrivable(source.type)) { + lowerer.noLowering( + `.${terminal} over an iterator yielding '${lowerer.fmt(source.type.yieldT)}'`, + call, + "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", + ); + } + // A take/drop budget check closes a generator source, so the source is + // bound before the stage arguments. + const bound = + source.kind === "generator" && stages.some((s) => s.name === "take" || s.name === "drop") + ? bindSource(lowerer, items as IrExpr & { type: GenType }, loc) + : null; + if (bound !== null) items = bound.ref; + let elem = seed; + const stageIrs: StageIr[] = []; + for (const s of stages) { + const lowered = lowerStage(lowerer, s, elem, loc, bound?.ref ?? null); + stageIrs.push(lowered.stage); + elem = lowered.out; + } + const { termArgs, resultT } = lowerTerminal(lowerer, call, terminal, elem); // The checker's own result type must agree (annotation drift fences). const checkerT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (terminal !== "forEach" && (checkerT === null || !typeEquals(checkerT, resultT))) { @@ -231,11 +705,14 @@ export function lowerIteratorHelperCall( "the chain's element and result types must be representable — annotate the callbacks' types", ); } - const stageKeys = stageIrs.map((s) => { - if (!s) throw new InternalCompilerError("missing iterator stage"); - return "budget" in s ? s.kind : `${s.kind}:${typeKey(s.fn.type)}`; - }); - const key = `iter:${srcProj}:${typeKey(items.type)}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; + const stageKeys = stageIrs.map((s) => + "budget" in s ? s.kind : `${s.kind}:${typeKey(s.fn.type)}`, + ); + const srcKey = + source.kind === "generator" + ? `${typeKey(source.type)}:${typeKey(source.elem)}` + : `${source.proj}:${typeKey(source.type)}`; + const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; @@ -244,8 +721,7 @@ export function lowerIteratorHelperCall( buildIterChainFn( lowerer, helper, - items.type, - srcProj, + source, stageIrs, terminal, termArgs.map((a) => a.type), @@ -254,35 +730,67 @@ export function lowerIteratorHelperCall( ), ); } - const budgetOrFn = stageIrs.map((s): IrExpr => { - if (!s) throw new InternalCompilerError("missing iterator stage"); - return "budget" in s ? s.budget : s.fn; - }); - return { + const budgetOrFn = stageIrs.map((s): IrExpr => ("budget" in s ? s.budget : s.fn)); + const fused: IrExpr = { kind: "call", callee: helper, args: [items, ...budgetOrFn, ...termArgs], type: resultT, loc, }; + return bound === null + ? fused + : { kind: "seqExpr", stmts: [bound.decl], result: fused, type: resultT, loc }; } -const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); -const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); - -/** The ts.PropertyAccessExpression of stage `s` inside the chain under - * `call` — for the stdlib-membership check. Walks the same spine the - * parser walked; null only on shape drift (checked defensively). */ -function findStageAccess( +/** An iterator helper object for one stage over `src`: an interned + * generator `%iter..N(src, f | n)` typed as the call's checker + * type (IteratorObject). */ +function lowerHelperObject( + lowerer: Lowerer, call: ts.CallExpression, - s: { name: string; argNode: ts.Expression }, -): ts.PropertyAccessExpression | null { - let cur: ts.Expression = (call.expression as ts.PropertyAccessExpression).expression; - while (ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) { - if (cur.arguments[0] === s.argNode) return cur.expression; - cur = cur.expression.expression; + src: IrExpr & { type: GenType }, + srcElem: IrType, + stage: StageIr, + out: IrType, + loc: SrcLoc, +): IrExpr { + const outT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const resultType = + outT?.kind === "generator" + ? genResultRecord(outT.yieldT, outT.retT, lowerer.shapes, lowerer.unions) + : null; + if ( + outT?.kind !== "generator" || + outT.async === true || + !typeEquals(outT.yieldT, out) || + outT.retT.kind !== "void" || + resultType === null + ) { + lowerer.noLowering( + `.${stage.kind} at this result type`, + call, + "the helper's element type must be representable — annotate the callback's types", + ); } - return null; + if (!stepDrivable(src.type)) { + lowerer.noLowering( + `.${stage.kind} over an iterator yielding '${lowerer.fmt(src.type.yieldT)}'`, + call, + "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", + ); + } + const arg = "budget" in stage ? stage.budget : stage.fn; + const key = `iterHelper:${stage.kind}:${typeKey(src.type)}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + lowerer.liftedFns.push( + buildHelperGenFn(lowerer, name, src.type, srcElem, stage, arg.type, outT, resultType, loc), + ); + } + return { kind: "call", callee: name, args: [src, arg], type: outT, loc }; } /** The eager take/drop budget check, interned once per program: @@ -292,11 +800,11 @@ function findStageAccess( * Node's exact behavior: NaN and negative INTEGER budgets throw (the * message carries the RAW argument — take(-1.5) says "-1.5"), -0.5 * truncates to 0 and passes, Infinity passes through. */ -function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { - const key = "iter:budget"; +function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null): string { + const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = "%iter.budget"; + name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); @@ -351,13 +859,52 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { }, loc, }; + // The source closes first; an error from its return() is dropped, so + // the RangeError is what propagates. + const closeSource: IrStmt[] = + srcT === null + ? [] + : [ + { + kind: "tryCatch", + tryBody: [ + { + kind: "exprStmt", + expr: { + kind: "genResume", + mode: "close", + gen: varRef("src.0", srcT, loc), + arg: null, + type: VOID, + loc, + }, + loc, + }, + ], + catchBody: [], + catchLocalId: "exc.0", + finallyBody: null, + loc, + }, + ]; lowerer.liftedFns.push({ name, - params: [{ localId: "v.0", name: "v", type: F64 }], + params: [ + { localId: "v.0", name: "v", type: F64 }, + ...(srcT === null ? [] : [{ localId: "src.0", name: "src", type: srcT }]), + ], returnType: F64, - locals: [{ id: "v.0", name: "v", type: F64, mutable: true }], + locals: [ + { id: "v.0", name: "v", type: F64, mutable: true }, + ...(srcT === null + ? [] + : [ + { id: "src.0", name: "src", type: srcT, mutable: false }, + { id: "exc.0", name: "exc", type: CAUGHT, mutable: false }, + ]), + ], body: [ - { kind: "if", cond: bad, then: [throwStmt], else_: null, loc }, + { kind: "if", cond: bad, then: [...closeSource, throwStmt], else_: null, loc }, { kind: "return", value: t(), loc }, ], loc, @@ -365,97 +912,30 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc): string { return name; } -/** The fused chain body. One loop over the LIVE source (length re-read - * per pass), a `done` flag every loop condition carries (take closing - * the pipeline, short-circuit terminals), stage code nested inside in - * source order, flatMap as an inner loop over its returned array. - * Pre-loop, a zero take budget marks done immediately — Node pulls - * nothing at all through a take(0). */ -function buildIterChainFn( +/** A hidden local holding `gen`, and its declaration. */ +function bindSource( lowerer: Lowerer, - name: string, - itemsT: IrType & { kind: "array" }, - srcProj: "values" | "entries", - stages: { kind: string; fn?: IrExpr & { type: IrType & { kind: "func" } } }[], - terminal: string, - termArgTs: IrType[], - resultT: IrType, + gen: IrExpr & { type: GenType }, loc: SrcLoc, -): IrFunction { - const locals: IrLocal[] = [{ id: "items.0", name: "items", type: itemsT, mutable: true }]; - const params: IrParam[] = [{ localId: "items.0", name: "items", type: itemsT }]; - let n = 0; - const addLocal = (base: string, type: IrType, mutable: boolean): string => { - const id = `${base}.${n++}`; - locals.push({ id, name: base, type, mutable }); - return id; +): { ref: IrExpr & { type: GenType }; decl: IrStmt } { + const local = lowerer.declareHiddenLocal("%helperSource", gen.type); + return { + ref: varRef(local.id, gen.type, loc) as IrExpr & { type: GenType }, + decl: { kind: "varDecl", localId: local.id, init: gen, loc }, }; - const addParam = (base: string, type: IrType): string => { - const id = `${base}.${n++}`; - locals.push({ id, name: base, type, mutable: true }); - params.push({ localId: id, name: base, type }); - return id; - }; - const doneId = addLocal("done", BOOL, true); - const done = (): IrExpr => varRef(doneId, BOOL, loc); - const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; - // Parameters in call order: per-stage budget/callback, then terminal's. - interface StageSlot { - kind: string; - paramId: string; - fnT?: (IrType & { kind: "func" }) | undefined; - cntId?: string | undefined; - elem: IrType; - } - // The entries() seed builds each pass's [index, element] pair — the - // same interned tuple the caller computed its stage types against. - const seedT: IrType = - srcProj === "entries" - ? { - kind: "record", - shapeId: lowerer.shapes.intern( - [ - { name: "0", type: F64 }, - { name: "1", type: itemsT.elem }, - ], - true, - ), - } - : itemsT.elem; - let elem: IrType = seedT; - const slots: StageSlot[] = []; - for (const s of stages) { - if (s.kind === "take" || s.kind === "drop") { - const paramId = addParam("n", F64); - const cntId = addLocal(s.kind === "take" ? "taken" : "dropped", F64, true); - slots.push({ kind: s.kind, paramId, cntId, elem }); - continue; - } - const fnT = s.fn!.type; - const paramId = addParam("f", fnT); - const cntId = fnT.params.length === 2 ? addLocal("c", F64, true) : undefined; - const out = - s.kind === "map" - ? fnT.ret - : s.kind === "flatMap" - ? (fnT.ret as IrType & { kind: "array" }).elem - : elem; - slots.push({ kind: s.kind, paramId, fnT, cntId, elem: out }); - elem = out; - } - const termIds = termArgTs.map((t, i) => addParam(i === 0 ? "tf" : "init", t)); - const termFnT = - termArgTs[0]?.kind === "func" ? (termArgTs[0] as IrType & { kind: "func" }) : null; - const termCntId = - (terminal === "reduce" && termFnT !== null && termFnT.params.length === 3) || - (terminal !== "reduce" && - terminal !== "toArray" && - termFnT !== null && - termFnT.params.length === 2) || - terminal === "reduce" - ? addLocal("k", F64, true) - : undefined; - const bump = (id: string): IrStmt => ({ +} + +/** Where chain code allocates its locals: a lifted helper function's own + * frame, or hidden locals of the function being lowered (a fused for-of). */ +interface ChainFrame { + local(base: string, type: IrType, mutable: boolean): string; + /** `id = id + 1` over a number local. */ + bump(id: string): IrStmt; +} + +/** `id = id + 1` over a number local. */ +function bumpLocal(id: string, loc: SrcLoc): IrStmt { + return { kind: "assign", localId: id, value: { @@ -467,18 +947,633 @@ function buildIterChainFn( loc, }, loc, - }); - const callWith = ( - fnT: IrType & { kind: "func" }, - fnId: string, - value: IrExpr, - cntId: string | undefined, - ): IrExpr => { - const args: IrExpr[] = []; - if (fnT.params.length >= 1) args.push(value); - if (cntId !== undefined && fnT.params.length >= 2) args.push(varRef(cntId, F64, loc)); - return { kind: "callValue", callee: varRef(fnId, fnT, loc), args, type: fnT.ret, loc }; }; +} + +/** Hidden locals of the function being lowered. */ +class HostFrame implements ChainFrame { + constructor( + private readonly lowerer: Lowerer, + private readonly loc: SrcLoc, + ) {} + local(base: string, type: IrType, mutable: boolean): string { + const local = this.lowerer.declareHiddenLocal(`%${base}`, type); + local.mutable = mutable; + return local.id; + } + bump(id: string): IrStmt { + return bumpLocal(id, this.loc); + } +} + +/** The locals and parameters of a lifted helper function under + * construction. */ +class LiftedFrame implements ChainFrame { + readonly locals: IrLocal[] = []; + readonly params: IrParam[] = []; + private n = 0; + constructor(private readonly loc: SrcLoc) {} + local(base: string, type: IrType, mutable: boolean): string { + const id = `${base}.${this.n++}`; + this.locals.push({ id, name: base, type, mutable }); + return id; + } + param(base: string, type: IrType): string { + const id = `${base}.${this.n++}`; + this.locals.push({ id, name: base, type, mutable: true }); + this.params.push({ localId: id, name: base, type }); + return id; + } + bump(id: string): IrStmt { + return bumpLocal(id, this.loc); + } +} + +/** Statements that drive the synchronous generator in local `genId` the + * way for-of does. Each pass runs `pre` (which may `break`), steps the + * generator, binds the yielded value and runs `body` over it (`continue` + * moves to the next pass). Leaving the loop while the generator is + * unfinished closes it: a `break` in `pre` or `body`, a throw from the + * body, or a `return()` at a yield inside it. Exhaustion and a throwing + * step leave it unclosed, as in Node. The generator counts as unfinished + * before the first step, so a `pre` that breaks at once (take(0)) closes + * a source it never pulled. `genT` must be stepDrivable. */ +function driveGenerator( + lowerer: Lowerer, + frame: ChainFrame, + genId: string, + genT: GenType, + elemT: IrType, + pre: IrStmt[], + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const g = (): IrExpr & { type: GenType } => + varRef(genId, genT, loc) as IrExpr & { type: GenType }; + const openId = frame.local("open", BOOL, true); + const setOpen = (value: boolean): IrStmt => ({ + kind: "assign", + localId: openId, + value: boolLit(value, loc), + loc, + }); + const vId = frame.local("gv", genT.yieldT, false); + // A checked-dynamic yield converts once into the element type. + const converted = typeEquals(elemT, genT.yieldT) + ? null + : lowerer.coerceToExpected(varRef(vId, genT.yieldT, loc), elemT); + if (converted !== null && !typeEquals(converted.type, elemT)) + throw new InternalCompilerError("lowerer bug: helper source element has no conversion"); + const eId = converted === null ? vId : frame.local("ev", elemT, false); + return [ + { kind: "varDecl", localId: openId, init: boolLit(true, loc), loc }, + { + kind: "tryCatch", + tryBody: [ + { + kind: "while", + cond: boolLit(true, loc), + body: [ + ...pre, + setOpen(false), + { + kind: "if", + cond: genStep(g(), loc), + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + setOpen(true), + { kind: "varDecl", localId: vId, init: genValue(g(), loc), loc }, + ...(converted === null + ? [] + : [{ kind: "varDecl" as const, localId: eId, init: converted, loc }]), + ...body(varRef(eId, elemT, loc)), + ], + ...(labels && { labels }), + loc, + }, + ], + catchBody: null, + catchLocalId: null, + // After a throw, an error from closing the source is dropped. + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: varRef(openId, BOOL, loc), + then: [ + { + kind: "exprStmt", + expr: { kind: "genResume", mode: "close", gen: g(), arg: null, type: VOID, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }, + ]; +} + +/** `for (j = 0; j < arr.length && !stop; j++) body(arr[j])` over a live + * array local (the array iterator's per-step length read). */ +function walkArray( + frame: ChainFrame, + arrId: string, + arrT: IrType & { kind: "array" }, + stop: IrExpr | null, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt { + const jId = frame.local("j", F64, true); + const wId = frame.local("w", arrT.elem, false); + const inBounds: IrExpr = { + kind: "bin", + op: "<", + left: varRef(jId, F64, loc), + right: { + kind: "arrIntrinsic", + method: "length", + receiver: varRef(arrId, arrT, loc), + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }; + return { + kind: "for", + init: { kind: "varDecl", localId: jId, init: numLit(0, loc), loc }, + cond: + stop === null + ? inBounds + : { + kind: "logical", + op: "&&", + left: inBounds, + right: { kind: "unary", op: "!", operand: stop, type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(jId), + body: [ + { + kind: "varDecl", + localId: wId, + init: { + kind: "arrayGet", + arr: varRef(arrId, arrT, loc), + index: varRef(jId, F64, loc), + type: arrT.elem, + loc, + }, + loc, + }, + ...body(varRef(wId, arrT.elem, loc)), + ], + loc, + }; +} + +/** The live walk over the Map or Set in local `collId` that for-of does: + * iterEnter, then each live index from iterStart below the current + * iterCount until `stop`, then iterExit on every exit (a throw exits + * before rethrowing). The body sees the projected element. */ +function walkCollection( + lowerer: Lowerer, + frame: ChainFrame, + collId: string, + source: ChainSource & { kind: "collection" }, + stop: IrExpr, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const t = source.type; + const iId = frame.local("i", F64, true); + const iter = (method: string, args: IrExpr[], type: IrType): IrExpr => + t.kind === "map" + ? { + kind: "mapIntrinsic", + method: method as IrMapIntrinsicMethod, + receiver: varRef(collId, t, loc), + args, + type, + loc, + } + : { + kind: "setIntrinsic", + method: method as IrSetIntrinsicMethod, + receiver: varRef(collId, t, loc), + args, + type, + loc, + }; + const keyT = t.kind === "map" ? t.key : t.elem; + const keyRead = (): IrExpr => iter("iterKey", [varRef(iId, F64, loc)], keyT); + const valRead = (): IrExpr => + t.kind === "map" ? iter("iterValue", [varRef(iId, F64, loc)], t.value) : keyRead(); + const seedT = collectionSeed(lowerer, source); + const seed: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: keyRead() }, + { name: "1", value: valRead() }, + ], + type: seedT, + loc, + } + : source.proj === "values" && t.kind === "map" + ? valRead() + : keyRead(); + const vId = frame.local("v", seedT, false); + const exit = (): IrStmt => ({ kind: "exprStmt", expr: iter("iterExit", [], VOID), loc }); + const caughtId = frame.local("exc", CAUGHT, false); + const loop: IrStmt = { + kind: "for", + init: { kind: "varDecl", localId: iId, init: iter("iterStart", [], F64), loc }, + cond: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: iter("iterCount", [], F64), + type: BOOL, + loc, + }, + right: { kind: "unary", op: "!", operand: stop, type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(iId), + body: [ + { + kind: "if", + cond: iter("iterLive", [varRef(iId, F64, loc)], BOOL), + then: [ + { kind: "varDecl", localId: vId, init: seed, loc }, + ...body(varRef(vId, seedT, loc)), + ], + else_: null, + loc, + }, + ], + ...(labels && { labels }), + loc, + }; + return [ + { kind: "exprStmt", expr: iter("iterEnter", [], VOID), loc }, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: [exit(), { kind: "rethrow", localId: caughtId, loc }], + catchLocalId: caughtId, + finallyBody: null, + loc, + }, + exit(), + ]; +} + +/** A flatMap stage's inner walk over the callback's result in local + * `innerId`: an array by index, or a generator driven (and closed on + * early exit) like the source. `stop` ends the walk early. */ +function walkInner( + lowerer: Lowerer, + frame: ChainFrame, + innerId: string, + innerT: IrType, + stop: IrExpr | null, + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt[] { + if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)]; + if (innerT.kind !== "generator") + throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result"); + const pre: IrStmt[] = + stop === null + ? [] + : [{ kind: "if", cond: stop, then: [{ kind: "break", loc }], else_: null, loc }]; + return driveGenerator(lowerer, frame, innerId, innerT, innerT.yieldT, pre, body, loc); +} + +/** A stage inside fused chain code: the local holding its budget or + * callback, and its counter when the callback reads one (take/drop count + * in theirs). */ +interface StageSlot { + kind: string; + argId: string; + fnT?: (IrType & { kind: "func" }) | undefined; + cntId?: string | undefined; +} + +/** A stage callback call with (value) or (value, counter), as declared. */ +function callStage( + fnT: IrType & { kind: "func" }, + fnId: string, + value: IrExpr, + cntId: string | undefined, + loc: SrcLoc, +): IrExpr { + const args: IrExpr[] = []; + if (fnT.params.length >= 1) args.push(value); + if (cntId !== undefined && fnT.params.length >= 2) args.push(varRef(cntId, F64, loc)); + return { kind: "callValue", callee: varRef(fnId, fnT, loc), args, type: fnT.ret, loc }; +} + +/** The slot for `stage` whose budget or callback is in local `argId`. */ +function stageSlot(frame: ChainFrame, stage: StageIr, argId: string): StageSlot { + if ("budget" in stage) { + return { + kind: stage.kind, + argId, + cntId: frame.local(stage.kind === "take" ? "taken" : "dropped", F64, true), + }; + } + const fnT = stage.fn.type; + return { + kind: stage.kind, + argId, + fnT, + cntId: fnT.params.length === 2 ? frame.local("c", F64, true) : undefined, + }; +} + +/** The declarations fused chain code starts with: the `done` flag — + * already true when a take budget is zero, so nothing pulls — and each + * stage counter at zero. */ +function stagePrologue(slots: StageSlot[], doneId: string, loc: SrcLoc): IrStmt[] { + let doneInit: IrExpr = boolLit(false, loc); + for (const s of slots) { + if (s.kind !== "take") continue; + const isZero: IrExpr = { + kind: "bin", + op: "===", + left: varRef(s.argId, F64, loc), + right: numLit(0, loc), + type: BOOL, + loc, + }; + doneInit = + doneInit.kind === "boolLit" + ? isZero + : { kind: "logical", op: "||", left: doneInit, right: isZero, type: BOOL, loc }; + } + const out: IrStmt[] = [{ kind: "varDecl", localId: doneId, init: doneInit, loc }]; + for (const s of slots) { + if (s.cntId !== undefined) + out.push({ kind: "varDecl", localId: s.cntId, init: numLit(0, loc), loc }); + } + return out; +} + +/** The per-element code of a chain: each stage nested in source order + * around `last` (the terminal's statements, or a for-of body), folded + * from the end outward. filter and drop skip with `continue`; take spends + * its budget before delivery and sets `done`, so the nth element flows on + * and nothing pulls after it (every pass tests done); flatMap walks its + * callback's array or generator in an inner loop that also tests done. */ +function foldStages( + lowerer: Lowerer, + frame: ChainFrame, + slots: StageSlot[], + last: (value: IrExpr) => IrStmt[], + doneId: string, + loc: SrcLoc, +): (value: IrExpr) => IrStmt[] { + const done = (): IrExpr => varRef(doneId, BOOL, loc); + const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; + let emit = last; + for (let si = slots.length - 1; si >= 0; si--) { + const s = slots[si]!; + const inner = emit; + if (s.kind === "map") { + emit = (value) => { + const vId = frame.local("m", s.fnT!.ret, false); + return [ + { + kind: "varDecl", + localId: vId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + ...inner(varRef(vId, s.fnT!.ret, loc)), + ]; + }; + } else if (s.kind === "filter") { + emit = (value) => { + const okId = frame.local("ok", BOOL, false); + return [ + { + kind: "varDecl", + localId: okId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(okId, BOOL, loc), type: BOOL, loc }, + then: [{ kind: "continue", loc }], + else_: null, + loc, + }, + ...inner(value), + ]; + }; + } else if (s.kind === "drop") { + emit = (value) => [ + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(s.cntId!, F64, loc), + right: varRef(s.argId, F64, loc), + type: BOOL, + loc, + }, + then: [frame.bump(s.cntId!), { kind: "continue", loc }], + else_: null, + loc, + }, + ...inner(value), + ]; + } else if (s.kind === "take") { + emit = (value) => [ + frame.bump(s.cntId!), + { + kind: "if", + cond: { + kind: "bin", + op: ">=", + left: varRef(s.cntId!, F64, loc), + right: varRef(s.argId, F64, loc), + type: BOOL, + loc, + }, + then: [setDone], + else_: null, + loc, + }, + ...inner(value), + ]; + } else { + emit = (value) => { + const innerT = s.fnT!.ret; + const innerId = frame.local("fm", innerT, false); + return [ + { + kind: "varDecl", + localId: innerId, + init: callStage(s.fnT!, s.argId, value, s.cntId, loc), + loc, + }, + ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), + ...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc), + ]; + }; + } + } + return emit; +} + +/** The loop over a chain's source in local `itemsId`, running `emit` per + * element until `done`: a live array (length re-read per pass), a Map or + * Set walked live, or a generator driven like for-of (closed when the + * loop leaves it unfinished). `labels` name the loop for a fused for-of + * body's labeled jumps. */ +function sourceLoop( + lowerer: Lowerer, + frame: ChainFrame, + itemsId: string, + source: ChainSource, + doneId: string, + emit: (value: IrExpr) => IrStmt[], + loc: SrcLoc, + labels?: string[], +): IrStmt[] { + const done = (): IrExpr => varRef(doneId, BOOL, loc); + if (source.kind === "collection") + return walkCollection(lowerer, frame, itemsId, source, done(), emit, loc, labels); + if (source.kind === "generator") { + return driveGenerator( + lowerer, + frame, + itemsId, + source.type, + source.elem, + [{ kind: "if", cond: done(), then: [{ kind: "break", loc }], else_: null, loc }], + emit, + loc, + labels, + ); + } + const itemsT = source.type; + const seedT = source.proj === "entries" ? entryPairType(lowerer, itemsT.elem) : itemsT.elem; + const iId = frame.local("i", F64, true); + const vId = frame.local("v", seedT, false); + const elemRead = (): IrExpr => ({ + kind: "arrayGet", + arr: varRef(itemsId, itemsT, loc), + index: varRef(iId, F64, loc), + type: itemsT.elem, + loc, + }); + const seedInit: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: varRef(iId, F64, loc) }, + { name: "1", value: elemRead() }, + ], + type: seedT, + loc, + } + : elemRead(); + return [ + { + kind: "for", + init: { kind: "varDecl", localId: iId, init: numLit(0, loc), loc }, + cond: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: { + kind: "arrIntrinsic", + method: "length", + receiver: varRef(itemsId, itemsT, loc), + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, + type: BOOL, + loc, + }, + update: frame.bump(iId), + body: [ + { kind: "varDecl", localId: vId, init: seedInit, loc }, + ...emit(varRef(vId, seedT, loc)), + ], + ...(labels && { labels }), + loc, + }, + ]; +} + +/** The fused chain function: the source, each stage's budget or callback + * and the terminal's arguments as parameters; one loop over the source + * with the stages nested in it and the terminal's statements innermost. + * A zero take budget marks done before the first pass: Node pulls nothing + * through a take(0), and closes a generator source. */ +function buildIterChainFn( + lowerer: Lowerer, + name: string, + source: ChainSource, + stages: StageIr[], + terminal: string, + termArgTs: IrType[], + resultT: IrType, + loc: SrcLoc, +): IrFunction { + const frame = new LiftedFrame(loc); + const itemsId = frame.param("items", source.type); + const doneId = frame.local("done", BOOL, true); + const setDone: IrStmt = { kind: "assign", localId: doneId, value: boolLit(true, loc), loc }; + // Parameters in call order: per-stage budget/callback, then terminal's. + const slots = stages.map((s) => + stageSlot(frame, s, frame.param("budget" in s ? "n" : "f", "budget" in s ? F64 : s.fn.type)), + ); + const termIds = termArgTs.map((t, i) => frame.param(i === 0 ? "tf" : "init", t)); + const termFnT = + termArgTs[0]?.kind === "func" ? (termArgTs[0] as IrType & { kind: "func" }) : null; + const termCntId = + (terminal === "reduce" && termFnT !== null && termFnT.params.length === 3) || + (terminal !== "reduce" && + terminal !== "toArray" && + termFnT !== null && + termFnT.params.length === 2) || + terminal === "reduce" + ? frame.local("k", F64, true) + : undefined; // Terminal state. const preLoop: IrStmt[] = []; const postLoop: IrStmt[] = []; @@ -487,7 +1582,7 @@ function buildIterChainFn( let hasAccId = ""; let resId = ""; if (terminal === "toArray") { - outId = addLocal("out", resultT, false); + outId = frame.local("out", resultT, false); preLoop.push({ kind: "varDecl", localId: outId, @@ -496,7 +1591,7 @@ function buildIterChainFn( }); postLoop.push({ kind: "return", value: varRef(outId, resultT, loc), loc }); } else if (terminal === "some" || terminal === "every") { - resId = addLocal("res", BOOL, true); + resId = frame.local("res", BOOL, true); preLoop.push({ kind: "varDecl", localId: resId, @@ -505,14 +1600,14 @@ function buildIterChainFn( }); postLoop.push({ kind: "return", value: varRef(resId, BOOL, loc), loc }); } else if (terminal === "find") { - resId = addLocal("res", resultT, true); + resId = frame.local("res", resultT, true); const undef = lowerer.wrappedUndefined(resultT, loc); if (undef === null) throw new InternalCompilerError("lowerer bug: find result union lacks undefined"); preLoop.push({ kind: "varDecl", localId: resId, init: undef, loc }); postLoop.push({ kind: "return", value: varRef(resId, resultT, loc), loc }); } else if (terminal === "reduce") { - accId = addLocal("acc", resultT, true); + accId = frame.local("acc", resultT, true); if (termIds.length === 2) { preLoop.push({ kind: "varDecl", @@ -521,7 +1616,7 @@ function buildIterChainFn( loc, }); } else { - hasAccId = addLocal("hasAcc", BOOL, true); + hasAccId = frame.local("hasAcc", BOOL, true); preLoop.push({ kind: "varDecl", localId: hasAccId, init: boolLit(false, loc), loc }); postLoop.push({ kind: "if", @@ -547,28 +1642,7 @@ function buildIterChainFn( } if (termCntId !== undefined) preLoop.push({ kind: "varDecl", localId: termCntId, init: numLit(0, loc), loc }); - // done starts true when any take budget is zero (nothing pulls). - let doneInit: IrExpr = boolLit(false, loc); - for (const s of slots) { - if (s.kind !== "take") continue; - const isZero: IrExpr = { - kind: "bin", - op: "===", - left: varRef(s.paramId, F64, loc), - right: numLit(0, loc), - type: BOOL, - loc, - }; - doneInit = - doneInit.kind === "boolLit" - ? isZero - : { kind: "logical", op: "||", left: doneInit, right: isZero, type: BOOL, loc }; - } - preLoop.unshift({ kind: "varDecl", localId: doneId, init: doneInit, loc }); - for (const s of slots) { - if (s.cntId !== undefined) - preLoop.push({ kind: "varDecl", localId: s.cntId, init: numLit(0, loc), loc }); - } + preLoop.unshift(...stagePrologue(slots, doneId, loc)); // The terminal's per-element statements over `value`. const terminalBody = (value: IrExpr): IrStmt[] => { const out: IrStmt[] = []; @@ -586,9 +1660,13 @@ function buildIterChainFn( loc, }); } else if (terminal === "forEach") { - out.push({ kind: "exprStmt", expr: callWith(termFnT!, termIds[0]!, value, termCntId), loc }); + out.push({ + kind: "exprStmt", + expr: callStage(termFnT!, termIds[0]!, value, termCntId, loc), + loc, + }); } else if (terminal === "some" || terminal === "every") { - const hit = callWith(termFnT!, termIds[0]!, value, termCntId); + const hit = callStage(termFnT!, termIds[0]!, value, termCntId, loc); const cond: IrExpr = terminal === "some" ? hit : { kind: "unary", op: "!", operand: hit, type: BOOL, loc }; out.push({ @@ -610,7 +1688,7 @@ function buildIterChainFn( ); out.push({ kind: "if", - cond: callWith(termFnT!, termIds[0]!, value, termCntId), + cond: callStage(termFnT!, termIds[0]!, value, termCntId, loc), then: [ { kind: "assign", @@ -658,230 +1736,203 @@ function buildIterChainFn( out.push({ kind: "assign", localId: accId, value: reducer(), loc }); } } - if (termCntId !== undefined) out.push(bump(termCntId)); + if (termCntId !== undefined) out.push(frame.bump(termCntId)); return out; }; - // Fold stages from the terminal outward. - let emit: (value: IrExpr) => IrStmt[] = terminalBody; - for (let si = slots.length - 1; si >= 0; si--) { - const s = slots[si]!; - const inner = emit; - if (s.kind === "map") { - emit = (value) => { - const vId = addLocal("m", s.fnT!.ret, false); - return [ - { kind: "varDecl", localId: vId, init: callWith(s.fnT!, s.paramId, value, s.cntId), loc }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - ...inner(varRef(vId, s.fnT!.ret, loc)), - ]; - }; - } else if (s.kind === "filter") { - emit = (value) => { - const okId = addLocal("ok", BOOL, false); - return [ - { - kind: "varDecl", - localId: okId, - init: callWith(s.fnT!, s.paramId, value, s.cntId), - loc, - }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - { - kind: "if", - cond: { kind: "unary", op: "!", operand: varRef(okId, BOOL, loc), type: BOOL, loc }, - then: [{ kind: "continue", loc }], - else_: null, - loc, - }, - ...inner(value), - ]; - }; - } else if (s.kind === "drop") { - emit = (value) => [ - { - kind: "if", - cond: { - kind: "bin", - op: "<", - left: varRef(s.cntId!, F64, loc), - right: varRef(s.paramId, F64, loc), - type: BOOL, - loc, - }, - then: [bump(s.cntId!), { kind: "continue", loc }], - else_: null, - loc, - }, - ...inner(value), - ]; - } else if (s.kind === "take") { - // Budget consumed BEFORE delivery: the nth element flows on with - // done already set, so no later element pulls upstream — and a - // downstream `continue` can't skip the close (the loop conditions - // carry !done). - emit = (value) => [ - bump(s.cntId!), - { - kind: "if", - cond: { - kind: "bin", - op: ">=", - left: varRef(s.cntId!, F64, loc), - right: varRef(s.paramId, F64, loc), - type: BOOL, - loc, - }, - then: [setDone], - else_: null, - loc, - }, - ...inner(value), - ]; - } else { - // flatMap: an inner loop over the returned array, `!done` carried. - emit = (value) => { - const innerT = s.fnT!.ret as IrType & { kind: "array" }; - const arrId = addLocal("fm", innerT, false); - const jId = addLocal("j", F64, true); - const wId = addLocal("w", innerT.elem, false); - return [ - { - kind: "varDecl", - localId: arrId, - init: callWith(s.fnT!, s.paramId, value, s.cntId), - loc, - }, - ...(s.cntId !== undefined ? [bump(s.cntId)] : []), - { - kind: "for", - init: { kind: "varDecl", localId: jId, init: numLit(0, loc), loc }, - cond: { - kind: "logical", - op: "&&", - left: { - kind: "bin", - op: "<", - left: varRef(jId, F64, loc), - right: { - kind: "arrIntrinsic", - method: "length", - receiver: varRef(arrId, innerT, loc), - args: [], - type: F64, - loc, - }, - type: BOOL, - loc, - }, - right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, - type: BOOL, - loc, - }, - update: { - kind: "assign", - localId: jId, - value: { - kind: "bin", - op: "+", - left: varRef(jId, F64, loc), - right: numLit(1, loc), - type: F64, - loc, - }, - loc, - }, - body: [ - { - kind: "varDecl", - localId: wId, - init: { - kind: "arrayGet", - arr: varRef(arrId, innerT, loc), - index: varRef(jId, F64, loc), - type: innerT.elem, - loc, - }, - loc, - }, - ...inner(varRef(wId, innerT.elem, loc)), - ], - loc, - }, - ]; - }; - } - } - const iId = addLocal("i", F64, true); - const vId = addLocal("v", seedT, false); - const elemRead = (): IrExpr => ({ - kind: "arrayGet", - arr: varRef("items.0", itemsT, loc), - index: varRef(iId, F64, loc), - type: itemsT.elem, - loc, - }); - const seedInit: IrExpr = - srcProj === "entries" - ? { - kind: "recordLit", - fields: [ - { name: "0", value: varRef(iId, F64, loc) }, - { name: "1", value: elemRead() }, - ], - type: seedT, - loc, - } - : elemRead(); - const loop: IrStmt = { - kind: "for", - init: { kind: "varDecl", localId: iId, init: numLit(0, loc), loc }, - cond: { - kind: "logical", - op: "&&", - left: { - kind: "bin", - op: "<", - left: varRef(iId, F64, loc), - right: { - kind: "arrIntrinsic", - method: "length", - receiver: varRef("items.0", itemsT, loc), - args: [], - type: F64, - loc, - }, - type: BOOL, - loc, - }, - right: { kind: "unary", op: "!", operand: done(), type: BOOL, loc }, - type: BOOL, - loc, - }, - update: { - kind: "assign", - localId: iId, - value: { - kind: "bin", - op: "+", - left: varRef(iId, F64, loc), - right: numLit(1, loc), - type: F64, - loc, - }, - loc, - }, + const emit = foldStages(lowerer, frame, slots, terminalBody, doneId, loc); + return { + name, + params: frame.params, + returnType: resultT, + locals: frame.locals, body: [ - { kind: "varDecl", localId: vId, init: seedInit, loc }, - ...emit(varRef(vId, seedT, loc)), + ...preLoop, + ...sourceLoop(lowerer, frame, itemsId, source, doneId, emit, loc), + ...postLoop, ], loc, }; +} + +/** An iterator helper object's generator for one stage over `src`: + * + * map: for (v of src) yield f(v, c++) + * filter: for (v of src) if (f(v, c++)) yield v + * take: for (;;) { if (rem === 0) { close src; return } rem--; pull; yield v } + * drop: for (v of src) { if (dropped < n) { dropped++; continue } yield v } + * flatMap: for (v of src) for (w of f(v, c++)) yield w + * + * The source drive closes `src` on every early exit, including a + * `return()` at one of the yields; `closesSource` closes it on a + * `return()` before the first `next()`. */ +function buildHelperGenFn( + lowerer: Lowerer, + name: string, + srcT: GenType, + srcElem: IrType, + stage: StageIr, + argT: IrType, + outT: GenType, + resultType: IrType & { kind: "record" }, + loc: SrcLoc, +): IrFunction { + const frame = new LiftedFrame(loc); + const srcId = frame.param("src", srcT); + const argId = frame.param(stage.kind === "take" || stage.kind === "drop" ? "n" : "f", argT); + const yieldOf = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "yieldExpr", + value, + type: outT.nextT.kind === "undefinedT" ? VOID : outT.nextT, + loc, + }, + loc, + }); + const fnT = argT.kind === "func" ? argT : null; + const counted = fnT !== null && fnT.params.length === 2; + const cntId = counted ? frame.local("c", F64, true) : null; + const callF = (value: IrExpr): IrExpr => { + const args: IrExpr[] = []; + if (fnT!.params.length >= 1) args.push(value); + if (cntId !== null) args.push(varRef(cntId, F64, loc)); + return { kind: "callValue", callee: varRef(argId, fnT!, loc), args, type: fnT!.ret, loc }; + }; + const body: IrStmt[] = []; + if (cntId !== null) body.push({ kind: "varDecl", localId: cntId, init: numLit(0, loc), loc }); + const bumpCount = (): IrStmt[] => (cntId !== null ? [frame.bump(cntId)] : []); + const n = (): IrExpr => varRef(argId, F64, loc); + if (stage.kind === "map") { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const mId = frame.local("m", fnT!.ret, false); + return [ + { kind: "varDecl", localId: mId, init: callF(value), loc }, + ...bumpCount(), + yieldOf(varRef(mId, fnT!.ret, loc)), + ]; + }, + loc, + ), + ); + } else if (stage.kind === "filter") { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const okId = frame.local("ok", BOOL, false); + return [ + { kind: "varDecl", localId: okId, init: callF(value), loc }, + ...bumpCount(), + { kind: "if", cond: varRef(okId, BOOL, loc), then: [yieldOf(value)], else_: null, loc }, + ]; + }, + loc, + ), + ); + } else if (stage.kind === "take") { + // The budget is spent before each pull; a spent budget closes the + // source on the next pass instead of pulling. + const pre: IrStmt[] = [ + { + kind: "if", + cond: { kind: "bin", op: "===", left: n(), right: numLit(0, loc), type: BOOL, loc }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + { + kind: "assign", + localId: argId, + value: { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc }, + loc, + }, + ]; + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + pre, + (value) => [yieldOf(value)], + loc, + ), + ); + } else if (stage.kind === "drop") { + const droppedId = frame.local("dropped", F64, true); + body.push({ kind: "varDecl", localId: droppedId, init: numLit(0, loc), loc }); + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => [ + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(droppedId, F64, loc), + right: n(), + type: BOOL, + loc, + }, + then: [frame.bump(droppedId), { kind: "continue", loc }], + else_: null, + loc, + }, + yieldOf(value), + ], + loc, + ), + ); + } else { + body.push( + ...driveGenerator( + lowerer, + frame, + srcId, + srcT, + srcElem, + [], + (value) => { + const innerT = fnT!.ret; + const innerId = frame.local("fm", innerT, false); + return [ + { kind: "varDecl", localId: innerId, init: callF(value), loc }, + ...bumpCount(), + ...walkInner(lowerer, frame, innerId, innerT, null, (w) => [yieldOf(w)], loc), + ]; + }, + loc, + ), + ); + } + body.push({ kind: "return", value: null, loc }); return { name, - params, - returnType: resultT, - locals, - body: [...preLoop, loop, ...postLoop], + params: frame.params, + returnType: VOID, + locals: frame.locals, + body, + generator: { yieldT: outT.yieldT, nextT: outT.nextT, resultType, closesSource: true }, loc, }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 9ae66f859..a936b9c78 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -83,7 +83,10 @@ import { import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js"; import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js"; import { lowerGroupByStaticCall } from "./containers/collection-grouping.js"; -import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js"; +import { + lowerGeneratorHelperCall, + lowerIteratorHelperCall, +} from "./containers/iterator-helpers.js"; import { lowerObjectAssignIndexShape, lowerObjectConstructor, @@ -1425,6 +1428,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // array method paths (the terminal names collide with array // methods, but only iterator-typed receivers reach this). lowerIteratorHelperCall(lowerer, expr, expr.expression) ?? + // Helper calls on generators and other iterator objects. + lowerGeneratorHelperCall(lowerer, expr, expr.expression) ?? lowerIteratorStaticFence(lowerer, expr, expr.expression) ?? lowerObjectStaticCall(lowerer, expr, expr.expression) ?? lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ?? @@ -3078,8 +3083,9 @@ function lowerOptionalStringNumber( return { kind: "call", callee: helper, args: [arg], type: F64, loc }; } -/** Iterator types whose helper calls have no lowering on any path (the - * array iterator's chains lower in lowerIteratorHelperCall). */ +/** Iterator types with checked runtime receivers: Map/Set iterators' + * helper calls lower in lowerGeneratorHelperCall; the string iterator's + * have no lowering. */ const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]); const ITERATOR_HELPER_METHODS = new Set([ "map", @@ -3146,6 +3152,8 @@ function lowerDynReceiverMethodCall( lowerer.isStdlibMember(access) ) { if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") { + const helper = lowerGeneratorHelperCall(lowerer, call, access); + if (helper) return helper; lowerer.noLowering( `collection iterator .${access.name.text}()`, call, @@ -3156,7 +3164,7 @@ function lowerDynReceiverMethodCall( lowerer.noLowering( `${recvSymbol.name}.${access.name.text}`, call, - "iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values()", + "iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()", ); } const arrayReceiver = lowerer.checker.isArrayType(recvTs); @@ -6546,9 +6554,9 @@ function recordAssignHelper( } /** The `Iterator` global's statics (ES2025 — Iterator.from, and the - * abstract constructor as a value): no first-class iterator objects - * exist here, so every member fences with the working spelling named - * instead of the generic-method fence's monomorphization wording. */ + * abstract constructor as a value) have no lowering: every member fences + * with the working spelling named instead of the generic-method fence's + * monomorphization wording. */ function lowerIteratorStaticFence( lowerer: Lowerer, call: ts.CallExpression, @@ -6559,8 +6567,8 @@ function lowerIteratorStaticFence( lowerer.noLowering( `Iterator.${access.name.text}`, call, - "first-class iterator objects have no lowering — iterator helpers compile as one chain on an " + - "array iterator, consumed in place: arr.values().map(f).take(n).toArray()", + "iterator helpers apply directly to generators and to array, Map and Set iterators: " + + "arr.values().map(f).take(n).toArray()", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 2eda4c4ce..a9b467a6a 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -115,6 +115,16 @@ export function lowerGenMethodCall( if (call.arguments.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments"); } + // Iterator helpers and the built-in iterators have no throw method; an + // iterator-typed value may hold one, so only a generator's own member + // lowers. + if (name === "throw" && !isGeneratorMember(lowerer, access)) { + lowerer.noLowering( + `${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.throw`, + call, + "throw() lowers on values typed as generators (iterator helpers and built-in iterators have no throw method)", + ); + } const loc = locOf(call); const gen = lowerer.lowerExpr(access.expression); if (gen.type.kind !== "generator") return null; @@ -187,6 +197,23 @@ export function lowerGenMethodCall( return { kind: "genResume", mode: name, gen, arg, type: resultT, loc }; } +/** Whether `access` names a member the lib declares on Generator or + * AsyncGenerator itself. */ +function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + const member = lowerer.checker.getSymbolAtLocation(access.name); + return ( + member !== undefined && + lowerer.checker + .declarationsOf(member) + .some( + (d) => + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && + lowerer.isStdlibFile(d.getSourceFile()), + ) + ); +} + /** Extraction of a suspended resume's yield value out of the result * record's V slot, typed back to the yield channel: V IS the channel for * dyn (and for a union channel already equal to V); a non-union channel @@ -319,7 +346,7 @@ export function genValue(gen: IrExpr & { type: GenType }, loc: SrcLoc): IrExpr { return { kind: "genResume", mode: "value", gen, arg: null, type: gen.type.yieldT, loc }; } -function generatorLoopBinding( +export function generatorLoopBinding( lowerer: Lowerer, stmt: ts.ForOfStatement, value: IrExpr, @@ -572,7 +599,9 @@ export function lowerForOfGenerator( catchBody: null, catchLocalId: null, // IteratorClose: every exit that leaves the generator unfinished - // closes it — finallys run; the .return() result is dropped. + // closes it — finallys run; the .return() result is dropped, and + // after a throw from the body an error from the close is too. + dropFinallyErrors: true, finallyBody: [ { kind: "if", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 7f3be4d44..b8ccd958d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -97,6 +97,7 @@ import { lowerForOfSearchParams, lowerForOfSet, } from "./containers/for-of.js"; +import { lowerForOfHelperChain } from "./containers/iterator-helpers.js"; import { objectIterOverIndexShape } from "./containers/indexed-objects.js"; import { strCharsCall } from "./containers/array-construction.js"; import { @@ -8303,6 +8304,12 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } } } + // A head that is a chain of iterator-helper stages runs as one loop + // over the chain's source. + { + const fused = lowerForOfHelperChain(lowerer, stmt, labels); + if (fused) return fused; + } // `for (const k of m.keys())` / `.values()` / `.entries()`: the // iterator methods consumed DIRECTLY by a for-of head ride the // container's live walk — exactly `for..of m` with the projection diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c6e70577c..417fb5b52 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3224,7 +3224,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isStdlibInterface("IterableIterator") || // An Iterator annotation holds a single-pass iterator object: a // generator, or a native iterator adapted into one. - isStdlibInterface("Iterator") + isStdlibInterface("Iterator") || + // IteratorObject is what the iterator helpers return: a helper + // generator over its source (containers/iterator-helpers.ts). + isStdlibInterface("IteratorObject") ) { const args = checker.getTypeArguments(widened as ts.TypeReference); const channels = genChannels(args[0], args[1], args[2], ctx); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..a77ea2e4b 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1651,7 +1651,15 @@ export interface IrFunction { * out, `nextT` what `.next(v)` sends in (the yield expression's result * type). `async` alongside this field marks an async generator: its * resume methods queue requests and return promises. */ - generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } }; + generator?: { + yieldT: IrType; + nextT: IrType; + resultType: IrType & { kind: "record" }; + /** The first parameter is a generator this one reads from (an + * iterator helper's source): `.return()` before the first `.next()` + * closes it. */ + closesSource?: true; + }; /** Source function name for native error stack frames. */ sourceName?: string; /** Ordinary authored function declarations/expressions own a prototype. */ @@ -1924,6 +1932,11 @@ export type IrStmt = /** Resource-management cleanup: if the guarded body and cleanup both * throw, raise a SuppressedError instead of replacing the first error. */ suppressFinallyErrors?: true; + /** Iterator closing: if the guarded body threw and the cleanup throws + * too, the cleanup's error is dropped and the first one propagates. + * After a generator .return() the cleanup's error replaces it, and on + * normal exits it propagates. */ + dropFinallyErrors?: true; loc: SrcLoc; }; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 6dc871976..aad2e4861 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4464,7 +4464,7 @@ "kind": "stdlib", "name": "Map.prototype.entries", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.map.forEach", @@ -4489,7 +4489,7 @@ "kind": "stdlib", "name": "Map.prototype.keys", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.map.set", @@ -4502,7 +4502,7 @@ "kind": "stdlib", "name": "Map.prototype.values", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.math.E", @@ -5371,7 +5371,7 @@ "kind": "stdlib", "name": "Set.prototype.entries", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.set.forEach", @@ -5418,7 +5418,7 @@ "kind": "stdlib", "name": "Set.prototype.keys", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.set.symmetricDifference", @@ -5439,7 +5439,7 @@ "kind": "stdlib", "name": "Set.prototype.values", "status": "static", - "note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused" + "note": "live native iterators support next(), for...of, drains, and the iterator helper methods" }, { "id": "stdlib.sharedArrayBuffer", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json new file mode 100644 index 000000000..5aa23f5d9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5908-iterator-helper-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5908-iterator-helper-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json new file mode 100644 index 000000000..fd3454670 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5909-iterator-helper-objects-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5909-iterator-helper-objects-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 48f3a5329..f630ee5f9 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4890,6 +4890,8 @@ struct ScrGen { /* The never-started teardown: drops the packed (already-retained) * arguments the spawn wrapper built. Emitted per generator function. */ void (*drop_args)(void *); + /* scr_gen_set_unstarted_return's hook; NULL for most generators. */ + void (*unstarted_return)(void *); bool is_async; void (*settle_async)(ScrGen *, ScrPromise *); ScrAsyncGenRequest *requests_head; @@ -5449,6 +5451,8 @@ void scr_gen_resume(ScrGen *g) { } } +void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; } + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); @@ -5483,13 +5487,16 @@ void scr_gen_resume_return(ScrGen *g) { return; case SCR_GEN_UNSTARTED: /* The body never runs: tear the fiber down cleanly (drop the packed - * arguments) and complete with the parked value. */ + * arguments) and complete with the parked value. The generator is + * done before the hook runs, so a resume from inside it answers done; + * an exception the hook raises stays pending for the caller. */ + g->state = SCR_GEN_DONE; + if (g->unstarted_return != NULL) g->unstarted_return(g->fiber->argpack); if (g->drop_args != NULL) g->drop_args(g->fiber->argpack); else free(g->fiber->argpack); scr_fiber_destroy(g->fiber); g->fiber = NULL; scr_fibers_live--; - g->state = SCR_GEN_DONE; scr_gen_ret_to_out(g); return; default: diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index fb152e078..3a79cf85b 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -389,6 +389,17 @@ void scr_exc_suppressed_forget(const void *error_object) { } } +void scr_exc_keep_first(ScrCaught *first) { + if (first->kind == SCR_EXC_GENRET) { + scr_caught_release(first); + return; + } + ScrCaught *dropped = scr_exc_take(); + if (dropped) scr_caught_release(dropped); + scr_rethrow(first); + scr_caught_release(first); +} + void scr_exc_suppress(ScrCaught *suppressed) { if (suppressed->kind == SCR_EXC_GENRET) { /* A generator .return() is a return completion, not a thrown error. diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h index 80fb98957..29460a1b9 100644 --- a/packages/runtime/src/scr_exception.h +++ b/packages/runtime/src/scr_exception.h @@ -172,6 +172,10 @@ void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx); * when scr_json.c builds its dyn view, and scr_exc_suppressed_forget * releases them. */ void scr_exc_suppress(ScrCaught *suppressed); +/* An iterator close that threw while `first` was propagating: the close's + * error is dropped and `first` is raised again (consumed). A generator + * .return() in flight is replaced by the close's error instead. */ +void scr_exc_keep_first(ScrCaught *first); bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error, const ScrCaught **suppressed); void scr_exc_suppressed_forget(const void *error_object); diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 2c2c0e953..f0ecb9d85 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -60,6 +60,9 @@ void scr_gen_coro_finish(ScrFiber *self); ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, void (*drop_args)(void *), void (*settle)(ScrGen *, ScrPromise *)); +/* Runs when return() reaches the generator before its first resume, with + * the packed arguments still alive: an iterator helper closes its source. */ +void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/tests/corpus/5908-iterator-helper-objects.ts b/tests/corpus/5908-iterator-helper-objects.ts new file mode 100644 index 000000000..5e57b9ddf --- /dev/null +++ b/tests/corpus/5908-iterator-helper-objects.ts @@ -0,0 +1,214 @@ +// Iterator helpers over generators, stored array iterators and Map/Set +// iterators: chains that end in a consuming method, helper objects driven +// by next(), for...of, spreads, Array.from and yield*, and the points at +// which a helper closes its source (runs its return()). +const log: string[] = []; +function* count(label: string, n: number): Generator { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what: unknown): void { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// Consuming methods over generator chains. +flush(count("a", 5).map((x) => x * 10).filter((x) => x !== 20).toArray()); +flush(count("b", 4).reduce((acc, x) => acc + x)); +flush(count("c", 4).reduce((acc, x, i) => acc + x * i, 100)); +flush(count("d", 5).some((x) => x === 2)); +flush(count("e", 5).every((x) => x < 3)); +flush(count("f", 5).find((x) => x > 3) ?? -1); +flush(count("g", 2).find((x) => x > 3) ?? -1); +let each = ""; +count("h", 3).forEach((x, i) => { + each += `${x}@${i} `; +}); +flush(each.trim()); + +// take, drop and their budgets. +flush(count("i", 9).drop(2).take(3).toArray()); +flush(count("j", 3).take(Infinity).toArray()); +flush([count("k", 4).take(2.9).toArray(), count("l", 4).drop(1.5).toArray()]); +for (const budget of [-1, NaN, -0.5]) { + try { + console.log(count("m", 1).take(budget).toArray()); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +try { + count("n", 0).reduce((a, b) => a + b); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +flush("budgets"); + +// An invalid budget closes a started source before the RangeError. +const invalid = count("bad", 3); +invalid.next(); +try { + invalid.take(-1); +} catch (e) { + log.push((e as Error).message); +} +flush(invalid.next().done); + +// A started source closes through take(0) without another pull. +const started = count("o", 3); +started.next(); +flush([started.take(0).toArray(), started.next().done]); + +// Helper objects step lazily, one source value per delivered value. +const h = count("p", 4) + .map((x) => x * 2) + .filter((x) => x !== 4); +log.push("made"); +console.log(h.next(), h.next()); +console.log(h.next(), h.next()); +flush("next"); +const words = count("q", 3).map((x, i) => "w".repeat(x) + i); +flush([...words]); +flush(Array.from(count("r", 3).drop(1))); +function* wrap(): Generator { + yield 0; + yield* count("s", 2).map((x) => -x); + yield 9; +} +flush([...wrap()]); +function sum(it: Iterator): number { + let total = 0; + for (let r = it.next(); !r.done; r = it.next()) total += r.value; + return total; +} +flush(sum(count("t", 4).filter((x) => x % 2 === 0))); + +// next() from inside the helper's own callback. +const self: IteratorObject = count("re", 2).map((x) => { + if (x === 2) { + try { + self.next(); + } catch (e) { + log.push(`${(e as Error).name}: ${(e as Error).message}`); + } + } + return x; +}); +flush(self.toArray()); + +// Leaving a for...of over a helper closes the helper and its source. +for (const x of count("u", 9).map((v) => v * v)) { + if (x > 4) break; + log.push(`got${x}`); +} +flush("break"); +try { + count("v", 3).forEach((x) => { + if (x === 2) throw new Error("callback threw"); + }); +} catch (e) { + log.push((e as Error).message); +} +flush("throw"); +function* failing(): Generator { + try { + yield 1; + throw new Error("source threw"); + } finally { + log.push("failing:finally"); + } +} +try { + failing().map((x) => x).toArray(); +} catch (e) { + log.push((e as Error).message); +} +flush("source"); +// When closing the source throws while another error propagates, the +// first error is the one that reaches the caller. +function* closeThrows(): Generator { + try { + yield 1; + } finally { + log.push("closeThrows:finally"); + throw new Error("close threw"); + } +} +try { + closeThrows() + .map((x): number => { + if (x > 0) throw new Error("callback threw"); + return x; + }) + .toArray(); +} catch (e) { + log.push((e as Error).message); +} +try { + for (const x of closeThrows().filter(() => true)) throw new Error(`body threw at ${x}`); +} catch (e) { + log.push((e as Error).message); +} +try { + for (const x of closeThrows()) throw new Error(`plain loop threw at ${x}`); +} catch (e) { + log.push((e as Error).message); +} +flush("close errors"); + +// flatMap over arrays and generators; take stops mid-way through an inner +// generator and closes it before the outer source. +flush(count("w", 2).flatMap((x) => [x, -x]).toArray()); +flush(count("x", 3).flatMap((x) => count(`y${x}-`, 2)).take(3).toArray()); + +// Map and Set iterators, observed live. +const set = new Set([1, 2, 3]); +console.log( + set + .values() + .map((x) => { + if (x === 1) set.add(4); + if (x === 2) set.delete(3); + return x * 100; + }) + .toArray(), +); +const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const pairs = map.entries().filter(([, v]) => v !== 2); +map.set("d", 4); +console.log([...pairs].map(([k, v]) => k + v).join(",")); +console.log(map.keys().map((k) => k.toUpperCase()).toArray(), map.values().reduce((a, b) => a + b, 0)); +console.log(set.keys().some((x) => x > 3), set.entries().find(([k]) => k === 2)?.[1]); + +// Array iterators as helper sources. +const nums = [5, 6, 7]; +const doubled = nums.values().map((x) => x * 2); +nums.push(8); +console.log([...doubled], nums.keys().filter((i) => i > 0).toArray(), [...nums.entries().drop(2)]); + +// Records and strings flow through the stages by reference. +interface Item { + name: string; + qty: number; +} +const items: Item[] = [ + { name: "pen", qty: 3 }, + { name: "ink", qty: 0 }, + { name: "pad", qty: 7 }, +]; +function* stock(): Generator { + yield* items; +} +const inStock = stock().filter((it) => it.qty > 0); +console.log(inStock.next().value === items[0], stock().find((it) => it.name === "pad")?.qty); +console.log(stock().map((it) => `${it.name}x${it.qty}`).toArray().join(" ")); diff --git a/tests/corpus/5909-iterator-helper-objects-js.js b/tests/corpus/5909-iterator-helper-objects-js.js new file mode 100644 index 000000000..759cc2321 --- /dev/null +++ b/tests/corpus/5909-iterator-helper-objects-js.js @@ -0,0 +1,58 @@ +// Iterator helper objects in JavaScript: inferred callback parameters, +// return() on a helper (before its first next(), mid-way and after +// completion), and chains over Map/Set iterators. +const log = []; +function* count(label, n) { + try { + for (let i = 1; i <= n; i++) { + log.push(label + i); + yield i; + } + } finally { + log.push(label + ":closed"); + } +} +function flush(what) { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +flush(count("a", 4).map((x) => x + 0.5).toArray()); +flush(count("b", 6).filter((x) => x % 2 === 1).map((x, i) => x * 10 + i).take(2).toArray()); +flush(count("c", 3).reduce((acc, x) => acc + x, "")); + +// return() before the first next() closes a source that has started. +const started = count("d", 3); +started.next(); +const mapped = started.map((x) => x * 2); +flush([mapped.return(), started.next()]); +// ... and leaves an unstarted source without running its body. +const fresh = count("e", 3); +const filtered = fresh.filter(() => true); +flush([filtered.return(), fresh.next(), filtered.next()]); +// return() at a yield closes the source; later calls report done. +const stepping = count("f", 5).take(4); +flush([stepping.next(), stepping.return(), stepping.next(), stepping.return()]); +// A chain of helpers closes through to the source. +const chained = count("g", 5); +chained.next(); +const chain = chained.map((x) => x).filter(() => true).take(3); +flush([chain.return(), chained.next()]); + +// Map and Set iterators. +const scores = new Map([ + ["ann", 3], + ["bob", 9], + ["cy", 5], +]); +console.log(scores.entries().filter(([, v]) => v > 4).map(([k]) => k).toArray()); +console.log(scores.values().every((v) => v > 2), scores.keys().find((k) => k.length === 2)); +const tags = new Set(["x", "y", "z"]); +const upper = tags.values().map((t) => t.toUpperCase()); +tags.add("w"); +console.log([...upper], tags.keys().drop(3).toArray()); +for (const t of tags.values().flatMap((t) => [t, t + t])) { + if (t === "yy") break; + log.push(t); +} +flush("flat"); diff --git a/tests/diagnostics/collection-iterator-helpers.ts b/tests/diagnostics/collection-iterator-helpers.ts index 9d264ec51..127b5c64b 100644 --- a/tests/diagnostics/collection-iterator-helpers.ts +++ b/tests/diagnostics/collection-iterator-helpers.ts @@ -1,10 +1,9 @@ -// Stored collection cursors support next/iteration, but helper methods are -// refused explicitly instead of dispatching to array helpers at runtime. -const map = new Map(); -const values = map.values(); -values.map(value => value + 1); -map.keys().toArray(); -const set = new Set(); -set.values().filter(value => value > 0); -set.entries().take(1); +// Iterator helpers lower over Map/Set iterators, generators and array +// iterators. These helper forms are refused explicitly. +const set = new Set([1, 2]); +// flatMap callbacks return arrays or generators. +set.values().flatMap((value) => new Set([value, value + 1])); +// return() on a helper object. +const mapped = set.values().map((value) => value * 2); +mapped.return!(); // Each helper call must produce a diagnostic. diff --git a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt index b6996b3c1..87470b621 100644 --- a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt @@ -1,35 +1,17 @@ -collection-iterator-helpers.ts:5:1 - error SC2020: 'collection iterator .map()' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set'' is part of the standard library types but has no scriptc lowering yet - 4 | const values = map.values(); - 5 | values.map(value => value + 1); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | map.keys().toArray(); + 4 | // flatMap callbacks return arrays or generators. + 5 | set.values().flatMap((value) => new Set([value, value + 1])); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 6 | // return() on a helper object. - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering + hint: callbacks returning an array or a generator are the supported forms -collection-iterator-helpers.ts:6:1 - error SC2020: 'collection iterator .toArray()' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet - 5 | values.map(value => value + 1); - 6 | map.keys().toArray(); - | ^~~~~~~~~~~~~~~~~~~~ - 7 | const set = new Set(); + 7 | const mapped = set.values().map((value) => value * 2); + 8 | mapped.return!(); + | ^~~~~~~~~~~~~ + 9 | // Each helper call must produce a diagnostic. - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering - -collection-iterator-helpers.ts:8:1 - error SC2020: 'collection iterator .filter()' is part of the standard library types but has no scriptc lowering yet - - 7 | const set = new Set(); - 8 | set.values().filter(value => value > 0); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | set.entries().take(1); - - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering - -collection-iterator-helpers.ts:9:1 - error SC2020: 'collection iterator .take()' is part of the standard library types but has no scriptc lowering yet - - 8 | set.values().filter(value => value > 0); - 9 | set.entries().take(1); - | ^~~~~~~~~~~~~~~~~~~~~ - 10 | // Each helper call must produce a diagnostic. - - hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering \ No newline at end of file + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index e2dc5f40f..d7eda27b6 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -43,15 +43,6 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa hint: use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify -es2025-fences.ts:25:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - - 24 | // First-class iterator objects. - 25 | const it = Iterator.from([1, 2, 3].values()); - | ^~ - 26 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet 24 | // First-class iterator objects. @@ -59,7 +50,7 @@ es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard l | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | - hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray() + hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray() es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt index 4d9d1ad8d..ef768934e 100644 --- a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt @@ -1,12 +1,3 @@ -string-iterator-helpers.ts:3:7 - error SC2020: 'IteratorObject' is part of the standard library types but has no scriptc lowering yet - - 2 | // refused instead of failing at run time. - 3 | const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); - | ^~~~~ - 4 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of the standard library types but has no scriptc lowering yet 2 | // refused instead of failing at run time. @@ -14,4 +5,4 @@ string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | - hint: iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values() \ No newline at end of file + hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values() \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index b34eeecb0..852efc369 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2132,6 +2132,8 @@ "5905-array-from-async-generator.ts": 2750, "5906-array-from-async-generator-js.js": 2950, "5907-regex-groups-match-rows.ts": 3150, + "5908-iterator-helper-objects.ts": 6350, + "5909-iterator-helper-objects-js.js": 4850, "5910-set-prototype-of-primitive-object-kinds.ts": 2719, "5911-dyn-class-to-primitive.ts": 2850, "5912-dyn-class-conversion-methods.ts": 3251, @@ -5173,6 +5175,8 @@ "5905-array-from-async-generator.ts": 3500, "5906-array-from-async-generator-js.js": 3750, "5907-regex-groups-match-rows.ts": 3750, + "5908-iterator-helper-objects.ts": 5950, + "5909-iterator-helper-objects-js.js": 3750, "5910-set-prototype-of-primitive-object-kinds.ts": 5033, "5911-dyn-class-to-primitive.ts": 4732, "5912-dyn-class-conversion-methods.ts": 4424,