diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts index 90eb8e537fc..4e9ce352fc2 100644 --- a/packages/codemode/src/interpreter/errors.ts +++ b/packages/codemode/src/interpreter/errors.ts @@ -1,8 +1,7 @@ -import { consumer, STATE } from "../machine/native-kind.js" import type { Diagnostic } from "../codemode.js" import { ToolError } from "../tool-error.js" import { ToolRuntimeError } from "../tool-runtime.js" -import { type AstNode, formatLocation, PendingThrow, Throw, sourceLocation, typeError } from "./model.js" +import { type AstNode, formatLocation, PendingThrow, Throw, sourceLocation } from "./model.js" import { containsRuntimeReference } from "./references.js" import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js" import { constructor, methods, prototypeFrom, receiver } from "./native.js" @@ -20,9 +19,8 @@ import { type Value, } from "./objects.js" import type { Interpreter } from "./interpreter.js" -import { Push } from "./signals.js" +import { Invoke } from "./signals.js" import { withPrimitives } from "./callback.js" -import { Drain, drain } from "../machine/natives.js" import { formatValue } from "../stdlib/console.js" import { inPrelude } from "./source.js" @@ -159,27 +157,6 @@ export const createAggregateErrorValue = ( return value } -// State: the message, the prototype, the options, and the errors so far. -const AggregateErrors = consumer({ - name: "new AggregateError", - notIterable: () => { - throw typeError("new AggregateError(...) expects a synchronous iterable of errors.") - }, - take: (frame, value) => { - ;(frame.slots[STATE + 3] as Array).push(value) - }, - done: (task, frame) => { - const slots = frame.slots - const created = createAggregateErrorValue( - task.ctx, - slots[STATE + 3] as Array, - slots[STATE] as string, - slots[STATE + 1] as Obj, - ) - return withCause(created, slots[STATE + 2] as Value) - }, -}) - // ES2022 `new Error(message, { cause })`: installed only when the options object has the property at all. const withCause = (created: ErrorObj, options: Value) => { if (options instanceof Obj && has(options, "cause")) define(created, "cause", get(options, "cause"), hidden) @@ -198,8 +175,8 @@ export const errorGlobal = (type: ErrorType, ctx: Interpreter) => { "string", message === undefined ? [] : [message], ([text]) => { - const state = [message === undefined ? "" : coerceToString(text), proto, args[2], []] - return new Push(Drain, drain(AggregateErrors, args[0], state)) + const created = withCause(createErrorValue(proto, message === undefined ? "" : coerceToString(text)), args[2]) + return new Invoke(ctx.prelude.fn("aggregateError"), undefined, [created, args[0]], undefined) }, true, ) diff --git a/packages/codemode/src/prelude/helpers.ts b/packages/codemode/src/prelude/helpers.ts index bb214b5adcf..4aeb58f6395 100644 --- a/packages/codemode/src/prelude/helpers.ts +++ b/packages/codemode/src/prelude/helpers.ts @@ -1,7 +1,9 @@ import { isSupportedCallback, requireCallback } from "../interpreter/callback.js" import type { Builtins } from "../interpreter/intrinsics.js" import { iterateDirect } from "../machine/iteration.js" +import { finishBytes } from "../stdlib/bytes.js" import { weakKey } from "../stdlib/collections.js" +import { buildPairs } from "../stdlib/url.js" import type { Interpreter } from "../interpreter/interpreter.js" import { checkArrayLength } from "../interpreter/limits.js" import { invalidData, IteratorSymbol, PendingThrow } from "../interpreter/model.js" @@ -10,13 +12,13 @@ import { Arr, Bytes, coerceToString, + data, define, + hidden, getOwn, IteratorObj, positionIterator, - coerceToInteger, coerceToNumber, - GeneratorObj, get, HeadersObj, MapObj, @@ -30,7 +32,7 @@ import { WeakSetObj, } from "../interpreter/objects.js" import { builtinCursor } from "../interpreter/operations.js" -import { describeValue, isOpaque } from "../interpreter/references.js" +import { containsOpaqueReference, describeValue, isOpaque } from "../interpreter/references.js" import { Invoke } from "../interpreter/signals.js" // The prelude's privileged helpers: leaf built-ins (they never call program code) bound only in the prelude's scope. @@ -68,7 +70,43 @@ export const helpers = (ctx: Interpreter): Array receiver(classes[kind as keyof typeof classes], value, String(method)), ], - $IsObject: [1, ([value]) => value instanceof Obj], + /** Whether `value` is an object; with `data`, one holding no opaque reference. */ + $IsObject: [2, ([value, data]) => value instanceof Obj && !(data === true && containsOpaqueReference(value))], + /** Whether `for...of` can iterate `value`. */ + $IsIterable: [ + 1, + ([value]) => { + if (builtinCursor(builtins, value) !== undefined) return true + if (!(value instanceof Obj)) return false + const method = get(value, IteratorSymbol) + return method !== undefined && method !== null + }, + ], + $Describe: [1, ([value]) => describeValue(value)], + /** Defines an own data property, enumerable or hidden, and returns the object. */ + $Define: [ + 4, + ([target, key, value, enumerable]) => { + define(target as Obj, key as string, value, enumerable === true ? data : hidden) + return target + }, + ], + $SumPrecise: [1, ([numbers]) => Math.sumPrecise((numbers as Arr).items as Array)], + /** A Uint8Array of the values (see `collectBytes`). */ + $Bytes: [ + 3, + ([values, into, offset]) => { + const items = (values as Arr).items + checkArrayLength(items.length) + return finishBytes(Uint8Array.from(items, coerceToNumber), into as Obj, offset as number) + }, + ], + /** Headers or URLSearchParams from the pairs `readPairs` read, or from a record. */ + $Pairs: [ + 4, + ([kind, proto, value, label]) => + buildPairs({ kind: kind as "Headers", proto: proto as Obj }, value, String(label)), + ], $IsCallable: [1, ([value]) => isSupportedCallback(value)], /** An iterator object as `for...of` steps it, reading `next` once (GetIteratorDirect); an array's values. */ $Direct: [ diff --git a/packages/codemode/src/prelude/source.ts b/packages/codemode/src/prelude/source.ts index eb5390a6a9d..69a8fa04b01 100644 --- a/packages/codemode/src/prelude/source.ts +++ b/packages/codemode/src/prelude/source.ts @@ -136,4 +136,68 @@ function setMethod(target, other, name) { } return result === undefined ? true : result } +function Object$fromEntries(entries) { + const result = {} + for (const entry of entries) { + if (!$IsObject(entry, true)) $Throw("TypeError", "Object.fromEntries expects [key, value] entry objects.") + $Define(result, String(entry[0]), entry[1], true) + } + return result +} + +function Math$sumPrecise(items) { + const numbers = [] + for (const item of items) { + if (typeof item !== "number") $Throw("TypeError", "Math.sumPrecise expects an iterable of numbers.") + numbers[numbers.length] = item + } + return $SumPrecise(numbers) +} + +// new AggregateError: the error is made (message and cause) before its errors are read. +function aggregateError(error, errors) { + const list = [] + for (const item of errors) list[list.length] = item + return $Define(error, "errors", list, false) +} + +// A Uint8Array's bytes from an iterable: a new one with prototype into, or set into the Uint8Array into at +// offset. As in TypedArray.from, a mapper sees the original values, after the source is consumed in full. +function collectBytes(source, name, mapper, thisArg, into, offset) { + if (!$IsIterable(source)) $Throw("TypeError", name + " expects a Uint8Array, an array, or an iterable of numbers, received " + $Describe(source) + ".") + const values = [] + for (const item of source) values[values.length] = item + let index = 0 + if (mapper !== undefined) { + while (index < values.length) { + values[index] = $Call(mapper, thisArg, values[index], index) + index++ + } + } + return $Bytes(values, into, offset) +} + +// Headers and URLSearchParams from iterable [name, value] pairs, read in full as strings before any is checked; from a +// record of names to values when init is not iterable. +function readPairs(init, label, kind, proto) { + if (!$IsIterable(init)) return $Pairs(kind, proto, init, label) + const pairs = [] + let strings + for (const pair of init) { + if (!$IsIterable(pair)) $Throw("TypeError", label + " expects iterable [name, value] pairs.") + strings = [] + for (const item of pair) strings[strings.length] = String(item) + pairs[pairs.length] = strings + } + return $Pairs(kind, proto, pairs, label) +} + +// Headers and URLSearchParams forEach: the callback sees each pair as listed when it started. +function pairsForEach(target, pairs, callback, thisArg) { + let index = 0 + while (index < pairs.length) { + $Call(callback, thisArg, pairs[index][1], pairs[index][0], target) + index++ + } +} ` diff --git a/packages/codemode/src/stdlib/bytes.ts b/packages/codemode/src/stdlib/bytes.ts index 4201d5249cf..26783a2019e 100644 --- a/packages/codemode/src/stdlib/bytes.ts +++ b/packages/codemode/src/stdlib/bytes.ts @@ -1,7 +1,4 @@ -import { consumer, nativeKind, STATE } from "../machine/native-kind.js" -import { Push } from "../interpreter/signals.js" -import { PENDING } from "../machine/frame.js" -import { Drain, drain } from "../machine/natives.js" +import { Invoke } from "../interpreter/signals.js" import { checkArrayLength, checkStringLength } from "../interpreter/limits.js" import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js" import { IteratorSymbol, rangeError, syntaxError, typeError } from "../interpreter/model.js" @@ -23,82 +20,37 @@ import { requireCallback } from "../interpreter/callback.js" import type { Interpreter } from "../interpreter/interpreter.js" import { callbackMethods, sortArray } from "./array.js" -/** What becomes of the bytes `collectBytes` reads: a new Uint8Array (with its prototype), or bytes set into one. */ -type BytesInto = - | { readonly kind: "new"; readonly proto: Obj } - | { readonly kind: "set"; readonly target: Bytes; readonly offset: number } - /** - * The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. As in - * TypedArray.from, a mapper sees the original values, after the source is consumed in full. + * The bytes a Uint8Array, array, or other iterable of numbers describes, read in the prelude: a new Uint8Array with + * prototype `into`, or set into the Uint8Array `into` at `offset`. The host array clamps each value. */ -const collectBytes = (source: Value, name: string, into: BytesInto, mapper?: Value, thisArg?: Value) => { +const collectBytes = ( + ctx: Interpreter, + source: Value, + name: string, + into: Obj, + offset: number, + mapper?: Value, + thisArg?: Value, +) => { const callback = mapper === undefined ? undefined : requireCallback(mapper, name) - if (source instanceof Bytes && callback === undefined) return finishBytes(into, new Uint8Array(source.bytes)) - return new Push(Drain, drain(CollectBytes, source, [name, into, callback, thisArg, [], 0])) + if (source instanceof Bytes && callback === undefined) return finishBytes(new Uint8Array(source.bytes), into, offset) + return new Invoke(ctx.prelude.fn("collectBytes"), undefined, [source, name, callback, thisArg, into, offset], undefined) } -const finishBytes = (into: BytesInto, bytes: Uint8Array): Value => { - if (into.kind === "new") return new Bytes(into.proto, bytes) - const offset = into.offset - if (!Number.isInteger(offset) || offset < 0 || bytes.length + offset > into.target.bytes.length) { +export const finishBytes = (bytes: Uint8Array, into: Obj, offset: number): Value => { + if (!(into instanceof Bytes)) return new Bytes(into, bytes) + if (!Number.isInteger(offset) || offset < 0 || bytes.length + offset > into.bytes.length) { throw rangeError("Uint8Array.set: the source does not fit at that offset.") } - into.target.bytes.set(bytes, offset) + into.bytes.set(bytes, offset) return undefined } -// State: the method's name, where the bytes go, the mapper and its `this`, the values, and the next one to map. -const CollectBytes = consumer({ - name: "Uint8Array bytes", - notIterable: (_task, frame, source) => { - throw typeError( - `${frame.slots[STATE] as string} expects a Uint8Array, an array, or an iterable of numbers, received ${describeValue(source)}.`, - ) - }, - take: (frame, value) => { - const values = frame.slots[STATE + 4] as Array - values.push(value) - checkArrayLength(values.length) - }, - done: (task, frame) => { - const slots = frame.slots - if (slots[STATE + 2] === undefined) { - return finishBytes( - slots[STATE + 1] as BytesInto, - Uint8Array.from(slots[STATE + 4] as Array, coerceToNumber), - ) - } - return task.push(MapBytes, frame.node, slots.slice(STATE)) - }, -}) - -// Slots: as `CollectBytes`'s state, with the mapped values so far. -const MapBytes = nativeKind("Uint8Array mapper", (task, frame, input) => { - const slots = frame.slots - const values = slots[4] as Array - const mapped = (slots[6] ??= []) as Array - let index = slots[5] as number - if (frame.phase === 1) { - mapped.push(input as Value) - index++ - } - for (; index < values.length; index++) { - const result = task.call(slots[2] as Value, slots[3] as Value, [values[index], index], frame.node) - if (result === PENDING) { - slots[5] = index - frame.phase = 1 - return PENDING - } - mapped.push(result) - } - return finishBytes(slots[1] as BytesInto, Uint8Array.from(mapped, coerceToNumber)) -}) - -const constructBytes = (args: Array, proto: Obj) => { +const constructBytes = (ctx: Interpreter, args: Array, proto: Obj) => { const source = args[0] if (source !== null && typeof source === "object") - return collectBytes(source, "new Uint8Array(...)", { kind: "new", proto }) + return collectBytes(ctx, source, "new Uint8Array(...)", proto, 0) const length = source === undefined ? 0 : coerceToNumber(source) if (!Number.isInteger(length) || length < 0) throw rangeError(`Invalid typed array length: ${coerceToString(source)}`) checkArrayLength(length) @@ -113,7 +65,7 @@ export const uint8ArrayGlobal = (ctx: Interpreter) => { name: "Uint8Array", length: 3, call: requiresNew("Uint8Array"), - construct: (args, newTarget) => constructBytes(args, prototypeFrom(newTarget, proto)), + construct: (args, newTarget) => constructBytes(ctx, args, prototypeFrom(newTarget, proto)), }) const decode = (name: string, args: Array, from: (text: string) => Uint8Array) => { if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`) @@ -124,7 +76,7 @@ export const uint8ArrayGlobal = (ctx: Interpreter) => { } } methods(builtins, uint8Array, [ - ["from", 1, (_, args) => collectBytes(args[0], "Uint8Array.from", { kind: "new", proto }, args[1], args[2])], + ["from", 1, (_, args) => collectBytes(ctx, args[0], "Uint8Array.from", proto, 0, args[1], args[2])], ["of", 0, (_, args) => wrap(Uint8Array.from(args, coerceToNumber))], ["fromBase64", 1, (_, args) => decode("fromBase64", args, (text) => Uint8Array.fromBase64(text))], ["fromHex", 1, (_, args) => decode("fromHex", args, (text) => Uint8Array.fromHex(text))], @@ -153,7 +105,7 @@ export const uint8ArrayGlobal = (ctx: Interpreter) => { 1, (thisValue, args) => { const target = self(thisValue, "set") - return collectBytes(args[0], "Uint8Array.set", { kind: "set", target, offset: optNumber(args[1]) ?? 0 }) + return collectBytes(ctx, args[0], "Uint8Array.set", target, optNumber(args[1]) ?? 0) }, ], [ diff --git a/packages/codemode/src/stdlib/headers.ts b/packages/codemode/src/stdlib/headers.ts index a1806e4ad3c..3fc3c1b8d7b 100644 --- a/packages/codemode/src/stdlib/headers.ts +++ b/packages/codemode/src/stdlib/headers.ts @@ -15,9 +15,9 @@ import { requireCallback } from "../interpreter/callback.js" import type { Interpreter } from "../interpreter/interpreter.js" import { attempt, pairsForEach, readPairs } from "./url.js" -const constructHeaders = (init: Value, proto: Obj) => { +const constructHeaders = (ctx: Interpreter, init: Value, proto: Obj) => { if (init === undefined) return new HeadersObj(proto, new Headers()) - if (init instanceof Obj) return readPairs(init, "new Headers(...)", { kind: "Headers", proto }) + if (init instanceof Obj) return readPairs(ctx, init, "new Headers(...)", { kind: "Headers", proto }) throw typeError("new Headers(...) expects a record of names to values, iterable [name, value] pairs, or Headers.") } @@ -27,7 +27,7 @@ export const headersGlobal = (ctx: Interpreter) => { const headers = constructor(builtins, proto, { name: "Headers", call: requiresNew("Headers"), - construct: (args, newTarget) => constructHeaders(args[0], prototypeFrom(newTarget, proto)), + construct: (args, newTarget) => constructHeaders(ctx, args[0], prototypeFrom(newTarget, proto)), }) const self = (thisValue: Value, name: string) => receiver(HeadersObj, thisValue, `Headers.prototype.${name}`) const wrap = (items: Array) => new Arr(builtins.Array, items) @@ -95,7 +95,7 @@ export const headersGlobal = (ctx: Interpreter) => { requireArgs("forEach", args, 1) const target = self(thisValue, "forEach") const callback = requireCallback(args[0], "Headers.forEach") - return pairsForEach(target, Array.from(target.headers.entries()), callback, args[1]) + return pairsForEach(ctx, target, Array.from(target.headers.entries()), callback, args[1]) }, ], ]) diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 5140ec9690d..1cdd0f2abf6 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -1,10 +1,6 @@ -import { consumer, STATE } from "../machine/native-kind.js" import { constants, type Method, methods } from "../interpreter/native.js" -import { typeError } from "../interpreter/model.js" import { Obj, coerceToNumber } from "../interpreter/objects.js" import { withPrimitives } from "../interpreter/callback.js" -import { Push } from "../interpreter/signals.js" -import { Drain, drain } from "../machine/natives.js" import type { Interpreter } from "../interpreter/interpreter.js" // Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types. @@ -81,20 +77,6 @@ export const mathGlobal = (ctx: Interpreter) => { unary("fround", Math.fround), unary("clz32", Math.clz32), binary("imul", Math.imul), - ["sumPrecise", 1, (_, args) => new Push(Drain, drain(SumPrecise, args[0], [[]]))], ]) return math } - -// State: the numbers so far. -const SumPrecise = consumer({ - name: "Math.sumPrecise", - notIterable: () => { - throw typeError("Math.sumPrecise expects a synchronous iterable.") - }, - each: (_task, frame, value) => { - if (typeof value !== "number") throw typeError("Math.sumPrecise expects an iterable of numbers.") - ;(frame.slots[STATE] as Array).push(value) - }, - done: (_task, frame) => Math.sumPrecise(frame.slots[STATE] as Array), -}) diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index 4a20715d070..854d211a329 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,16 +1,11 @@ -import { consumer, STATE } from "../machine/native-kind.js" -import { Push } from "../interpreter/signals.js" -import { Drain, drain } from "../machine/natives.js" import { constructor, methods, receiver } from "../interpreter/native.js" import { type AstNode, AsyncIteratorSymbol, invalidData, IteratorSymbol, typeError } from "../interpreter/model.js" import { - define, entries, enumerableKeys, getOwn, hasOwn, hasPrototype, - hidden, keys, own, ownKeys, @@ -23,7 +18,7 @@ import { type Value, } from "../interpreter/objects.js" import { primitivePrototype } from "../interpreter/intrinsics.js" -import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js" +import { describeValue, rejectCircularInsertion } from "../interpreter/references.js" import { invokeMethod } from "../interpreter/callback.js" import type { Interpreter } from "../interpreter/interpreter.js" import { ToolReference } from "../tool-runtime.js" @@ -67,21 +62,6 @@ export const objectAssign = (ctx: Interpreter, args: Array): Value return target } -// State: the object being filled. A rejected entry closes the source iterator. -const FromEntries = consumer({ - name: "Object.fromEntries", - notIterable: () => { - throw typeError("Object.fromEntries expects a synchronous iterable of entries.") - }, - each: (_task, frame, entry) => { - if (!(entry instanceof Obj) || containsOpaqueReference(entry)) { - throw typeError("Object.fromEntries expects [key, value] entry objects.") - } - define(frame.slots[STATE] as Obj, coerceToString(getOwn(entry, 0)), getOwn(entry, 1)) - }, - done: (_task, frame) => frame.slots[STATE] as Obj, -}) - const classTag = (value: Value): string => { if (value === null) return "Null" if (value === undefined) return "Undefined" @@ -172,7 +152,6 @@ export const objectGlobal = (ctx: Interpreter) => { ["hasOwn", 2, (_, args) => hasOwn(enumerableSource(ctx, "Object.hasOwn(...)", args[0]), propertyKey(args[1]))], ["is", 2, (_, args) => Object.is(args[0], args[1])], ["assign", 2, (_, args) => objectAssign(ctx, args)], - ["fromEntries", 1, (_, args) => new Push(Drain, drain(FromEntries, args[0], [new Obj(ctx.builtins.Object)]))], ["freeze", 1, (_, args) => restrict("freeze", args[0])], ["seal", 1, (_, args) => restrict("seal", args[0])], ["preventExtensions", 1, (_, args) => restrict("preventExtensions", args[0])], diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts index ed5e393390b..07c495498af 100644 --- a/packages/codemode/src/stdlib/url.ts +++ b/packages/codemode/src/stdlib/url.ts @@ -1,7 +1,4 @@ -import { consumer, nativeKind, STATE } from "../machine/native-kind.js" -import { Push } from "../interpreter/signals.js" -import { PENDING } from "../machine/frame.js" -import { Drain, drain } from "../machine/natives.js" +import { Invoke } from "../interpreter/signals.js" import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js" import { IteratorSymbol, PendingThrow, typeError, uriError } from "../interpreter/model.js" import { @@ -128,47 +125,26 @@ export const urlGlobal = (ctx: Interpreter) => { export type PairsInto = { readonly kind: "Headers" | "URLSearchParams"; readonly proto: Obj } /** - * Reads a synchronous iterable of `[name, value]` pairs as strings, then builds `into` from them; a record of names to - * values when `init` is not iterable. As in WebIDL, the whole sequence is converted before any pair's length is - * checked. + * Reads a synchronous iterable of `[name, value]` pairs as strings (in the prelude), then builds `into` from them; a + * record of names to values when `init` is not iterable. As in WebIDL, the whole sequence is converted before any + * pair's length is checked. */ -export const readPairs = (init: Value, label: string, into: PairsInto) => - new Push(Drain, drain(Pairs, init, [label, [], into])) +export const readPairs = (ctx: Interpreter, init: Value, label: string, into: PairsInto) => + new Invoke(ctx.prelude.fn("readPairs"), undefined, [init, label, into.kind, into.proto], undefined) -// State: the label, the pairs so far, and what they build. -const Pairs = consumer({ - name: "pairs", - notIterable: (_task, frame, init) => fromRecord(frame.slots[STATE + 2] as PairsInto, init), - each: (task, frame, value) => task.push(Drain, frame.node, drain(Pair, value, [frame.slots[STATE], []])), - resume: (_task, frame, input) => { - ;(frame.slots[STATE + 1] as Array>).push(input as Array) - }, - done: (_task, frame) => { - const pairs = frame.slots[STATE + 1] as Array> - const into = frame.slots[STATE + 2] as PairsInto - if (pairs.some((entry) => entry.length !== 2)) - throw typeError(`${frame.slots[STATE] as string} expects iterable [name, value] pairs.`) - if (into.kind === "URLSearchParams") { - return new URLSearchParamsObj(into.proto, new URLSearchParams(pairs as Array<[string, string]>)) - } - return new HeadersObj( - into.proto, - attempt(() => new Headers(pairs as Array<[string, string]>)), - ) - }, -}) - -// One pair's strings; a failing conversion closes the pair's iterator. State: the label and the strings so far. -const Pair = consumer({ - name: "pair", - notIterable: (_task, frame) => { - throw typeError(`${frame.slots[STATE] as string} expects iterable [name, value] pairs.`) - }, - each: (_task, frame, value) => { - ;(frame.slots[STATE + 1] as Array).push(coerceToString(value)) - }, - done: (_task, frame) => frame.slots[STATE + 1] as never, -}) +/** What `readPairs` makes of the pairs it read (an array of arrays of strings), or of a record. */ +export const buildPairs = (into: PairsInto, value: Value, label: string): Value => { + if (!(value instanceof Arr)) return fromRecord(into, value) + const pairs = value.items.map((pair) => (pair as Arr).items as Array) + if (pairs.some((entry) => entry.length !== 2)) throw typeError(`${label} expects iterable [name, value] pairs.`) + if (into.kind === "URLSearchParams") { + return new URLSearchParamsObj(into.proto, new URLSearchParams(pairs as Array<[string, string]>)) + } + return new HeadersObj( + into.proto, + attempt(() => new Headers(pairs as Array<[string, string]>)), + ) +} const fromRecord = (into: PairsInto, init: Value): Value => { if (into.kind === "Headers") { @@ -202,27 +178,21 @@ export const attempt = (run: () => T): T => { } /** Headers and URLSearchParams `forEach`: the callback sees each `[name, value]` pair as listed when it started. */ -export const pairsForEach = (target: Value, pairs: Array<[string, string]>, callback: Callable, thisArg: Value) => - new Push(PairsForEach, [target, pairs, callback, thisArg, 0]) +export const pairsForEach = ( + ctx: Interpreter, + target: Value, + pairs: Array<[string, string]>, + callback: Callable, + thisArg: Value, +) => { + const list = new Arr( + ctx.builtins.Array, + pairs.map((pair) => new Arr(ctx.builtins.Array, pair)), + ) + return new Invoke(ctx.prelude.fn("pairsForEach"), undefined, [target, list, callback, thisArg], undefined) +} -// Slots: the receiver, its pairs, the callback, its `this`, and the next pair. -const PairsForEach = nativeKind("pairs forEach", (task, frame) => { - const slots = frame.slots - const pairs = slots[1] as Array<[string, string]> - let index = slots[4] as number - if (frame.phase === 1) index++ - frame.phase = 1 - for (; index < pairs.length; index++) { - const [key, value] = pairs[index]! - if (task.call(slots[2] as Callable, slots[3] as Value, [value, key, slots[0] as Value], frame.node) === PENDING) { - slots[4] = index - return PENDING - } - } - return undefined -}) - -const constructURLSearchParams = (init: Value, proto: Obj) => { +const constructURLSearchParams = (ctx: Interpreter, init: Value, proto: Obj) => { const wrap = (params: URLSearchParams) => new URLSearchParamsObj(proto, params) if (init === undefined) return wrap(new URLSearchParams()) if (init instanceof URLSearchParamsObj) return wrap(new URLSearchParams(init.params)) @@ -230,7 +200,7 @@ const constructURLSearchParams = (init: Value, proto: Obj) => { if (init === null || typeof init === "number" || typeof init === "boolean") { return wrap(new URLSearchParams(coerceToString(init))) } - return readPairs(init, "new URLSearchParams(...)", { kind: "URLSearchParams", proto }) + return readPairs(ctx, init, "new URLSearchParams(...)", { kind: "URLSearchParams", proto }) } export const urlSearchParamsGlobal = (ctx: Interpreter) => { @@ -239,7 +209,7 @@ export const urlSearchParamsGlobal = (ctx: Interpreter) => { const searchParams = constructor(builtins, proto, { name: "URLSearchParams", call: requiresNew("URLSearchParams"), - construct: (args, newTarget) => constructURLSearchParams(args[0], prototypeFrom(newTarget, proto)), + construct: (args, newTarget) => constructURLSearchParams(ctx, args[0], prototypeFrom(newTarget, proto)), }) const self = (thisValue: Value, name: string) => receiver(URLSearchParamsObj, thisValue, `URLSearchParams.prototype.${name}`) @@ -325,7 +295,7 @@ export const urlSearchParamsGlobal = (ctx: Interpreter) => { requireArgs("forEach", args, 1) const target = self(thisValue, "forEach") const callback = requireCallback(args[0], "URLSearchParams.forEach") - return pairsForEach(target, Array.from(target.params.entries()), callback, args[1]) + return pairsForEach(ctx, target, Array.from(target.params.entries()), callback, args[1]) }, ], ])