)` 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,