feat(codemode): add Iterator helpers and Iterator.from (#50410)

This commit is contained in:
Aiden Cline
2026-09-21 14:14:24 -05:00
committed by GitHub
parent b8aa08f260
commit 97e833a297
15 changed files with 608 additions and 83 deletions
+16 -2
View File
@@ -239,7 +239,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
accepted, including `.then`/`.catch` handlers and collection callbacks, and vanish at the data boundary like
any function.
- [x] `Promise.withResolvers()`: the same promise and resolver callables as the constructor, as a `{ promise, resolve,
reject }` object.
reject }` object.
- [x] Recursive assimilation of objects with an own callable `then` field across `Promise.resolve`, combinators,
constructors, reactions, `finally`, `await`, and async returns. Thenable methods run deferred, receive
first-call-wins resolve/reject functions, and ignore throws after settlement. Inherited/accessor `then` fields
@@ -416,6 +416,20 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
- [x] `RegExp.escape`.
## Iterator
- [x] `Iterator.prototype.map`, `filter`, `take`, `drop`, and `flatMap` on any iterator or generator: lazy, one source
step per result, closing the source when a callback throws, on early `return()`, or when `for...of` or
destructuring finishes with it early. Once done or closed a helper stays done, and a callback that re-enters its
own helper is a `TypeError`. `take`/`drop` coerce their count and reject `NaN` or negative counts with a
`RangeError`; `flatMap` callbacks must return an iterable or iterator, not a string.
- [x] `Iterator.prototype.reduce`, `toArray`, `forEach`, `some`, `every`, and `find`, closing the source on early exit.
- [x] `Iterator.from(value)` returns iterators and generators as they are, and wraps strings, iterables, and objects
with a `next` method. `Iterator` itself is abstract: calling or constructing it is a `TypeError`.
- [x] Helpers and `Iterator.from` wrappers have `return()`; collection iterators (`array.values()`) do not, as in JS,
so an early exit from them leaves them where they were.
- [ ] `Iterator.concat`, `Iterator.zip`, and `Iterator.zipKeyed` (stage 3 proposals).
## Map and Set
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
@@ -424,7 +438,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `new Set()` from synchronous iterables.
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
- [x] Live `keys`, `values`, `entries`, and `[Symbol.iterator]` iterators for Map and Set; a Set-like operand's `keys()`
may return a built-in iterator or an array.
may return any iterator or an array.
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
- [x] Map and Set values serialize to `{}` at host/JSON boundaries.
- [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
+1 -1
View File
@@ -70,6 +70,7 @@ const table: Record<string, Factory> = {
console: (ctx) => consoleGlobal(ctx),
Promise: (ctx) => promiseGlobal(ctx),
Symbol: (ctx) => symbolGlobal(ctx),
Iterator: (ctx) => iteratorGlobals(ctx),
Number: (ctx) => numberGlobal(ctx),
String: (ctx) => stringGlobal(ctx),
Boolean: (ctx) => booleanGlobal(ctx),
@@ -103,6 +104,5 @@ export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
/** The immutable global bindings of every program, in declaration order. */
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, Value]> => {
generatorGlobals(ctx)
iteratorGlobals(ctx)
return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const)
}
@@ -70,6 +70,9 @@ import {
Callable,
define,
get,
hostCursor,
IteratorObj,
type Cursor,
has,
hasPrototype,
keys,
@@ -303,6 +306,10 @@ export class Interpreter<R> {
return this.root.iterate(value)
}
iterateDirect(value: Value) {
return this.root.iterateDirect(value)
}
/** Runs one host tool: arguments cross as JSON and the result comes back as program values. */
tool(
run: (args: Array<Json | undefined>) => Effect.Effect<Json | undefined, unknown, R>,
@@ -657,7 +664,7 @@ class Frame<R> {
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
const right = yield* self.evaluateExpression(node.right)
const cursor = self.hostCursor(right)
const cursor = self.builtinCursor(right)
const iterator = cursor === undefined ? yield* self.customIterator(right, node, awaiting) : undefined
if (iterator === undefined && cursor === undefined) {
throw invalidData(
@@ -752,35 +759,47 @@ class Frame<R> {
})
}
iterate(value: Value, node?: AstNode) {
const cursor = this.hostCursor(value)
iterate(value: Value, node?: AstNode): Effect.Effect<Cursor<R> | undefined, unknown, R> {
const cursor = this.builtinCursor(value)
if (cursor !== undefined) return Effect.succeed(cursor)
const self = this
return Effect.map(this.customIterator(value, node, false), (iterator) =>
iterator === undefined
? undefined
: {
next: self.nextIteratorResult(iterator, node, false),
close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
},
iterator === undefined ? undefined : this.customCursor(iterator, node),
)
}
private hostCursor(value: Value) {
/** GetIteratorDirect: drive an iterator by its own `next`, without asking for `[Symbol.iterator]`. */
iterateDirect(value: Value, node?: AstNode): Cursor<R> {
if (value instanceof IteratorObj) return value.cursor as Cursor<R>
if (!(value instanceof Obj)) {
throw typeError(`An iterator must be an object, received ${describeValue(value)}.`, node)
}
return this.customCursor(
{
iterator: value,
next: this.requireIteratorMethod(get(value, "next"), "Iterator next", node),
asynchronous: false,
},
node,
)
}
private customCursor(iterator: CustomIterator, node: AstNode | undefined): Cursor<R> {
return {
next: this.nextIteratorResult(iterator, node, false),
close: Effect.suspend(() => this.closeIterator(iterator, node, false)),
}
}
private builtinCursor(value: Value): Cursor<R> | undefined {
// Natives build their cursors without knowing R, like `lift` in native.ts.
if (value instanceof IteratorObj) return value.cursor as Cursor<R>
const iterator =
typeof value === "string"
? value[Symbol.iterator]()
: value instanceof Obj
? value.iterator(this.ctx.builtins)
: undefined
if (iterator === undefined) return undefined
return {
next: Effect.sync(() => {
const step = iterator.next()
return { done: Boolean(step.done), value: step.value }
}),
close: Effect.void,
}
return iterator === undefined ? undefined : hostCursor(iterator)
}
private customIterator(value: Value, node: AstNode | undefined, allowAsync = true) {
@@ -1831,7 +1850,7 @@ class Frame<R> {
private delegateYield(value: Value, node: AstNode): Effect.Effect<Value, unknown, R> {
const self = this
return Effect.gen(function* () {
const cursor = self.hostCursor(value)
const cursor = self.builtinCursor(value)
if (cursor !== undefined) {
while (true) {
const step = yield* cursor.next
@@ -35,6 +35,7 @@ const builtins = [
"TextDecoder",
"Promise",
"Iterator",
"IteratorHelper",
"AsyncIterator",
"Generator",
"AsyncGenerator",
@@ -95,6 +96,7 @@ export const createBuiltins = (): Builtins => {
TextDecoder: plain(),
Promise: plain(),
Iterator: iterator,
IteratorHelper: new Obj(iterator),
AsyncIterator: asyncIterator,
Generator: new Obj(iterator),
AsyncGenerator: new Obj(asyncIterator),
+25 -6
View File
@@ -1,5 +1,5 @@
import type { BlockStatement, Expression, Pattern } from "acorn"
import type { Effect, Fiber } from "effect"
import { Effect, type Fiber } from "effect"
import { ToolReference } from "../tool-runtime.js"
import type { Builtins } from "./intrinsics.js"
import { checkArrayLength } from "./limits.js"
@@ -224,23 +224,42 @@ export class GeneratorObj extends Opaque {
}
}
/** A built-in collection iterator: live over the host collection, yielding program values. */
/** One pull from an iterator, as `for...of` sees it. */
export type Step = { readonly done: boolean; readonly value: Value }
/** How the interpreter drives any iterator: pull the next step, or close it early. */
export type Cursor<R = unknown> = {
readonly next: Effect.Effect<Step, unknown, R>
readonly close: Effect.Effect<void, unknown, R>
}
/** A cursor over a host iterator; there is nothing to close. */
export const hostCursor = (iterator: Iterator<Value, undefined>): Cursor<never> => ({
next: Effect.sync(() => {
const step = iterator.next()
return { done: Boolean(step.done), value: step.value }
}),
close: Effect.void,
})
/** A built-in iterator: a live cursor over a host collection or an iterator helper, yielding program values. */
export class IteratorObj extends Opaque {
override readonly tag = "Iterator"
constructor(
proto: Obj,
readonly source: IteratorObject<Value, undefined>,
readonly cursor: Cursor,
) {
super(proto)
}
override get describe() {
return "an iterator"
}
override iterator() {
return this.source
}
}
/** A built-in iterator over a host iterator, e.g. `array.values()`. */
export const hostIterator = (builtins: Builtins, iterator: Iterator<Value, undefined>): IteratorObj =>
new IteratorObj(builtins.Iterator, hostCursor(iterator))
/** A built-in object around a host value: data-like, so it prints as itself and crosses to extensions as a copy. */
export abstract class Wrapper extends Obj {
override inspect(item: (value: Value) => string) {
+5 -5
View File
@@ -8,7 +8,7 @@ import {
hidden,
Arr,
GeneratorObj,
IteratorObj,
hostIterator,
Obj,
coerceToNumber,
coerceToString,
@@ -363,14 +363,14 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
(parts) => parts.join(","),
),
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").items.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").items.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").items.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").items.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.items.entries()
.map(([index, item]) => wrap([index, item])),
+5 -5
View File
@@ -9,7 +9,7 @@ import {
hidden,
Arr,
Bytes,
IteratorObj,
hostIterator,
Obj,
coerceToNumber,
coerceToString,
@@ -184,14 +184,14 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
["toString", 0, (thisValue) => self(thisValue, "toString").bytes.join(",")],
["toBase64", 0, (thisValue) => self(thisValue, "toBase64").bytes.toBase64()],
["toHex", 0, (thisValue) => self(thisValue, "toHex").bytes.toHex()],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").bytes.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").bytes.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").bytes.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").bytes.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.bytes.entries()
.map(([index, byte]) => wrapAll([index, byte])),
+18 -12
View File
@@ -9,7 +9,8 @@ import {
hidden,
Arr,
coerceToString,
IteratorObj,
hostCursor,
hostIterator,
MapObj,
Obj,
PromiseObj,
@@ -177,9 +178,9 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").map.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").map.values())],
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").map.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").map.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
[
"forEach",
1,
@@ -239,10 +240,15 @@ const loadSetRecord = <R>(
size: Math.max(Math.trunc(size), 0),
has: (item: Value) => Effect.map(ctx.call(has, source, [item]), Boolean),
keys: () =>
Effect.flatMap(ctx.call(keys, source, []), (result): Effect.Effect<Iterable<Value>> => {
if (result instanceof IteratorObj) return Effect.succeed(result.source)
if (result instanceof Arr) return Effect.succeed(result.items)
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
Effect.gen(function* () {
const result = yield* ctx.call(keys, source, [])
const cursor = result instanceof Arr ? hostCursor(result.items.values()) : ctx.iterateDirect(result)
const items: Array<Value> = []
while (true) {
const step = yield* cursor.next
if (step.done) return items
items.push(step.value)
}
}),
}
})
@@ -360,14 +366,14 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").set.values())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").set.values())],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").set.values())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").set.values())],
[
"entries",
0,
(thisValue) =>
new IteratorObj(
builtins.Iterator,
hostIterator(
builtins,
self(thisValue, "entries")
.set.values()
.map((item) => wrap([item, item])),
+4 -13
View File
@@ -8,7 +8,7 @@ import {
hidden,
Arr,
HeadersObj,
IteratorObj,
hostIterator,
Obj,
coerceToString,
isRuntimeReference,
@@ -106,18 +106,9 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
// Iterator.from because Bun's Headers typings predate iterator helpers; the runtime iterators already have them.
[
"keys",
0,
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "keys").headers.keys())),
],
[
"values",
0,
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "values").headers.values())),
],
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").headers.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").headers.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
[
"forEach",
1,
+336 -7
View File
@@ -1,19 +1,348 @@
import { methods, receiver } from "../interpreter/native.js"
import { IteratorObj, record } from "../interpreter/objects.js"
import { Cause, Effect, Exit } from "effect"
import { applyCollectionCallback } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { rangeError, typeError } from "../interpreter/model.js"
import { constructor, fn, methods, receiver, requiresNew } from "../interpreter/native.js"
import {
Arr,
coerceToNumber,
type Cursor,
define,
hasPrototype,
hidden,
IteratorObj,
Obj,
record,
type Step,
type Value,
} from "../interpreter/objects.js"
import { describeValue } from "../interpreter/references.js"
// Every built-in collection iterator shares the Iterator prototype; JS gives each collection its own, which is only
const finished: Step = { done: true, value: undefined }
// Every built-in iterator and helper shares the Iterator prototype; JS gives each collection its own, which is only
// observable through getPrototypeOf.
export const iteratorGlobals = <R>(ctx: Interpreter<R>): void => {
export const iteratorGlobals = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
methods(builtins, builtins.Iterator, [
const proto = builtins.Iterator
const method = (name: string) => `Iterator.prototype.${name}`
// A step that runs program code: when it throws, close the source first, as IteratorClose does.
const guarded = <A>(source: Cursor<R>, body: Effect.Effect<A, unknown, R>) =>
Effect.flatMap(Effect.exit(body), (exit) => {
if (Exit.isSuccess(exit)) return Effect.succeed(exit.value)
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
return Effect.andThen(Effect.exit(source.close), Effect.failCause(exit.cause))
})
// A lazy helper: `pull` advances it once. Once it reports done, throws, or is closed, it stays done. A callback
// that re-enters its own helper is a TypeError, as for a running generator.
const helper = (source: Cursor<R>, pull: Effect.Effect<Step, unknown, R>, close = source.close) => {
let done = false
let running = false
const enter = () => {
if (running) throw typeError("Iterator helper is already running.")
running = true
}
return new IteratorObj(builtins.IteratorHelper, {
next: Effect.suspend(() => {
if (done) return Effect.succeed(finished)
enter()
return Effect.flatMap(Effect.exit(pull), (exit) => {
running = false
done = Exit.isSuccess(exit) ? exit.value.done : true
return exit
})
}),
close: Effect.suspend(() => {
if (done) return Effect.void
enter()
done = true
return Effect.ensuring(
close,
Effect.sync(() => {
running = false
}),
)
}),
})
}
// GetIteratorFlattenable: an iterable by `[Symbol.iterator]`, otherwise the object itself as an iterator.
const flattenable = (value: Value, name: string, strings: boolean) =>
Effect.gen(function* () {
if (typeof value === "string" ? !strings : !(value instanceof Obj)) {
throw typeError(`${name} expects an iterable or iterator, received ${describeValue(value)}.`)
}
return (yield* ctx.iterate(value)) ?? ctx.iterateDirect(value)
})
const limit = (value: Value, name: string) => {
const count = Math.trunc(coerceToNumber(value))
if (Number.isNaN(count) || count < 0) {
throw rangeError(`${method(name)} expects a non-negative count, received ${count}.`)
}
return count
}
methods(builtins, proto, [
[
"next",
0,
(thisValue) =>
Effect.map(receiver(IteratorObj, thisValue, method("next")).cursor.next, (step) =>
record(builtins.Object, { value: step.value, done: step.done }),
),
],
[
"map",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("map"))
let counter = 0
return helper(
source,
Effect.gen(function* () {
const step = yield* source.next
if (step.done) return finished
return { done: false, value: yield* guarded(source, apply([step.value, counter++])) }
}),
)
},
],
[
"filter",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("filter"))
let counter = 0
return helper(
source,
Effect.gen(function* () {
while (true) {
const step = yield* source.next
if (step.done) return finished
if (yield* guarded(source, apply([step.value, counter++]))) return step
}
}),
)
},
],
[
"take",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
let remaining = limit(args[0], "take")
return helper(
source,
Effect.gen(function* () {
if (remaining === 0) {
yield* source.close
return finished
}
remaining -= 1
return yield* source.next
}),
)
},
],
[
"drop",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
let remaining = limit(args[0], "drop")
return helper(
source,
Effect.gen(function* () {
while (remaining > 0) {
remaining -= 1
const step = yield* source.next
if (step.done) return finished
}
return yield* source.next
}),
)
},
],
[
"flatMap",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("flatMap"))
let counter = 0
let inner: Cursor<R> | undefined
return helper(
source,
Effect.gen(function* () {
while (true) {
if (inner !== undefined) {
const step = yield* guarded(source, inner.next)
if (!step.done) return step
inner = undefined
}
const step = yield* source.next
if (step.done) return finished
inner = yield* guarded(
source,
Effect.flatMap(apply([step.value, counter++]), (mapped) =>
flattenable(mapped, method("flatMap"), false),
),
)
}
}),
Effect.suspend(() => (inner === undefined ? source.close : Effect.andThen(inner.close, source.close))),
)
},
],
[
"reduce",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("reduce"))
return Effect.gen(function* () {
let counter = 0
let accumulator = args[1]
if (args.length < 2) {
const first = yield* source.next
if (first.done) throw typeError("Iterator.prototype.reduce of an empty iterator with no initial value.")
accumulator = first.value
counter = 1
}
while (true) {
const step = yield* source.next
if (step.done) return accumulator
accumulator = yield* guarded(source, apply([accumulator, step.value, counter++]))
}
})
},
],
[
"toArray",
0,
(thisValue) => {
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").source.next()
return record(builtins.Object, { value: step.value, done: Boolean(step.done) })
const source = ctx.iterateDirect(thisValue)
return Effect.gen(function* () {
const items: Array<Value> = []
while (true) {
const step = yield* source.next
if (step.done) return new Arr(builtins.Array, items)
items.push(step.value)
}
})
},
],
[
"forEach",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("forEach"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return undefined
yield* guarded(source, apply([step.value, counter++]))
}
})
},
],
[
"some",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("some"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return false
if (yield* guarded(source, apply([step.value, counter++]))) {
yield* source.close
return true
}
}
})
},
],
[
"every",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("every"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return true
if (!(yield* guarded(source, apply([step.value, counter++])))) {
yield* source.close
return false
}
}
})
},
],
[
"find",
1,
(thisValue, args) => {
const source = ctx.iterateDirect(thisValue)
const apply = applyCollectionCallback(ctx, args[0], method("find"))
return Effect.gen(function* () {
let counter = 0
while (true) {
const step = yield* source.next
if (step.done) return undefined
if (yield* guarded(source, apply([step.value, counter++]))) {
yield* source.close
return step.value
}
}
})
},
],
])
// Helpers and `Iterator.from` wrappers can be closed; collection iterators, as in JS, cannot.
methods(builtins, builtins.IteratorHelper, [
[
"return",
0,
(thisValue) =>
Effect.map(receiver(IteratorObj, thisValue, "Iterator.prototype.return").cursor.close, () =>
record(builtins.Object, { value: undefined, done: true }),
),
],
])
const iterator = constructor<R>(builtins, proto, {
name: "Iterator",
length: 0,
call: requiresNew("Iterator"),
construct: () =>
Effect.sync(() => {
throw typeError("Iterator is abstract; use Iterator.from(...) or a built-in iterator.")
}),
})
define(
iterator,
"from",
fn(builtins, "from", 1, (_, args) =>
Effect.gen(function* () {
if (args[0] instanceof Obj && hasPrototype(args[0], proto)) return args[0]
return new IteratorObj(builtins.IteratorHelper, yield* flattenable(args[0], "Iterator.from", true))
}),
),
hidden,
)
return iterator
}
+3 -6
View File
@@ -6,7 +6,7 @@ import {
define,
hidden,
Arr,
IteratorObj,
hostIterator,
RegExpObj,
record,
coerceToNumber,
@@ -272,11 +272,8 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
define(
builtins.String,
IteratorSymbol,
fn(
builtins,
"[Symbol.iterator]",
0,
(thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
fn(builtins, "[Symbol.iterator]", 0, (thisValue) =>
hostIterator(builtins, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
),
hidden,
)
+4 -4
View File
@@ -8,7 +8,7 @@ import {
get,
hidden,
Arr,
IteratorObj,
hostIterator,
Obj,
URLObj,
URLSearchParamsObj,
@@ -269,9 +269,9 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
return undefined
},
],
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").params.keys())],
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").params.values())],
["entries", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "entries").iterator(builtins))],
["keys", 0, (thisValue) => hostIterator(builtins, self(thisValue, "keys").params.keys())],
["values", 0, (thisValue) => hostIterator(builtins, self(thisValue, "values").params.values())],
["entries", 0, (thisValue) => hostIterator(builtins, self(thisValue, "entries").iterator(builtins))],
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
[
"forEach",
+141
View File
@@ -1165,6 +1165,147 @@ describe("TextEncoder and TextDecoder", () => {
})
})
describe("Iterator helpers", () => {
test("lazy helpers chain over collection iterators and generators, one source step per result", async () => {
expect(
await value(`
const pulled = []
function* naturals() { let n = 0; while (true) { pulled.push(n); yield n++ } }
const squares = naturals().map((n) => n * n).filter((n) => n % 2 === 0).drop(1).take(3)
const first = squares.next()
return {
first,
rest: squares.toArray(),
after: squares.next(),
pulled,
values: new Map([["a", 1], ["b", 2]]).values().map((v, i) => v * 10 + i).toArray(),
flat: [1, 2].values().flatMap((n) => [n, [n]]).toArray(),
entries: [...new Map([[1, 2]]).entries().map(([k, v]) => k + v)],
}
`),
).toEqual({
first: { value: 4, done: false },
rest: [16, 36],
after: { done: true },
pulled: [0, 1, 2, 3, 4, 5, 6],
values: [10, 21],
flat: [1, [1], 2, [2]],
entries: [3],
})
})
test("eager helpers consume the source and close it on early exit", async () => {
expect(
await value(`
const log = []
function* g() { try { yield 1; yield 2; yield 3 } finally { log.push("closed") } }
const seen = []
g().forEach((v, i) => seen.push([v, i]))
return {
seen,
sum: g().reduce((a, b) => a + b),
sumFrom: g().reduce((a, b) => a + b, 10),
some: g().some((v) => v === 2),
every: g().every((v) => v < 2),
find: g().find((v) => v > 1),
missing: g().find((v) => v > 5),
log,
}
`),
).toEqual({
seen: [
[1, 0],
[2, 1],
[3, 2],
],
sum: 6,
sumFrom: 16,
some: true,
every: false,
find: 2,
log: ["closed", "closed", "closed", "closed", "closed", "closed", "closed"],
})
})
test("a helper closes its source when a callback throws, on return(), and on early for...of exit", async () => {
expect(
await value(`
const log = []
function* g(name) { try { yield 1; yield 2 } finally { log.push(name) } }
const throwing = g("throw").map((v) => { if (v === 2) throw new Error("boom"); return v })
throwing.next()
let message
try { throwing.next() } catch (error) { message = error.message }
const returned = g("return").map((v) => v)
returned.next()
const closed = returned.return()
for (const v of g("loop").filter((v) => true)) break
return { message, afterThrow: throwing.next(), closed, afterReturn: returned.next(), log }
`),
).toEqual({
message: "boom",
afterThrow: { done: true },
closed: { done: true },
afterReturn: { done: true },
log: ["throw", "return", "loop"],
})
})
test("collection iterators have no return() and continue after an early exit, as in JS", async () => {
expect(
await value(`
const it = [1, 2, 3].values()
const found = it.some((v) => v === 2)
return [typeof it.return, found, it.next().value, typeof it.map(x => x).return]
`),
).toEqual(["undefined", true, 3, "function"])
})
test("Iterator.from and the abstract Iterator constructor", async () => {
expect(
await value(`
const it = [1].values()
let n = 0
const errors = []
for (const attempt of [() => Iterator(), () => new Iterator(), () => Iterator.from(5)]) {
try { attempt() } catch (error) { errors.push(error.name) }
}
return [
Iterator.from(it) === it,
Iterator.from("ab").toArray(),
Iterator.from([1, 2]).map((v) => v * 2).toArray(),
Iterator.from({ next: () => ({ done: n > 1, value: n++ }) }).toArray(),
it instanceof Iterator,
errors,
]
`),
).toEqual([true, ["a", "b"], [2, 4], [0, 1], true, ["TypeError", "TypeError", "TypeError"]])
})
test("argument validation", async () => {
const cases: Array<[string, string]> = [
["[1].values().map(1)", "Iterator.prototype.map expects a function callback."],
["[1].values().take(-1)", "Iterator.prototype.take expects a non-negative count, received -1."],
["[1].values().drop()", "Iterator.prototype.drop expects a non-negative count, received NaN."],
[
"[1].values().flatMap((v) => 'ab').toArray()",
"Iterator.prototype.flatMap expects an iterable or iterator, received a string.",
],
["[].values().reduce((a, b) => a)", "Iterator.prototype.reduce of an empty iterator with no initial value."],
]
for (const [code, message] of cases) {
expect((await error(`return ${code}`)).message).toContain(message)
}
expect(
await error(`
function* g() { while (true) yield 1 }
const it = g().map(() => it.next())
return it.next()
`),
).toMatchObject({ message: expect.stringContaining("Iterator helper is already running.") })
})
})
describe("built-in iterators", () => {
test("keys/values/entries and [Symbol.iterator] step with next() and stay live", async () => {
expect(
+4 -2
View File
@@ -1,6 +1,6 @@
{
"revision": "250f204f23a9249ff204be2baec29600faae7b75",
"directories": ["built-ins/Array/prototype", "language/statements"],
"directories": ["built-ins/Array/prototype", "language/statements", "built-ins/Iterator"],
"harness": ["assert.js", "sta.js", "compareArray.js", "doneprintHandle.js"],
"flags": ["module", "raw", "noStrict"],
"boundaries": {
@@ -101,6 +101,8 @@
"legacy-regexp",
"__proto__",
"__getter__",
"__setter__"
"__setter__",
"iterator-sequencing",
"joint-iteration"
]
}
@@ -236,6 +236,11 @@ built-ins/Array/prototype/unshift/S15.4.4.13_A2_T3.js # Array.prototype.unshift
built-ins/Array/prototype/unshift/S15.4.4.13_A3_T2.js # Array.prototype.unshift called on incompatible receiver a data object.
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T1.js # Array.prototype.unshift called on incompatible receiver a data object.
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T2.js # #3: Array.prototype[0] = 1; x = []; x.length = 1; x.unshift(0); x[1] === 1. Actual: undefined
built-ins/Iterator/from/return-method-returns-iterator-result.js # Iterator next must be a function.
built-ins/Iterator/prototype/drop/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/drop/limit-tonumber.js # Iterator.prototype.drop expects a non-negative count, received NaN.
built-ins/Iterator/prototype/take/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/take/limit-tonumber.js # Iterator.prototype.take expects a non-negative count, received NaN.
language/statements/async-generator/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
language/statements/async-generator/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all