mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(codemode): add Iterator helpers and Iterator.from (#50410)
This commit is contained in:
@@ -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`,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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])),
|
||||
|
||||
@@ -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])),
|
||||
|
||||
@@ -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])),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user