refactor(codemode): move fromEntries, sumPrecise, AggregateError, Uint8Array sources, and Headers/URLSearchParams pairs to the prelude

This commit is contained in:
James Long
2026-11-05 15:24:01 +00:00
Unverified
parent d8504a97b0
commit fa7b94f2fe
8 changed files with 174 additions and 212 deletions
+4 -27
View File
@@ -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 = <R>(
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<Value>).push(value)
},
done: (task, frame) => {
const slots = frame.slots
const created = createAggregateErrorValue(
task.ctx,
slots[STATE + 3] as Array<Value>,
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 = <R>(type: ErrorType, ctx: Interpreter<R>) => {
"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,
)
+42 -4
View File
@@ -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<unknown>): Array<readonly [string, Valu
3,
([value, kind, method]) => receiver<Obj>(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<number>)],
/** 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: [
+64
View File
@@ -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++
}
}
`
+23 -71
View File
@@ -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 = <R>(
ctx: Interpreter<R>,
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<Value>
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<Value>, 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<Value>
const mapped = (slots[6] ??= []) as Array<Value>
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<Value>, proto: Obj) => {
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<Value>, 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 = <R>(ctx: Interpreter<R>) => {
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<Value>, from: (text: string) => Uint8Array) => {
if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`)
@@ -124,7 +76,7 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
}
}
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 = <R>(ctx: Interpreter<R>) => {
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)
},
],
[
+4 -4
View File
@@ -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 = <R>(ctx: Interpreter<R>, 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 = <R>(ctx: Interpreter<R>) => {
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<Value>) => new Arr(builtins.Array, items)
@@ -95,7 +95,7 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
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])
},
],
])
-18
View File
@@ -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 = <R>(ctx: Interpreter<R>) => {
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<number>).push(value)
},
done: (_task, frame) => Math.sumPrecise(frame.slots[STATE] as Array<number>),
})
+1 -22
View File
@@ -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 = <R>(ctx: Interpreter<R>, args: Array<Value>): 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 = <R>(ctx: Interpreter<R>) => {
["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])],
+36 -66
View File
@@ -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 = <R>(ctx: Interpreter<R>) => {
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 = <R>(ctx: Interpreter<R>, 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<Array<string>>).push(input as Array<string>)
},
done: (_task, frame) => {
const pairs = frame.slots[STATE + 1] as Array<Array<string>>
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<string>).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<string>)
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 = <T>(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 = <R>(
ctx: Interpreter<R>,
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 = <R>(ctx: Interpreter<R>, 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 = <R>(ctx: Interpreter<R>) => {
@@ -239,7 +209,7 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
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 = <R>(ctx: Interpreter<R>) => {
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])
},
],
])