Lower iterator helpers on generators and Map and Set iterators

map, filter, take, drop, flatMap, toArray, forEach, reduce, some, every
and find compiled only as one chain from arr.values() or arr.entries()
that ended in a consuming method. Every other form was refused, and in
JavaScript the refusal was thrown at run time.

IteratorObject now maps to the generator type. A consuming method over
a chain of stages fuses into one loop over the source: a generator
driven like for-of, a Map or Set walked live, or an array by index. A
stage on its own builds a helper object: a generator over its source
that supports next(), for...of, spreads, Array.from and yield*. A
for...of whose head is a chain of stages without flatMap runs the
stages inside the loop.

The source is closed where Node closes it: when take reaches its count,
when some, every or find has its answer, when a callback or the loop
body throws, when the loop exits early, and before the RangeError of an
invalid take or drop budget. It is not closed when it is exhausted or
its own next() throws. return() on a helper object before its first
next() closes the source through a new runtime hook,
scr_gen_set_unstarted_return.

When closing an iterator throws while another error propagates, the
first error reaches the caller, as in Node. This applies to for...of
over a generator too, where the error from the close used to replace
the body's error. The try statement takes a dropFinallyErrors flag,
whose cleanup handler calls the new scr_exc_keep_first.

throw() lowers only on values typed as generators. Iterator helpers and
the built-in iterators have no throw method, and a value typed as an
iterator may hold one.

Callbacks in JavaScript bind their unannotated parameters to the
element type, and flatMap callbacks may return generators. A generator
whose record yields share checked storage with its return channel has
each value converted to the declared element type as it is pulled.
This commit is contained in:
Chris Tate
2026-10-18 17:46:31 -05:00
parent 5b0c0f66b9
commit 2f40bcb8d4
27 changed files with 2003 additions and 581 deletions
+2
View File
@@ -120,6 +120,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/synchronous-iteration.mdx</include>
<include>../limitations/builtin-apis/iterator-helpers.mdx</include>
<include>../limitations/builtin-apis/regular-expressions.mdx</include>
<include>../limitations/builtin-apis/object-freezing.mdx</include>
@@ -0,0 +1 @@
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
@@ -1 +1 @@
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
+37 -10
View File
@@ -165,6 +165,7 @@ import {
mangleFnClosure,
mangleFunction,
mangleGenDrop,
mangleGenSourceClose,
mangleGenSpawn,
mangleGlobal,
mangleLocal,
@@ -4623,6 +4624,25 @@ export class LlEmitter {
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
for (const line of dr) out.push(line);
// An iterator helper's return() before its first next() closes the
// source generator in its first argument slot.
const closesSource = fn.generator.closesSource === true;
if (closesSource) {
const field = lifted ? 1 : 0;
this.declare(`declare void @scr_gen_close(ptr)`);
this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`);
out.push(
`define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`,
`entry:`,
` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`,
` %sv = load ptr, ptr %sp`,
` call void @scr_gen_close(ptr %sv)`,
` ret void`,
`}`,
``,
);
}
// Spawn wrapper: pack the args (+1 moves in), allocate the
// SUSPENDED fiber — nothing runs until the first .next().
if (lifted) {
@@ -4639,6 +4659,11 @@ export class LlEmitter {
settleAsync === null
? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`
: ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`,
...(closesSource
? [
` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`,
]
: []),
` ret ptr %gg`,
`}`,
``,
@@ -9517,14 +9542,15 @@ export class LlEmitter {
B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`);
B.line(`store ptr ${stash}, ptr ${stashSlot}`);
this.scopes.push([{ slot: stashSlot, type: CAUGHT }]);
const suppressHandler = s.suppressFinallyErrors
? {
label: B.newLabel("try.fs"),
used: false,
frameDepth: this.frames.length,
scopeDepth: this.scopes.length,
}
: null;
const suppressHandler =
s.suppressFinallyErrors || s.dropFinallyErrors
? {
label: B.newLabel("try.fs"),
used: false,
frameDepth: this.frames.length,
scopeDepth: this.scopes.length,
}
: null;
if (suppressHandler) this.tryStack.push(suppressHandler);
this.emitBlock(s.finallyBody!);
if (suppressHandler) this.tryStack.pop();
@@ -9534,8 +9560,9 @@ export class LlEmitter {
this.emitUnwind();
if (suppressHandler?.used) {
B.startBlock(suppressHandler.label);
this.declare(`declare void @scr_exc_suppress(ptr)`);
B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`);
const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress";
this.declare(`declare void @${cleanup}(ptr)`);
B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`);
this.emitUnwind();
}
}
+5
View File
@@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string {
export function mangleGenDrop(fnName: string): string {
return `sc_gd_${sanitize(fnName)}`;
}
/** The never-started return() hook of a generator that closes its source
* argument (an iterator helper). */
export function mangleGenSourceClose(fnName: string): string {
return `sc_gc_${sanitize(fnName)}`;
}
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
export function mangleResolveThunk(n: number): string {
return `sc_rslv_${n}`;
@@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
status: "static",
...(COLLECTION_ITERATOR_METHODS.has(name)
? {
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
}
: {}),
});
@@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
status: "static",
...(COLLECTION_ITERATOR_METHODS.has(name)
? {
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
}
: {}),
});
@@ -747,7 +747,7 @@ function lowerForOfMapOrSet(
* Recurses through every nested statement list; nested functions are
* lifted elsewhere and never appear here, so every return found exits
* THIS function through the active iteration. */
function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
const walk = (list: IrStmt[]): IrStmt[] =>
list.flatMap((s): IrStmt[] => {
switch (s.kind) {
File diff suppressed because it is too large Load Diff
@@ -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()",
);
}
@@ -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",
@@ -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
@@ -3224,7 +3224,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isStdlibInterface("IterableIterator") ||
// An Iterator<T> annotation holds a single-pass iterator object: a
// generator, or a native iterator adapted into one.
isStdlibInterface("Iterator")
isStdlibInterface("Iterator") ||
// IteratorObject<T> 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);
+14 -1
View File
@@ -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;
};
+6 -6
View File
@@ -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",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5908-iterator-helper-objects.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5909-iterator-helper-objects-js.js"
],
"diags": []
}
+9 -2
View File
@@ -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:
+11
View File
@@ -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.
+4
View File
@@ -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);
+3
View File
@@ -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);
@@ -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<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;
}
// 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<number> {
yield 0;
yield* count("s", 2).map((x) => -x);
yield 9;
}
flush([...wrap()]);
function sum(it: Iterator<number>): 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<number> = 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<number> {
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<number> {
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<string, number>([
["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<Item> {
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(" "));
@@ -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");
@@ -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<string, number>();
const values = map.values();
values.map(value => value + 1);
map.keys().toArray();
const set = new Set<number>();
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<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!();
// Each helper call must produce a diagnostic.
@@ -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<number>'' 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<number, undefined, unknown>.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<number>();
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<number>();
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
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
@@ -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<number, undefined, unknown>' 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
@@ -1,12 +1,3 @@
string-iterator-helpers.ts:3:7 - error SC2020: 'IteratorObject<string, undefined, unknown>' 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()
hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()
+4
View File
@@ -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,