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,