Lower string and typed array iterators, Iterator.from and helper return()

String iterators (s[Symbol.iterator]()) and typed array values(),
keys() and entries() compile as iterator objects: generators over the
code points and over the indices. The iterator helpers apply to them,
and for...of, spreads and Array.from consume them.

Iterator.from over an array, string, typed array, Set, Map or iterator
object answers the value's own iterator, as Node does for iterators
that already inherit from Iterator.prototype. Objects with their own
next method keep the Iterator fence. As the root of a helper chain,
Iterator.from(x) walks x itself: an array by index, a Set or Map live,
a generator directly.

h.return!() and h.return?.() compile when h is a helper call or a const
initialized with one: helper objects always have return(), so both
spellings are the call. Other iterator objects keep the fence, since the
array, string, Map and Set iterators have no return().

flatMap callbacks may return Sets (their values) and Maps (their
entries), walked live; a string result throws Node's TypeError and
closes the source.
This commit is contained in:
Chris Tate
2026-10-18 22:10:17 -05:00
parent ecff232688
commit dc6a638e63
19 changed files with 700 additions and 74 deletions
@@ -1 +1 @@
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported.
@@ -35,6 +35,7 @@ import {
} from "../optional-arguments.js";
import { lowerCheckedArrayFrom } from "./array-construction.js";
import { lowerToNumberArgument } from "../lower-exprs.js";
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
/** TypedArray.prototype.sort and toSorted. The receiver and comparator
* expressions are evaluated before entering the helper (buildBytesSortFn).
@@ -462,6 +463,21 @@ export function lowerBytesMethodCall(
if (name === "sort" || name === "toSorted") {
return lowerBytesSortCall(lowerer, call, access, receiverIr);
}
// values()/keys()/entries() as iterator objects (for...of heads and
// stored numeric iterators lower before this).
if ((name === "values" || name === "keys" || name === "entries") && nArgs === 0) {
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind === "generator") {
const value = lowerBytesIteratorValue(
lowerer,
lowerer.lowerExpr(access.expression),
name,
type,
loc,
);
if (value) return value;
}
}
if (name === "toReversed") {
if (nArgs !== 0) {
lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call);
@@ -5,6 +5,7 @@ import type { Lowerer } from "../lowerer.js";
import {
BOOL,
CAUGHT,
DYN,
F64,
type IrMapIntrinsicMethod,
type IrSetIntrinsicMethod,
@@ -28,12 +29,18 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js";
import {
type GenType,
generatorLoopBinding,
resultRecordOf,
genStep,
genValue,
stepDrivable,
} from "../lower-generators.js";
import { exitBeforeReturns } from "./for-of.js";
import { iteratorSlotAdapter } from "../iterator-adapters.js";
import {
iteratorSlotAdapter,
lowerArrayIteratorValue,
lowerBytesIteratorValue,
lowerStringIteratorValue,
} from "../iterator-adapters.js";
import { lowerArrayCallback } from "./callback-arguments.js";
/* ── iterator helpers (ES2025) ─────────────────────────────────────────── */
@@ -189,6 +196,24 @@ export function lowerGeneratorHelperCall(
collectionSeed(lowerer, source),
);
}
// Iterator.from(arr) walks the array by index.
const arrayRoot = arrayIteratorCall(lowerer, cur);
if (arrayRoot !== null) {
const items = lowerer.lowerExpr(arrayRoot.receiver);
if (items.type.kind !== "array")
lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver));
const elem =
arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem;
return lowerFusedChain(
lowerer,
call,
name,
stages,
{ kind: "array", type: items.type, proj: arrayRoot.proj },
items,
elem,
);
}
const src = lowerHelperSource(lowerer, cur);
return lowerFusedChain(
lowerer,
@@ -201,6 +226,128 @@ export function lowerGeneratorHelperCall(
);
}
/** `h.return!()` or `h.return?.()` on a helper object. The lib declares
* return() as optional on iterator objects, and a helper object always
* has one, so both spellings are the call: the helper completes and closes
* its source, and the result is `{ value: undefined, done: true }`. The
* receiver must be provably a helper object — a helper stage call, or a
* const binding initialized with one — since other iterator objects (the
* array, string, Map and Set iterators) have no return(). Null otherwise,
* and for the plain `h.return()` spelling (lowerGenMethodCall's). */
export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
let callee: ts.Expression = call.expression;
const nonNull = ts.isNonNullExpression(callee);
if (nonNull) callee = (callee as ts.NonNullExpression).expression;
if (!nonNull && call.questionDotToken === undefined) return null;
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
if (callee.name.text !== "return" || call.arguments.length !== 0) return null;
if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null;
const callT = lowerer.mapTypeOf(lowerer.typeOf(call));
const gen = lowerer.lowerExpr(callee.expression);
if (gen.type.kind !== "generator" || gen.type.async) return null;
const loc = locOf(call);
const recT = resultRecordOf(lowerer, gen.type);
const resume: IrExpr = { kind: "genResume", mode: "return", gen, arg: null, type: recT, loc };
// `?.` types the call `IteratorResult | undefined`, which has no mapping
// as a whole; the result is never undefined, so it is the record itself.
if (callT === null || typeEquals(callT, recT)) return resume;
const result = lowerer.coerceToExpected(resume, callT);
if (!typeEquals(result.type, callT)) lowerer.badType(call, lowerer.typeOf(call));
return result;
}
/** Whether `expr` is a helper stage call, or a const binding initialized
* with one. */
function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (isStageCall(lowerer, expr)) return true;
if (!ts.isIdentifier(expr)) return false;
const symbol = lowerer.resolveValueSymbol(expr);
const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined;
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
decl.initializer === undefined ||
!ts.isVariableDeclarationList(decl.parent) ||
(decl.parent.flags & ts.NodeFlags.Const) === 0
)
return false;
let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
return isStageCall(lowerer, init);
}
/** `Iterator.from(value)` over a typed built-in iterable: the value's own
* iterator, which already inherits from Iterator.prototype, so Node returns
* it rather than a wrapper. A generator (including a helper object or an
* array iterator) is itself; an array, string or typed array gives its
* values iterator; a Set its values and a Map its entries, adapted into a
* generator over the live collection. The result converts into the call's
* IteratorObject type. Null for other arguments (objects with their own
* next method, untyped values), which keep the Iterator fence. */
export function lowerIteratorFromCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.name.text !== "from" || call.arguments.length !== 1) return null;
const argNode = call.arguments[0]!;
if (ts.isSpreadElement(argNode)) return null;
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
if (resultT?.kind !== "generator" || resultT.async || argT === null) return null;
const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"];
if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null;
const loc = locOf(call);
const value = lowerer.lowerExpr(argNode);
const own = (yieldT: IrType): GenType => ({
kind: "generator",
yieldT,
retT: VOID,
nextT: UNDEFINED_T,
});
let iterator: IrExpr | null = null;
const t = value.type;
if (t.kind === "generator") iterator = value;
else if (t.kind === "array")
iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc);
else if (t.kind === "string")
iterator = lowerStringIteratorValue(lowerer, value, own(STRING), loc);
else if (t.kind === "bytes")
iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc);
else if (t.kind === "set" || t.kind === "map") {
const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value);
const native: IrExpr = {
kind: "dynInvoke",
recv: lowerer.coerceToExpected(value, DYN),
method: t.kind === "set" ? "values" : "entries",
calleeName: t.kind === "set" ? "set.values" : "map.entries",
args: [],
type: DYN,
loc,
};
iterator = iteratorSlotAdapter(lowerer, native, own(elem));
}
if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode));
const result = lowerer.coerceToExpected(iterator, resultT);
if (!typeEquals(result.type, resultT)) lowerer.badType(call, lowerer.typeOf(call));
return result;
}
/** The interned [K, V] pair a Map's entries yield. */
function entryPairTypeOf(lowerer: Lowerer, keyT: IrType, valueT: IrType): IrType {
return {
kind: "record",
shapeId: lowerer.shapes.intern(
[
{ name: "0", type: keyT },
{ name: "1", type: valueT },
],
true,
),
};
}
const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]);
const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]);
/** Native iterators that ride checked runtime values and adapt into
@@ -353,12 +500,30 @@ export function lowerForOfHelperChain(
}
}
/** `arr.values()` or `arr.entries()` on an array value, as the root of a
* helper chain. */
/** The argument of an `Iterator.from(x)` call, or null. As a chain's root,
* the call is the argument's own iterator, so the chain can walk the
* argument directly. */
function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1)
return null;
const arg = expr.arguments[0]!;
if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null;
if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null;
return lowerer.isStdlibMember(expr.expression) ? arg : null;
}
/** `arr.values()`, `arr.entries()` or `Iterator.from(arr)` 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 {
const fromArg = iteratorFromArgument(lowerer, expr);
if (fromArg !== null)
return lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "array"
? { receiver: fromArg, proj: "values" }
: null;
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
return null;
@@ -401,6 +566,17 @@ function collectionIteratorCall(
lowerer: Lowerer,
expr: ts.Expression,
): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null {
// Iterator.from(set) is the Set's values iterator, Iterator.from(map) the
// Map's entries iterator.
const fromArg = iteratorFromArgument(lowerer, expr);
if (fromArg !== null) {
const kind = lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind;
return kind === "set"
? { receiver: fromArg, proj: "values" }
: kind === "map"
? { receiver: fromArg, proj: "entries" }
: null;
}
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0)
return null;
@@ -480,6 +656,11 @@ function lowerHelperSource(
lowerer: Lowerer,
expr: ts.Expression,
): { gen: IrExpr & { type: GenType }; elem: IrType } {
// Iterator.from(gen) is the generator itself; the call's IteratorObject
// type would add an adapter generator over it.
const fromArg = iteratorFromArgument(lowerer, expr);
if (fromArg !== null && lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "generator")
return lowerHelperSource(lowerer, fromArg);
const tsType = lowerer.typeOf(expr);
const mapped = lowerer.mapTypeOf(tsType);
const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0];
@@ -577,10 +758,17 @@ function lowerStage(
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 };
// A Set flattens its values and a Map its entries, walked live. A string
// is rejected at run time with Node's TypeError (strings are not
// iterator objects).
if (ret.kind === "set") return { stage: { kind: "flatMap", fn }, out: ret.elem };
if (ret.kind === "map")
return { stage: { kind: "flatMap", fn }, out: entryPairTypeOf(lowerer, ret.key, ret.value) };
if (ret.kind === "string") return { stage: { kind: "flatMap", fn }, out: STRING };
lowerer.noLowering(
`.flatMap over a callback returning '${lowerer.fmt(ret)}'`,
s.argNode,
"callbacks returning an array or a generator are the supported forms",
"callbacks returning an array, a generator, a Set or a Map are the supported forms",
);
}
@@ -1245,8 +1433,9 @@ function walkCollection(
}
/** 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. */
* `innerId`: an array by index, a Set's values or a Map's entries walked
* live, or a generator driven (and closed on early exit) like the source.
* A string result throws Node's TypeError. `stop` ends the walk early. */
function walkInner(
lowerer: Lowerer,
frame: ChainFrame,
@@ -1257,6 +1446,27 @@ function walkInner(
loc: SrcLoc,
): IrStmt[] {
if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)];
if (innerT.kind === "set" || innerT.kind === "map") {
const source: ChainSource = {
kind: "collection",
type: innerT,
proj: innerT.kind === "set" ? "values" : "entries",
};
return walkCollection(lowerer, frame, innerId, source, stop ?? boolLit(false, loc), body, loc);
}
if (innerT.kind === "string") {
return [
throwTypeError(
{
kind: "strLit",
value: "Iterator.prototype.flatMap called on non-object",
type: STRING,
loc,
},
loc,
),
];
}
if (innerT.kind !== "generator")
throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result");
const pre: IrStmt[] =
@@ -265,6 +265,222 @@ function dynIteratorAdapter(
return { kind: "call", callee: name, args: [expr], type: expected, loc };
}
/** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a
* value: an interned generator over the indices, reading the length and
* the element at each step, as the typed array iterator does. Null when
* `type` is not the matching number (or [number, number]) generator. */
export function lowerBytesIteratorValue(
lowerer: Lowerer,
receiver: IrExpr,
method: "values" | "keys" | "entries",
type: IrType,
loc: SrcLoc,
): IrExpr | null {
const bytesT = receiver.type;
if (bytesT.kind !== "bytes" || type.kind !== "generator") return null;
if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null;
const pair = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined;
const pairOk =
pair?.tuple === true &&
pair.fields.length === 2 &&
pair.fields.every((f) => (f.name === "0" || f.name === "1") && f.type.kind === "f64");
if (method === "entries" ? !pairOk : type.yieldT.kind !== "f64") return null;
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
if (!resultType) return null;
const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`;
let name = lowerer.valueHelpers.get(key);
if (!name) {
name = `%bytes.iter.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, name);
const list = varRef("list.0", bytesT, loc);
const index = varRef("index.0", F64, loc);
const element: IrExpr = {
kind: "bytesIntrinsic",
method: "get",
receiver: list,
args: [index],
type: F64,
loc,
};
const yielded: IrExpr =
method === "keys"
? index
: method === "values"
? element
: {
kind: "recordLit",
fields: [
{ name: "0", value: index },
{ name: "1", value: element },
],
type: type.yieldT,
loc,
};
lowerer.liftedFns.push({
name,
params: [{ localId: "list.0", name: "list", type: bytesT }],
returnType: VOID,
locals: [
{ id: "list.0", name: "list", type: bytesT, mutable: false },
{ id: "index.0", name: "index", type: F64, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "index.0",
init: { kind: "numLit", value: 0, type: F64, loc },
loc,
},
{
kind: "while",
cond: {
kind: "bin",
op: "<",
left: index,
right: {
kind: "bytesIntrinsic",
method: "length",
receiver: list,
args: [],
type: F64,
loc,
},
type: BOOL,
loc,
},
body: [
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc },
{
kind: "assign",
localId: "index.0",
value: {
kind: "bin",
op: "+",
left: index,
right: { kind: "numLit", value: 1, type: F64, loc },
type: F64,
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: null, loc },
],
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
loc,
});
}
return { kind: "call", callee: name, args: [receiver], type, loc };
}
/** `s[Symbol.iterator]()` on a string value: an interned generator over
* its code points, the string iterator's steps (a lone surrogate is one
* step). Null when `type` is not the plain string-yielding generator. */
export function lowerStringIteratorValue(
lowerer: Lowerer,
receiver: IrExpr,
type: IrType,
loc: SrcLoc,
): IrExpr | null {
if (receiver.type.kind !== "string" || type.kind !== "generator") return null;
if (
type.yieldT.kind !== "string" ||
type.retT.kind !== "void" ||
type.nextT.kind !== "undefinedT"
)
return null;
const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions);
if (!resultType) return null;
const key = `strIter:${typeKey(type)}`;
let name = lowerer.valueHelpers.get(key);
if (!name) {
name = `%str.iter.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, name);
const text = varRef("text.0", STRING, loc);
const index = varRef("index.0", F64, loc);
const point = varRef("point.0", STRING, loc);
lowerer.liftedFns.push({
name,
params: [{ localId: "text.0", name: "text", type: STRING }],
returnType: VOID,
locals: [
{ id: "text.0", name: "text", type: STRING, mutable: false },
{ id: "index.0", name: "index", type: F64, mutable: true },
{ id: "point.0", name: "point", type: STRING, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "index.0",
init: { kind: "numLit", value: 0, type: F64, loc },
loc,
},
{
kind: "while",
cond: {
kind: "bin",
op: "<",
left: index,
right: {
kind: "strIntrinsic",
method: "length",
receiver: text,
args: [],
type: F64,
loc,
},
type: BOOL,
loc,
},
body: [
{
kind: "varDecl",
localId: "point.0",
init: {
kind: "strIntrinsic",
method: "cpAt",
receiver: text,
args: [index],
type: STRING,
loc,
},
loc,
},
{
kind: "assign",
localId: "index.0",
value: {
kind: "bin",
op: "+",
left: index,
right: {
kind: "strIntrinsic",
method: "length",
receiver: point,
args: [],
type: F64,
loc,
},
type: F64,
loc,
},
loc,
},
{ kind: "exprStmt", expr: { kind: "yieldExpr", value: point, type: VOID, loc }, loc },
],
loc,
},
{ kind: "return", value: null, loc },
],
generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType },
loc,
});
}
return { kind: "call", callee: name, args: [receiver], type, loc };
}
/** A generator flowing into a slot with other channel types (a
* `Generator<number, void, undefined>` returned through an
* `IterableIterator<number>` annotation). The interned adapter generator
@@ -94,6 +94,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
import {
lowerGeneratorHelperCall,
lowerHelperReturnCall,
lowerIteratorFromCall,
lowerIteratorHelperCall,
} from "./containers/iterator-helpers.js";
import {
@@ -223,6 +225,7 @@ import {
completeArgs,
} from "./call-arguments.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
import { lowerStringIteratorValue } from "./iterator-adapters.js";
import { checkedArmExtraction } from "./coercions/unions.js";
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
import {
@@ -365,6 +368,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// machinery (the member always exists on a POSIX target, so the
// optional call IS the call; `process.getuid` itself has no value
// lowering for the chain to guard).
// `h.return!()` / `h.return?.()` on a helper object: the method exists.
const helperReturn = lowerHelperReturnCall(lowerer, expr);
if (helperReturn) return helperReturn;
const processOptional = lowerer.lowerProcessOptionalMethodCall(expr);
if (processOptional) return processOptional;
const reqStreamOptional = lowerCompatReqStreamOptionalCall(lowerer, expr);
@@ -1616,6 +1622,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`);
}
// `s[Symbol.iterator]()` on a string: the string iterator as a value.
if (
ts.isElementAccessExpression(expr.expression) &&
!expr.expression.questionDotToken &&
!expr.questionDotToken &&
expr.arguments.length === 0
) {
const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression);
if (iterator) return iterator;
}
// The element spelling of an unsupported native Web method must fence
// before lowering the callee into a checked-dynamic keyed read.
if (ts.isElementAccessExpression(expr.expression)) {
@@ -3157,10 +3174,32 @@ function lowerOptionalStringNumber(
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
}
/** 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"]);
/** `s[Symbol.iterator]()` on a string-typed receiver: the generator over
* its code points. Null for other element calls. */
function lowerStringIteratorCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.ElementAccessExpression,
): IrExpr | null {
let key = access.argumentExpression;
while (ts.isParenthesizedExpression(key)) key = key.expression;
if (
!ts.isPropertyAccessExpression(key) ||
lowerer.stdlibGlobalMember(key, "Symbol") !== "iterator"
)
return null;
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null;
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
if (type?.kind !== "generator") return null;
const receiver = lowerer.lowerExpr(access.expression);
const value = lowerStringIteratorValue(lowerer, receiver, type, locOf(call));
if (!value) lowerer.unsupported("SC1090", call, "this string iterator type");
return value;
}
/** Iterator types with checked runtime receivers, whose helper calls lower
* in lowerGeneratorHelperCall. */
const CHECKED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]);
const ITERATOR_HELPER_METHODS = new Set([
"map",
"filter",
@@ -3220,25 +3259,17 @@ function lowerDynReceiverMethodCall(
const recvSymbol = recvTs.getSymbol();
if (
recvSymbol &&
UNLOWERED_ITERATOR_TYPES.has(recvSymbol.name) &&
CHECKED_ITERATOR_TYPES.has(recvSymbol.name) &&
lowerer.isStdlibSymbol(recvSymbol) &&
ITERATOR_HELPER_METHODS.has(access.name.text) &&
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,
"stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering",
);
}
// The checked runtime's string iterator has no helper methods.
const helper = lowerGeneratorHelperCall(lowerer, call, access);
if (helper) return helper;
lowerer.noLowering(
`${recvSymbol.name}.${access.name.text}`,
`collection iterator .${access.name.text}()`,
call,
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
"iterator helpers on Map/Set iterators are called directly on the iterator, without optional chaining",
);
}
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
@@ -6644,10 +6675,10 @@ function recordAssignHelper(
return helper;
}
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
* 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. */
/** The `Iterator` global's statics (ES2025): `Iterator.from` over a typed
* built-in iterable lowers (lowerIteratorFromCall); every other member,
* and other arguments, fence with the working spelling named instead of
* the generic-method fence's monomorphization wording. */
function lowerIteratorStaticFence(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -6655,11 +6686,15 @@ function lowerIteratorStaticFence(
): IrExpr | null {
if (!lowerer.isStdlibGlobal(access.expression, "Iterator")) return null;
if (!lowerer.isStdlibMember(access)) return null;
const from = lowerIteratorFromCall(lowerer, call, access);
if (from) return from;
lowerer.noLowering(
`Iterator.${access.name.text}`,
call,
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
"arr.values().map(f).take(n).toArray()",
access.name.text === "from"
? "Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators"
: "iterator helpers apply directly to generators and to array, Map and Set iterators: " +
"arr.values().map(f).take(n).toArray()",
);
}
@@ -147,6 +147,7 @@ import {
} from "../../diagnostics/diagnostic.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
import { lowerCollectionSpread } from "./containers/collection-methods.js";
import { lowerHelperReturnCall } from "./containers/iterator-helpers.js";
import {
arrayNumberRead,
arrayValueRead,
@@ -1904,6 +1905,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false);
if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr);
if (ts.isCallExpression(expr)) {
// `h.return?.()` on a helper object: the method exists, so the
// optional call is the call.
const helperReturn = lowerHelperReturnCall(lowerer, expr);
if (helperReturn) return helperReturn;
// The tail of an optional chain whose `?.` sits deeper
// (`x?.trim().toLowerCase()`): the whole tail short-circuits together.
if (isOptionalChainTail(lowerer, expr)) return lowerer.lowerOptionalChain(expr);
@@ -3267,8 +3267,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// Native collection iterators use checked runtime iterator objects.
if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN;
// Array iterators (`list.values()` and friends) are generators over the
// live array: each step reads the current length and element.
if (isStdlibInterface("ArrayIterator")) {
// live array: each step reads the current length and element. String
// iterators (`s[Symbol.iterator]()`) are generators over the code points.
if (isStdlibInterface("ArrayIterator") || isStdlibInterface("StringIterator")) {
const arg = checker.getTypeArguments(widened as ts.TypeReference)[0];
const yieldT = arg ? mapType(arg, ctx) : null;
if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6071-iterator-sources.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6072-iterator-sources-js.js"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/diagnostics/string-iterator-helpers.ts"
],
"diags": []
}
+70
View File
@@ -0,0 +1,70 @@
// Iterator objects from strings and typed arrays, Iterator.from over the
// built-in iterables, return() on helper objects, and flatMap over Set,
// Map and string results.
const log: string[] = [];
function* count(label: string, n: number): Generator<number> {
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;
}
// String iterators step by code point; a lone surrogate is one step.
const text = "a\u{1F600}b\uD800";
const chars = text[Symbol.iterator]();
console.log(chars.next(), chars.next().value?.length, [...chars].length);
console.log(text[Symbol.iterator]().map((c) => c.codePointAt(0)!.toString(16)).toArray());
console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join(""));
for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c);
console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h"));
// Typed array iterators.
const bytes = new Uint8Array([9, 8, 7]);
console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray());
console.log(bytes.entries().map(([i, v]) => i * v).toArray());
const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1);
console.log(floats.next(), [...floats]);
for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v);
// Iterator.from returns each built-in iterable's own iterator.
const list = [1, 2];
const fromList = Iterator.from(list);
list.push(3);
console.log([...fromList], Iterator.from("a\u{1F600}").toArray());
console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray());
console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0));
const fromGen = Iterator.from(count("a", 3));
flush([fromGen.next(), fromGen.take(1).toArray()]);
flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray());
// return() on helper objects.
const started = count("c", 3);
started.next();
const doubled = started.map((x) => x * 2);
flush([doubled.return!(), started.next()]);
const fresh = count("d", 3);
const kept = fresh.filter(() => true);
flush([kept.return?.(), fresh.next(), kept.next()]);
const stepping = count("e", 5).drop(1);
flush([stepping.next(), stepping.return!(), stepping.next()]);
flush(count("f", 4).map((x) => x).take(2).return!());
// flatMap over Set and Map results, and a string result's TypeError.
console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray());
console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray());
const pairs = new Map<string, number>([["m", 1], ["n", 2]]);
for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v);
try {
count("g", 3).flatMap((x) => `s${x}`).toArray();
} catch (e) {
log.push(`${(e as Error).name}: ${(e as Error).message}`);
}
flush("string result");
+70
View File
@@ -0,0 +1,70 @@
// The JavaScript form: iterator objects from strings and typed arrays,
// Iterator.from over the built-in iterables, return() on helper objects,
// and flatMap over Set, Map and string results.
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;
}
// String iterators step by code point; a lone surrogate is one step.
const text = "a\u{1F600}b\uD800";
const chars = text[Symbol.iterator]();
console.log(chars.next(), chars.next().value.length, [...chars].length);
console.log(text[Symbol.iterator]().map((c) => c.length).toArray());
console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join(""));
for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c);
console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h"));
// Typed array iterators.
const bytes = new Uint8Array([9, 8, 7]);
console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray());
console.log(bytes.entries().map(([i, v]) => i * v).toArray());
const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1);
console.log(floats.next(), [...floats]);
for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v);
// Iterator.from returns each built-in iterable's own iterator.
const list = [1, 2];
const fromList = Iterator.from(list);
list.push(3);
console.log([...fromList], Iterator.from("a\u{1F600}").toArray());
console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray());
console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0));
const fromGen = Iterator.from(count("a", 3));
flush([fromGen.next(), fromGen.take(1).toArray()]);
flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray());
// return() on helper objects.
const started = count("c", 3);
started.next();
const doubled = started.map((x) => x * 2);
flush([doubled.return(), started.next()]);
const fresh = count("d", 3);
const kept = fresh.filter(() => true);
flush([kept.return?.(), fresh.next(), kept.next()]);
const stepping = count("e", 5).drop(1);
flush([stepping.next(), stepping.return(), stepping.next()]);
flush(count("f", 4).map((x) => x).take(2).return());
// flatMap over Set and Map results, and a string result's TypeError.
console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray());
console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray());
const pairs = new Map ([["m", 1], ["n", 2]]);
for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v);
try {
count("g", 3).flatMap((x) => `s${x}`).toArray();
} catch (e) {
log.push(`${e.name}: ${e.message}`);
}
flush("string result");
@@ -1,9 +1,13 @@
// Iterator helpers lower over Map/Set iterators, generators and array
// iterators. These helper forms are refused explicitly.
// Iterator helpers lower over Map/Set iterators, generators and array,
// string and typed array iterators. These helper forms are refused
// explicitly.
const set = new Set<number>([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!();
// return() on an iterator object that is not provably a helper object
// (array, string, Map and Set iterators have no return method).
function close(it: IteratorObject<number>): void {
it.return!();
}
close(set.values().map((value) => value * 2));
// throw() on a helper object (helpers have no throw method).
set.values().map((value) => value + 1).throw!(new Error("stop"));
// Each helper call must produce a diagnostic.
+3 -3
View File
@@ -1,6 +1,6 @@
// The es2025-lib surface WITHOUT a lowering fences by name: resizable
// ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync,
// first-class iterator objects, and the unlowered forms of the
// Iterator.from over objects with their own next method, and the unlowered forms of the
// implemented statics (Promise.try with fewer arguments than parameters,
// groupBy's literal-union keys, a set-like non-Set argument).
@@ -21,8 +21,8 @@ sab.grow(16);
const ia = new Int32Array(4);
const waited = Atomics.waitAsync(ia, 0, 0, 1);
// First-class iterator objects.
const it = Iterator.from([1, 2, 3].values());
// Iterator.from over an object with its own next method.
const it = Iterator.from({ next: (): IteratorResult<number> => ({ value: 1, done: false }) });
// Promise.try with fewer arguments than the callback's parameters.
const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1);
@@ -1,3 +0,0 @@
// The string iterator has no iterator-helper lowering; the call is
// refused instead of failing at run time.
const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase());
@@ -1,17 +1,17 @@
collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set<number>'' is part of the standard library types but has no scriptc lowering yet
collection-iterator-helpers.ts:8:3 - error SC2020: 'IteratorObject<number, unknown, unknown>.return' is part of the standard library types but has no scriptc lowering yet
4 | // flatMap callbacks return arrays or generators.
5 | set.values().flatMap((value) => new Set([value, value + 1]));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 | // return() on a helper object.
7 | function close(it: IteratorObject<number>): void {
8 | it.return!();
| ^~~~~~~~~
9 | }
hint: callbacks returning an array or a generator are the supported forms
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject<number, undefined, unknown>.return' is part of the standard library types but has no scriptc lowering yet
collection-iterator-helpers.ts:12:1 - error SC2020: 'IteratorObject<number, undefined, unknown>.throw' is part of the standard library types but has no scriptc lowering yet
7 | const mapped = set.values().map((value) => value * 2);
8 | mapped.return!();
| ^~~~~~~~~~~~~
9 | // Each helper call must produce a diagnostic.
11 | // throw() on a helper object (helpers have no throw method).
12 | set.values().map((value) => value + 1).throw!(new Error("stop"));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 | // Each helper call must produce a diagnostic.
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
@@ -45,12 +45,12 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa
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.
25 | const it = Iterator.from([1, 2, 3].values());
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 | // Iterator.from over an object with its own next method.
25 | const it = Iterator.from({ next: (): IteratorResult<number> => ({ value: 1, done: false }) });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 |
hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray()
hint: Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators
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
@@ -1,8 +0,0 @@
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.
3 | const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase());
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 |
hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()
+4
View File
@@ -2234,6 +2234,8 @@
"6053-class-to-primitive.ts": 4385,
"6054-js-class-to-primitive.js": 2181,
"6070-iterator-helpers-untyped.js": 4050,
"6071-iterator-sources.ts": 5150,
"6072-iterator-sources-js.js": 5350,
"6100-record-get-or-create.ts": 3771,
"6101-record-get-or-create-unchecked/main.ts": 2107,
"6103-typeof-builtin-bindings.cjs": 2202,
@@ -5304,6 +5306,8 @@
"6053-class-to-primitive.ts": 36270,
"6054-js-class-to-primitive.js": 2958,
"6070-iterator-helpers-untyped.js": 6900,
"6071-iterator-sources.ts": 5150,
"6072-iterator-sources-js.js": 5350,
"6100-record-get-or-create.ts": 3173,
"6101-record-get-or-create-unchecked/main.ts": 3228,
"6103-typeof-builtin-bindings.cjs": 1796,