mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(codemode): add Uint8Array, drop the Program prefix, and give built-ins one context (#49076)
This commit is contained in:
@@ -25,9 +25,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
cannot replace a prototype; `JSON.stringify` still emits the key, like JS, since a string cannot pollute.
|
||||
- [x] Values `JSON.stringify` would flatten to `{}` cross the host boundary in a useful form instead: a Set as an
|
||||
array, a RegExp as `"/source/flags"`, a URLSearchParams as its query string. A Map still crosses as `{}`.
|
||||
Functions, generators, promises, and extension handles are rejected with a hint. In-program `JSON.stringify`
|
||||
keeps JS behavior for all of these.
|
||||
- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode.
|
||||
Functions, generators, promises, extension handles, and a Uint8Array are rejected with a hint. In-program
|
||||
`JSON.stringify` keeps JS behavior for all of these.
|
||||
- [x] Live Date, RegExp, Map, Set, URL, URLSearchParams, and Uint8Array values inside CodeMode.
|
||||
- [x] Tool calls through the host-provided `tools` tree only.
|
||||
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
|
||||
shadowable by program declarations like other globals.
|
||||
@@ -424,13 +424,31 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`entries`, `toString`, and `size`.
|
||||
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
|
||||
|
||||
## Uint8Array
|
||||
|
||||
The only binary type. Bytes stay inside the program or cross to extensions; the tool boundary rejects them with a
|
||||
hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
|
||||
- [x] `new Uint8Array(length | array | iterable | Uint8Array)`, `Uint8Array.from`, `Uint8Array.of`, `fromBase64`,
|
||||
and `fromHex`. Lengths are capped like arrays.
|
||||
- [x] Index reads and writes with JS byte semantics: values wrap modulo 256, out-of-range writes are ignored, indexes
|
||||
cannot be deleted. `length` is a prototype accessor, so `Object.keys` lists only indexes.
|
||||
- [x] `at`, `slice`, `subarray` (a view on the same bytes), `set`, `fill`, `reverse`, `indexOf`, `lastIndexOf`,
|
||||
`includes`, `join`, `toString`, `toBase64`, `toHex`, and materialized `keys`, `values`, and `entries` arrays.
|
||||
- [x] Spread, destructuring, `for...of`, `yield*`, `Array.from`, and `new Set(bytes)`. `Array.isArray` is false.
|
||||
- [x] String coercion joins with commas; `JSON.stringify` gives `{"0":1,...}`; `console.log` prints
|
||||
`Uint8Array(n) [...]`.
|
||||
- [ ] Callback methods (`forEach`, `map`, `filter`, `find`, `reduce`, ...); use `Array.from(bytes, fn)` meanwhile.
|
||||
- [ ] `ArrayBuffer`, `DataView`, and other typed arrays.
|
||||
|
||||
## Web platform helpers
|
||||
|
||||
- [x] `atob` and `btoa` with forgiving-base64 decoding and WebIDL string conversion; invalid input throws a
|
||||
`TypeError`, since there is no `DOMException`.
|
||||
- [x] `crypto.randomUUID()`.
|
||||
- [ ] `crypto.getRandomValues` and `crypto.subtle`, `TextEncoder`/`TextDecoder`, and `Blob`: these need a binary
|
||||
value type, which the JSON-like data model does not have yet.
|
||||
- [x] `crypto.randomUUID()` and `crypto.getRandomValues(uint8Array)`.
|
||||
- [x] `TextEncoder` and `TextDecoder` for UTF-8 only: any other label is a `RangeError`. `TextDecoder` accepts the
|
||||
`fatal` and `ignoreBOM` options; `decode` takes a Uint8Array or nothing.
|
||||
- [ ] `crypto.subtle`, `Blob`, and `TextDecoder` streaming or non-UTF-8 encodings.
|
||||
|
||||
## Extensions
|
||||
|
||||
@@ -444,7 +462,8 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
The same host instance is always the same handle within a run, so identity and `instanceof` hold. Handles
|
||||
cannot cross the data boundary: returning, stringifying, throwing, or passing one to a tool fails.
|
||||
- [x] Every value crossing in either direction is converted, never shared: plain objects and arrays are copied,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Map`, and `Set` become fresh copies with their contents converted,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Map`, `Set`, and `Uint8Array` become fresh copies with their
|
||||
contents converted (a host `ArrayBuffer` comes in as a `Uint8Array`; other typed arrays cannot come out),
|
||||
errors cross as errors with their name and message, and a `__proto__` key is dropped. Functions, generators,
|
||||
un-awaited promises, and symbols cannot be passed in; an instance of an unexposed class, a symbol, or a BigInt
|
||||
cannot come out.
|
||||
@@ -456,7 +475,6 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
that run only. Data properties on a class or prototype are not exposed, since a program write would change the
|
||||
host class itself; expose one through an accessor.
|
||||
- [ ] Program functions as arguments to extension code (callbacks such as `forEach`).
|
||||
- [ ] Binary values (`Uint8Array`, `ArrayBuffer`) at the extension boundary; needs the binary value type above.
|
||||
|
||||
## Errors and diagnostics
|
||||
|
||||
|
||||
@@ -148,6 +148,6 @@ export const make = <const Provided extends Record<string, unknown> = {}>(
|
||||
}
|
||||
return {
|
||||
catalog: prepared.catalog,
|
||||
execute: (code) => executeProgram(code, prepared, limits, options, (host) => extensionGlobals(host, extensions)),
|
||||
execute: (code) => executeProgram(code, prepared, limits, options, (ctx) => extensionGlobals(ctx, extensions)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as Data from "./data.js"
|
||||
|
||||
import type { DiagnosticKind } from "./codemode.js"
|
||||
import type { Prototypes } from "./interpreter/intrinsics.js"
|
||||
import type { Builtins } from "./interpreter/intrinsics.js"
|
||||
import {
|
||||
Callable,
|
||||
define,
|
||||
@@ -9,18 +9,19 @@ import {
|
||||
get,
|
||||
isWrapper,
|
||||
parseArrayIndex,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramError,
|
||||
ProgramGenerator,
|
||||
ProgramHandle,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramRegExp,
|
||||
ProgramSet,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
GeneratorObj,
|
||||
Handle,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "./interpreter/objects.js"
|
||||
|
||||
export const MAX_VALUE_DEPTH = 32
|
||||
@@ -44,15 +45,15 @@ export class ToolRuntimeError extends Error {
|
||||
* Map, Set, URL, and URLSearchParams become their built-in wrappers, and host objects and arrays
|
||||
* are copied.
|
||||
*/
|
||||
export const toProgram = (protos: Prototypes, value: unknown, label: string): unknown =>
|
||||
copy(value, label, "program", 0, new Set(), protos)
|
||||
export const toProgram = (builtins: Builtins, value: unknown, label: string): unknown =>
|
||||
copy(value, label, "program", 0, new Set(), builtins)
|
||||
|
||||
/**
|
||||
* Brings host data into the program: Date and URL become strings, other host collections become
|
||||
* empty objects, and objects become program copies. Used for tool results and parsed JSON.
|
||||
*/
|
||||
export const fromData = (protos: Prototypes, value: unknown, label: string): unknown =>
|
||||
copy(value, label, "data", 0, new Set(), protos)
|
||||
export const fromData = (builtins: Builtins, value: unknown, label: string): unknown =>
|
||||
copy(value, label, "data", 0, new Set(), builtins)
|
||||
|
||||
/**
|
||||
* Takes a program value out as plain JSON: runtime values serialize like `JSON.stringify` would,
|
||||
@@ -72,7 +73,7 @@ export const toData = (
|
||||
boundary = true,
|
||||
): unknown => copy(value, label, undefinedAs, 0, new Set(), undefined, boundary)
|
||||
|
||||
// "program" and "data" build program objects; "json" and "result" build ordinary objects for the host.
|
||||
// "program" and "data" build program objects; "json" and "result" build ordinary objects for the ctx.
|
||||
type Mode = "program" | "data" | "json" | "result"
|
||||
|
||||
const copy = (
|
||||
@@ -81,10 +82,10 @@ const copy = (
|
||||
mode: Mode,
|
||||
depth: number,
|
||||
seen: Set<object>,
|
||||
protos?: Prototypes,
|
||||
builtins?: Builtins,
|
||||
boundary = true,
|
||||
): unknown => {
|
||||
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos, boundary)
|
||||
const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, builtins, boundary)
|
||||
if (depth > MAX_VALUE_DEPTH) {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||
}
|
||||
@@ -94,53 +95,62 @@ const copy = (
|
||||
if (typeof value !== "object") {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
|
||||
}
|
||||
if (value instanceof ProgramPromise) {
|
||||
if (value instanceof PromiseObj) {
|
||||
throw new ToolRuntimeError(
|
||||
"InvalidDataValue",
|
||||
`${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`,
|
||||
)
|
||||
}
|
||||
if ((value instanceof Callable || value instanceof ProgramGenerator) && mode !== "program") {
|
||||
if ((value instanceof Callable || value instanceof GeneratorObj) && mode !== "program") {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
|
||||
}
|
||||
if (value instanceof ProgramHandle && mode !== "program") {
|
||||
if (value instanceof Handle && mode !== "program") {
|
||||
throw new ToolRuntimeError(
|
||||
"InvalidDataValue",
|
||||
`${label} contains a ${value.instance.constructor.name}, which only extension functions accept.`,
|
||||
)
|
||||
}
|
||||
// Host-produced input never holds program objects; one arriving here would come back as a host object.
|
||||
if (value instanceof ProgramObject && mode === "data") {
|
||||
if (value instanceof Obj && mode === "data") {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} must be host data, not a program value.`)
|
||||
}
|
||||
|
||||
if (protos !== undefined && mode === "program") {
|
||||
if (value instanceof ProgramObject) return value
|
||||
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
|
||||
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
|
||||
if (builtins !== undefined && mode === "program") {
|
||||
if (value instanceof Obj) return value
|
||||
if (value instanceof Date) return new DateObj(builtins.Date, value.getTime())
|
||||
if (value instanceof RegExp) return new RegExpObj(builtins.RegExp, value.source, value.flags)
|
||||
if (value instanceof Map) {
|
||||
const wrapped = new ProgramMap(protos.Map)
|
||||
const wrapped = new MapObj(builtins.Map)
|
||||
for (const [key, item] of value.entries()) wrapped.map.set(next(key), next(item))
|
||||
return wrapped
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
const wrapped = new ProgramSet(protos.Set)
|
||||
const wrapped = new SetObj(builtins.Set)
|
||||
for (const item of value.values()) wrapped.set.add(next(item))
|
||||
return wrapped
|
||||
}
|
||||
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
|
||||
if (value instanceof URL) return new URLObj(builtins.URL, builtins.URLSearchParams, new URL(value.href))
|
||||
if (value instanceof URLSearchParams)
|
||||
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
|
||||
return new URLSearchParamsObj(builtins.URLSearchParams, new URLSearchParams(value))
|
||||
}
|
||||
|
||||
if (value instanceof ProgramDate) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
|
||||
if (value instanceof DateObj) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
|
||||
if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : null
|
||||
if (value instanceof ProgramURL) return value.url.href
|
||||
if (value instanceof URLObj) return value.url.href
|
||||
if (value instanceof URL) return value.href
|
||||
if (boundary && protos === undefined) {
|
||||
if (value instanceof ProgramRegExp) return String(value.regex)
|
||||
if (value instanceof ProgramURLSearchParams) return value.params.toString()
|
||||
if (value instanceof ProgramSet) {
|
||||
if (value instanceof Bytes) {
|
||||
if (boundary) {
|
||||
throw new ToolRuntimeError(
|
||||
"InvalidDataValue",
|
||||
`${label} contains a Uint8Array; pass text instead, e.g. \`new TextDecoder().decode(bytes)\` or \`bytes.toBase64()\`.`,
|
||||
)
|
||||
}
|
||||
return Object.fromEntries(value.bytes.entries())
|
||||
}
|
||||
if (boundary && builtins === undefined) {
|
||||
if (value instanceof RegExpObj) return String(value.regex)
|
||||
if (value instanceof URLSearchParamsObj) return value.params.toString()
|
||||
if (value instanceof SetObj) {
|
||||
if (seen.has(value)) throw new ToolRuntimeError("InvalidDataValue", `${label} contains a circular value.`)
|
||||
seen.add(value)
|
||||
const copied = Array.from(value.set, (item) => next(item) ?? null)
|
||||
@@ -156,7 +166,7 @@ const copy = (
|
||||
value instanceof Set ||
|
||||
value instanceof URLSearchParams
|
||||
) {
|
||||
return protos !== undefined ? new ProgramObject(protos.Object) : {}
|
||||
return builtins !== undefined ? new Obj(builtins.Object) : {}
|
||||
}
|
||||
|
||||
if (seen.has(value)) {
|
||||
@@ -164,15 +174,15 @@ const copy = (
|
||||
}
|
||||
seen.add(value)
|
||||
|
||||
if (value instanceof ProgramArray) {
|
||||
if (value instanceof Arr) {
|
||||
const copied = Array.from(value.items, (item) => next(item) ?? null)
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
if (value instanceof ProgramObject) {
|
||||
if (value instanceof Obj) {
|
||||
const copied: Record<string, unknown> = {}
|
||||
// Errors serialize as { name, message, ...own }: both may be inherited, and neither is enumerable in JS.
|
||||
if (value instanceof ProgramError) {
|
||||
if (value instanceof ErrorObj) {
|
||||
defineHost(copied, "name", next(get(value, "name")))
|
||||
defineHost(copied, "message", next(get(value, "message")))
|
||||
}
|
||||
@@ -187,8 +197,8 @@ const copy = (
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (protos !== undefined) {
|
||||
const copied = new ProgramArray(protos.Array, value.map(next))
|
||||
if (builtins !== undefined) {
|
||||
const copied = new Arr(builtins.Array, value.map(next))
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (parseArrayIndex(key) === undefined) define(copied, key, next(item))
|
||||
}
|
||||
@@ -205,8 +215,8 @@ const copy = (
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`)
|
||||
}
|
||||
|
||||
if (protos !== undefined) {
|
||||
const copied = new ProgramObject(protos.Object)
|
||||
if (builtins !== undefined) {
|
||||
const copied = new Obj(builtins.Object)
|
||||
for (const [key, item] of Object.entries(value)) define(copied, key, next(item))
|
||||
seen.delete(value)
|
||||
return copied
|
||||
|
||||
+13
-25
@@ -1,8 +1,8 @@
|
||||
import { Effect, Exit } from "effect"
|
||||
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
|
||||
import type { Prototypes } from "./intrinsics.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { typeError } from "./model.js"
|
||||
import { Callable, get, NativeFunction, ProgramDate, ProgramObject, ProgramPromise } from "./objects.js"
|
||||
import { Callable, get, Native, DateObj, Obj } from "./objects.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
|
||||
export type IteratorCursor<R> = {
|
||||
@@ -10,18 +10,6 @@ export type IteratorCursor<R> = {
|
||||
readonly close: Effect.Effect<void, unknown, R>
|
||||
}
|
||||
|
||||
/** Everything a native function needs from the realm: calling back into the program and its intrinsic objects. */
|
||||
export type Runner<R> = {
|
||||
readonly invokeCallable: (
|
||||
callable: unknown,
|
||||
thisValue: unknown,
|
||||
args: Array<unknown>,
|
||||
) => Effect.Effect<unknown, unknown, R>
|
||||
readonly settlePromise: (promise: ProgramPromise) => Effect.Effect<unknown, unknown, never>
|
||||
readonly syncIterator: (value: unknown) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>
|
||||
readonly prototypes: Prototypes
|
||||
}
|
||||
|
||||
export const preserveConsumerError = <A, R>(
|
||||
cursor: IteratorCursor<R>,
|
||||
effect: Effect.Effect<A, unknown, R>,
|
||||
@@ -37,39 +25,39 @@ export const preserveConsumerError = <A, R>(
|
||||
* default hint as "string", like their `Symbol.toPrimitive`.
|
||||
*/
|
||||
export const toPrimitive = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
value: unknown,
|
||||
hint: "number" | "string" | "default",
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
|
||||
const asString = hint === "string" || (hint === "default" && value instanceof ProgramDate)
|
||||
if (!(value instanceof Obj)) return Effect.succeed(value)
|
||||
const asString = hint === "string" || (hint === "default" && value instanceof DateObj)
|
||||
const order = asString ? ["toString", "valueOf"] : ["valueOf", "toString"]
|
||||
return Effect.gen(function* () {
|
||||
for (const method of order) {
|
||||
const callable = get(value, method)
|
||||
if (!(callable instanceof Callable)) continue
|
||||
const result = yield* runner.invokeCallable(callable, value, [])
|
||||
const result = yield* ctx.call(callable, value, [])
|
||||
if (result === null || (typeof result !== "object" && typeof result !== "function")) return result
|
||||
}
|
||||
throw typeError("Cannot convert object to primitive value.")
|
||||
})
|
||||
}
|
||||
|
||||
export const toPrimitiveString = <R>(runner: Runner<R>, value: unknown) =>
|
||||
Effect.map(toPrimitive(runner, value, "string"), coerceToString)
|
||||
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
Effect.map(toPrimitive(ctx, value, "string"), coerceToString)
|
||||
|
||||
export const toPrimitiveNumber = <R>(runner: Runner<R>, value: unknown) =>
|
||||
Effect.map(toPrimitive(runner, value, "number"), coerceToNumber)
|
||||
export const toPrimitiveNumber = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
Effect.map(toPrimitive(ctx, value, "number"), coerceToNumber)
|
||||
|
||||
// The single acceptance list for callbacks: collections, sort, string replacers,
|
||||
// Array.from mappers, and promise reactions all admit exactly these callables.
|
||||
// Admission means dispatchable, not necessarily invocable: new-requiring
|
||||
// constructors pass the gate and throw a TypeError on call, like JS.
|
||||
export const isSupportedCallback = (value: unknown): value is Callable =>
|
||||
value instanceof Callable && !(value instanceof NativeFunction && !value.callback)
|
||||
value instanceof Callable && !(value instanceof Native && !value.callback)
|
||||
|
||||
export const applyCollectionCallback = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
callback: unknown,
|
||||
name: string,
|
||||
): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
|
||||
@@ -81,5 +69,5 @@ export const applyCollectionCallback = <R>(
|
||||
}
|
||||
throw typeError(`${name} expects a function callback.`)
|
||||
}
|
||||
return (callbackArgs) => runner.invokeCallable(callback, undefined, callbackArgs)
|
||||
return (callbackArgs) => ctx.call(callback, undefined, callbackArgs)
|
||||
}
|
||||
@@ -2,21 +2,12 @@ import { Effect } from "effect"
|
||||
import type { Diagnostic } from "../codemode.js"
|
||||
import { ToolError } from "../tool-error.js"
|
||||
import { toData, ToolRuntimeError } from "../data.js"
|
||||
import { type AstNode, formatLocation, PendingThrow, ProgramThrow, sourceLocation, typeError } from "./model.js"
|
||||
import { type AstNode, formatLocation, PendingThrow, Throw, sourceLocation, typeError } from "./model.js"
|
||||
import { containsRuntimeReference } from "./references.js"
|
||||
import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js"
|
||||
import { constructor, methods, prototypeFrom, receiver } from "./native.js"
|
||||
import {
|
||||
type Callable,
|
||||
define,
|
||||
get,
|
||||
hidden,
|
||||
type NativeFunction,
|
||||
ProgramArray,
|
||||
ProgramError,
|
||||
ProgramObject,
|
||||
} from "./objects.js"
|
||||
import { type Runner } from "./runner.js"
|
||||
import { type Callable, define, get, hidden, type Native, Arr, ErrorObj, Obj } from "./objects.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { coerceToString } from "../stdlib/value.js"
|
||||
|
||||
export const normalizeError = (error: unknown): Diagnostic => {
|
||||
@@ -41,9 +32,9 @@ export const normalizeError = (error: unknown): Diagnostic => {
|
||||
return { kind: "ToolFailure", message: error.message }
|
||||
}
|
||||
|
||||
if (error instanceof ProgramThrow) {
|
||||
if (error instanceof Throw) {
|
||||
const value = error.value
|
||||
if (value instanceof ProgramError) {
|
||||
if (value instanceof ErrorObj) {
|
||||
return value.host ? normalizeError(value.host) : { kind: "ExecutionFailure", message: errorToString(value) }
|
||||
}
|
||||
let message: string
|
||||
@@ -98,22 +89,22 @@ export const locate = (error: unknown, node?: AstNode): unknown => {
|
||||
}
|
||||
|
||||
/** The program value a handler receives for a failure; one failure always yields the same value. */
|
||||
export const materialize = <R>(runner: Runner<R>, thrown: unknown): unknown => {
|
||||
if (thrown instanceof ProgramThrow) return thrown.value
|
||||
const prototypes = runner.prototypes
|
||||
export const materialize = <R>(ctx: Interpreter<R>, thrown: unknown): unknown => {
|
||||
if (thrown instanceof Throw) return thrown.value
|
||||
const builtins = ctx.builtins
|
||||
if (thrown instanceof PendingThrow) {
|
||||
if (thrown.value === undefined) {
|
||||
thrown.value = createErrorValue(prototypes[thrown.type], thrown.message)
|
||||
thrown.value = createErrorValue(builtins[thrown.type], thrown.message)
|
||||
thrown.value.host = thrown
|
||||
}
|
||||
return thrown.value
|
||||
}
|
||||
const type = thrown instanceof Error && isErrorType(thrown.name) ? thrown.name : "Error"
|
||||
return createErrorValue(prototypes[type], normalizeError(thrown).message)
|
||||
return createErrorValue(builtins[type], normalizeError(thrown).message)
|
||||
}
|
||||
|
||||
/** Error.prototype.toString: `name: message`, omitting whichever side is empty. */
|
||||
const errorToString = (self: ProgramObject): string => {
|
||||
const errorToString = (self: Obj): string => {
|
||||
const name = get(self, "name")
|
||||
const message = get(self, "message")
|
||||
const shownName = name === undefined ? "Error" : coerceToString(name)
|
||||
@@ -124,53 +115,53 @@ const errorToString = (self: ProgramObject): string => {
|
||||
}
|
||||
|
||||
export const createAggregateErrorValue = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
errors: Array<unknown>,
|
||||
message: string,
|
||||
proto: ProgramObject = runner.prototypes.AggregateError,
|
||||
proto: Obj = ctx.builtins.AggregateError,
|
||||
) => {
|
||||
const value = createErrorValue(proto, message)
|
||||
define(value, "errors", new ProgramArray(runner.prototypes.Array, errors), { ...hidden })
|
||||
define(value, "errors", new Arr(ctx.builtins.Array, errors), { ...hidden })
|
||||
return value
|
||||
}
|
||||
|
||||
const constructAggregateErrorValue = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
args: Array<unknown>,
|
||||
proto: ProgramObject,
|
||||
): Effect.Effect<ProgramError, unknown, R> =>
|
||||
proto: Obj,
|
||||
): Effect.Effect<ErrorObj, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(args[0])
|
||||
const cursor = yield* ctx.iterate(args[0])
|
||||
if (cursor === undefined) throw typeError("new AggregateError(...) expects a synchronous iterable of errors.")
|
||||
const errors: Array<unknown> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) {
|
||||
return createAggregateErrorValue(runner, errors, args[1] === undefined ? "" : coerceToString(args[1]), proto)
|
||||
return createAggregateErrorValue(ctx, errors, args[1] === undefined ? "" : coerceToString(args[1]), proto)
|
||||
}
|
||||
errors.push(step.value)
|
||||
}
|
||||
})
|
||||
|
||||
/** An error constructor such as `Error` or `TypeError`; callable with or without `new`, like JS. */
|
||||
export const errorGlobal = <R>(type: ErrorType, runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const prototype = protos[type]
|
||||
export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const prototype = builtins[type]
|
||||
const construct = (args: Array<unknown>, newTarget: Callable) => {
|
||||
const proto = prototypeFrom(newTarget, prototype)
|
||||
return type === "AggregateError"
|
||||
? constructAggregateErrorValue(runner, args, proto)
|
||||
? constructAggregateErrorValue(ctx, args, proto)
|
||||
: Effect.sync(() => createErrorValue(proto, args[0] === undefined ? undefined : coerceToString(args[0])))
|
||||
}
|
||||
const ctor: NativeFunction<R> = constructor<R>(protos, prototype, {
|
||||
const ctor: Native<R> = constructor<R>(builtins, prototype, {
|
||||
name: type,
|
||||
length: type === "AggregateError" ? 2 : 1,
|
||||
call: (_, args) => construct(args, ctor),
|
||||
construct,
|
||||
})
|
||||
if (type === "Error") {
|
||||
methods(protos, prototype, [
|
||||
["toString", 0, (thisValue) => errorToString(receiver(ProgramObject, thisValue, "Error.prototype.toString"))],
|
||||
methods(builtins, prototype, [
|
||||
["toString", 0, (thisValue) => errorToString(receiver(Obj, thisValue, "Error.prototype.toString"))],
|
||||
])
|
||||
}
|
||||
return ctor
|
||||
|
||||
@@ -7,18 +7,17 @@ import type { DataValue, Diagnostic, ResolvedExecutionLimits, Result } from "../
|
||||
import { toData } from "../data.js"
|
||||
import { ToolRuntime } from "../tool-runtime.js"
|
||||
import { normalizeError } from "./errors.js"
|
||||
import { createPrototypes } from "./intrinsics.js"
|
||||
import type { Host } from "./globals.js"
|
||||
import { createBuiltins } from "./intrinsics.js"
|
||||
import { PendingThrow } from "./model.js"
|
||||
import { PromiseRuntime } from "./promises.js"
|
||||
import { Runtime } from "./runtime.js"
|
||||
import { Pending } from "./promises.js"
|
||||
import { Interpreter } from "./interpreter.js"
|
||||
|
||||
export const executeProgram = <R>(
|
||||
code: string,
|
||||
prepared: ToolRuntime.Prepared<R>,
|
||||
limits: ResolvedExecutionLimits,
|
||||
hooks: ToolRuntime.ToolCallHooks<R>,
|
||||
extraGlobals?: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]>,
|
||||
globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>,
|
||||
): Effect.Effect<Result, never, R> => {
|
||||
if (code.trim().length === 0) {
|
||||
return Effect.succeed({
|
||||
@@ -30,31 +29,23 @@ export const executeProgram = <R>(
|
||||
|
||||
// Allocate execution state inside suspension so reused Effects never share it.
|
||||
return Effect.suspend(() => {
|
||||
const prototypes = createPrototypes()
|
||||
const tools = ToolRuntime.make(prepared, prototypes, limits.maxToolCalls, hooks)
|
||||
const builtins = createBuiltins()
|
||||
const tools = ToolRuntime.make(prepared, builtins, limits.maxToolCalls, hooks)
|
||||
const logs: Array<string> = []
|
||||
const logged = () => (logs.length > 0 ? { logs: [...logs] } : {})
|
||||
// Set only after copy-out so timeouts cannot report invalid values as completed.
|
||||
let returned: { value: DataValue; promises: PromiseRuntime<R> } | undefined
|
||||
let returned: { value: DataValue; pending: Pending<R> } | undefined
|
||||
|
||||
const base = Effect.acquireUseRelease(
|
||||
Scope.make("parallel"),
|
||||
(scope) =>
|
||||
Effect.gen(function* () {
|
||||
const program = parseProgram(code)
|
||||
const promises = new PromiseRuntime<R>(scope, prototypes.Promise)
|
||||
const value = yield* new Runtime<R>(
|
||||
tools.execute,
|
||||
tools.search,
|
||||
tools.keys,
|
||||
promises,
|
||||
prototypes,
|
||||
logs,
|
||||
extraGlobals,
|
||||
).run(program)
|
||||
const pending = new Pending<R>(scope, builtins.Promise)
|
||||
const value = yield* new Interpreter<R>({ tools, pending, builtins, logs, globals }).run(program)
|
||||
const result = toData(value, "Execution result", "result") as DataValue
|
||||
returned = { value: result, promises }
|
||||
const warnings = yield* promises.interrupt()
|
||||
returned = { value: result, pending }
|
||||
const warnings = yield* pending.interrupt()
|
||||
return {
|
||||
ok: true,
|
||||
value: result,
|
||||
@@ -91,7 +82,7 @@ export const executeProgram = <R>(
|
||||
kind: "TimeoutExceeded",
|
||||
message: `The program returned, but background work was still running at the ${timeoutMs}ms timeout and was interrupted. Await all started promises.`,
|
||||
},
|
||||
...returned.promises.diagnostics(),
|
||||
...returned.pending.diagnostics(),
|
||||
],
|
||||
...logged(),
|
||||
toolCalls: tools.calls,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Effect } from "effect"
|
||||
import { MAX_VALUE_DEPTH } from "../data.js"
|
||||
import type { Extension } from "../extension.js"
|
||||
import { coerceToString } from "../stdlib/value.js"
|
||||
import type { Host } from "./globals.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { createErrorValue, isErrorType } from "./intrinsics.js"
|
||||
import { ProgramThrow, typeError } from "./model.js"
|
||||
import { Throw, typeError } from "./model.js"
|
||||
import { constructor, fn } from "./native.js"
|
||||
import {
|
||||
Callable,
|
||||
@@ -13,19 +13,20 @@ import {
|
||||
entries,
|
||||
get,
|
||||
hidden,
|
||||
type NativeFunction,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramError,
|
||||
ProgramGenerator,
|
||||
ProgramHandle,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramRegExp,
|
||||
ProgramSet,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
type Native,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
GeneratorObj,
|
||||
Handle,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "./objects.js"
|
||||
import { describeValue } from "./references.js"
|
||||
|
||||
@@ -44,16 +45,16 @@ const ownPrototypeKeys = new Set(["constructor"])
|
||||
* a program promise. Prototypes, constructors, and handle identity are all per run.
|
||||
*/
|
||||
export const extensionGlobals = <R>(
|
||||
host: Host<R>,
|
||||
ctx: Interpreter<R>,
|
||||
extensions: ReadonlyArray<Extension>,
|
||||
): ReadonlyArray<readonly [string, unknown]> => {
|
||||
const protos = host.runner.prototypes
|
||||
const builtins = ctx.builtins
|
||||
const classes = new Set(extensions.flatMap((extension) => Object.values(extension.globals)).filter(isClass))
|
||||
// Host prototype object → this run's program prototype, so an instance wraps as its most-derived exposed class.
|
||||
const prototypes = new Map<object, ProgramObject>()
|
||||
const exposed = new Map<Class, { ctor: NativeFunction<R>; proto: ProgramObject }>()
|
||||
const protoOf = new Map<object, Obj>()
|
||||
const exposed = new Map<Class, { ctor: Native<R>; proto: Obj }>()
|
||||
const classOf = new Map<unknown, Class>()
|
||||
const handles = new WeakMap<object, ProgramHandle>()
|
||||
const handles = new WeakMap<object, Handle>()
|
||||
|
||||
const toHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): unknown => {
|
||||
if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
|
||||
@@ -61,23 +62,24 @@ export const extensionGlobals = <R>(
|
||||
if (isPrimitive(value)) return value
|
||||
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
|
||||
}
|
||||
if (value instanceof ProgramHandle) return value.instance
|
||||
if (value instanceof ProgramDate) return new Date(value.time)
|
||||
if (value instanceof ProgramRegExp) return new RegExp(value.regex.source, value.regex.flags)
|
||||
if (value instanceof ProgramURL) return new URL(value.url.href)
|
||||
if (value instanceof ProgramURLSearchParams) return new URLSearchParams(value.params)
|
||||
if (value instanceof Handle) return value.instance
|
||||
if (value instanceof Bytes) return new Uint8Array(value.bytes)
|
||||
if (value instanceof DateObj) return new Date(value.time)
|
||||
if (value instanceof RegExpObj) return new RegExp(value.regex.source, value.regex.flags)
|
||||
if (value instanceof URLObj) return new URL(value.url.href)
|
||||
if (value instanceof URLSearchParamsObj) return new URLSearchParams(value.params)
|
||||
const next = (item: unknown) => toHost(item, label, depth + 1, seen)
|
||||
if (value instanceof ProgramMap) return new Map([...value.map].map(([key, item]) => [next(key), next(item)]))
|
||||
if (value instanceof ProgramSet) return new Set([...value.set].map(next))
|
||||
if (value instanceof MapObj) return new Map([...value.map].map(([key, item]) => [next(key), next(item)]))
|
||||
if (value instanceof SetObj) return new Set([...value.set].map(next))
|
||||
if (
|
||||
!(value instanceof ProgramObject) ||
|
||||
!(value instanceof Obj) ||
|
||||
value instanceof Callable ||
|
||||
value instanceof ProgramGenerator ||
|
||||
value instanceof ProgramPromise
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof PromiseObj
|
||||
) {
|
||||
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
|
||||
}
|
||||
if (value instanceof ProgramError) {
|
||||
if (value instanceof ErrorObj) {
|
||||
const name = coerceToString(get(value, "name"))
|
||||
const message = get(value, "message")
|
||||
const text = message === undefined ? "" : coerceToString(message)
|
||||
@@ -86,7 +88,7 @@ export const extensionGlobals = <R>(
|
||||
if (seen.has(value)) throw typeError(`${label} contains a circular value.`)
|
||||
seen.add(value)
|
||||
const copied =
|
||||
value instanceof ProgramArray
|
||||
value instanceof Arr
|
||||
? value.items.map(next)
|
||||
: Object.fromEntries(
|
||||
entries(value)
|
||||
@@ -105,40 +107,42 @@ export const extensionGlobals = <R>(
|
||||
if (existing !== undefined) return existing
|
||||
const proto = handlePrototype(value)
|
||||
if (proto !== undefined) {
|
||||
const handle = new ProgramHandle(proto, value)
|
||||
const handle = new Handle(proto, value)
|
||||
handles.set(value, handle)
|
||||
return handle
|
||||
}
|
||||
if (value instanceof Date) return new ProgramDate(protos.Date, value.getTime())
|
||||
if (value instanceof RegExp) return new ProgramRegExp(protos.RegExp, value.source, value.flags)
|
||||
if (value instanceof Date) return new DateObj(builtins.Date, value.getTime())
|
||||
if (value instanceof RegExp) return new RegExpObj(builtins.RegExp, value.source, value.flags)
|
||||
if (value instanceof Uint8Array) return new Bytes(builtins.Uint8Array, new Uint8Array(value))
|
||||
if (value instanceof ArrayBuffer) return new Bytes(builtins.Uint8Array, new Uint8Array(value.slice(0)))
|
||||
if (value instanceof Error) {
|
||||
return createErrorValue(protos[isErrorType(value.name) ? value.name : "Error"], value.message)
|
||||
return createErrorValue(builtins[isErrorType(value.name) ? value.name : "Error"], value.message)
|
||||
}
|
||||
if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href))
|
||||
if (value instanceof URL) return new URLObj(builtins.URL, builtins.URLSearchParams, new URL(value.href))
|
||||
if (value instanceof URLSearchParams) {
|
||||
return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value))
|
||||
return new URLSearchParamsObj(builtins.URLSearchParams, new URLSearchParams(value))
|
||||
}
|
||||
const next = (item: unknown) => fromHost(item, label, depth + 1, seen)
|
||||
if (value instanceof Map) {
|
||||
const wrapped = new ProgramMap(protos.Map)
|
||||
const wrapped = new MapObj(builtins.Map)
|
||||
for (const [key, item] of value) wrapped.map.set(next(key), next(item))
|
||||
return wrapped
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
const wrapped = new ProgramSet(protos.Set)
|
||||
const wrapped = new SetObj(builtins.Set)
|
||||
for (const item of value) wrapped.set.add(next(item))
|
||||
return wrapped
|
||||
}
|
||||
if (seen.has(value)) throw typeError(`${label} produced a circular value.`)
|
||||
seen.add(value)
|
||||
if (Array.isArray(value)) {
|
||||
const copied = new ProgramArray(protos.Array, value.map(next))
|
||||
const copied = new Arr(builtins.Array, value.map(next))
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
if (prototype === Object.prototype || prototype === null) {
|
||||
const copied = new ProgramObject(protos.Object)
|
||||
const copied = new Obj(builtins.Object)
|
||||
for (const [key, item] of Object.entries(value)) define(copied, key, next(item))
|
||||
seen.delete(value)
|
||||
return copied
|
||||
@@ -147,9 +151,9 @@ export const extensionGlobals = <R>(
|
||||
throw typeError(`${label} produced ${describeHost(value)}, which the program cannot hold.`)
|
||||
}
|
||||
|
||||
const handlePrototype = (instance: object): ProgramObject | undefined => {
|
||||
const handlePrototype = (instance: object): Obj | undefined => {
|
||||
for (let level = Object.getPrototypeOf(instance); level !== null; level = Object.getPrototypeOf(level)) {
|
||||
const proto = prototypes.get(level)
|
||||
const proto = protoOf.get(level)
|
||||
if (proto !== undefined) return proto
|
||||
}
|
||||
return undefined
|
||||
@@ -158,7 +162,7 @@ export const extensionGlobals = <R>(
|
||||
// Runs host code with already-converted inputs. Whatever it returns, resolves, throws, or rejects with crosses
|
||||
// the same way, so the program catches what the author threw.
|
||||
const invoke = (run: () => unknown, label: string): Effect.Effect<unknown, unknown, R> => {
|
||||
const thrown = (reason: unknown) => new ProgramThrow(fromHost(reason, label))
|
||||
const thrown = (reason: unknown) => new Throw(fromHost(reason, label))
|
||||
let result: unknown
|
||||
try {
|
||||
result = run()
|
||||
@@ -166,7 +170,7 @@ export const extensionGlobals = <R>(
|
||||
return Effect.fail(thrown(reason))
|
||||
}
|
||||
if (!(result instanceof Promise)) return Effect.succeed(fromHost(result, label))
|
||||
return host.promises.create(
|
||||
return ctx.pending.create(
|
||||
Effect.map(Effect.tryPromise({ try: () => result, catch: thrown }), (settled) => fromHost(settled, label)),
|
||||
)
|
||||
}
|
||||
@@ -176,7 +180,7 @@ export const extensionGlobals = <R>(
|
||||
|
||||
// Own members of each level from `from` up to (excluding) `root`, child first, as JS resolves them.
|
||||
const members = (
|
||||
target: ProgramObject,
|
||||
target: Obj,
|
||||
from: object,
|
||||
root: object,
|
||||
skip: ReadonlySet<string>,
|
||||
@@ -194,7 +198,7 @@ export const extensionGlobals = <R>(
|
||||
const converted = args(values, name)
|
||||
return invoke(() => method.apply(self, converted), name)
|
||||
}
|
||||
define(target, key, fn<R>(protos, key, method.length, impl), hidden)
|
||||
define(target, key, fn<R>(builtins, key, method.length, impl), hidden)
|
||||
continue
|
||||
}
|
||||
// Data properties stay host-side: a program write to one would change the host class itself.
|
||||
@@ -222,15 +226,15 @@ export const extensionGlobals = <R>(
|
||||
}
|
||||
}
|
||||
|
||||
const expose = (cls: Class): { ctor: NativeFunction<R>; proto: ProgramObject } => {
|
||||
const expose = (cls: Class): { ctor: Native<R>; proto: Obj } => {
|
||||
const existing = exposed.get(cls)
|
||||
if (existing !== undefined) return existing
|
||||
const ancestor = exposedAncestor(cls)
|
||||
const base = ancestor === undefined ? undefined : expose(ancestor)
|
||||
const proto = new ProgramObject(base === undefined ? protos.Object : base.proto)
|
||||
prototypes.set(cls.prototype, proto)
|
||||
const proto = new Obj(base === undefined ? builtins.Object : base.proto)
|
||||
protoOf.set(cls.prototype, proto)
|
||||
const name = cls.name
|
||||
const ctor = constructor<R>(protos, proto, {
|
||||
const ctor = constructor<R>(builtins, proto, {
|
||||
name,
|
||||
length: cls.length,
|
||||
call: (_, values) => {
|
||||
@@ -260,7 +264,7 @@ export const extensionGlobals = <R>(
|
||||
ownPrototypeKeys,
|
||||
`${name}.prototype`,
|
||||
(thisValue, member) => {
|
||||
if (thisValue instanceof ProgramHandle && thisValue.instance instanceof cls) return thisValue.instance
|
||||
if (thisValue instanceof Handle && thisValue.instance instanceof cls) return thisValue.instance
|
||||
throw typeError(`Illegal invocation: ${member} called on ${describeValue(thisValue)}.`)
|
||||
},
|
||||
)
|
||||
@@ -281,7 +285,7 @@ export const extensionGlobals = <R>(
|
||||
const converted = args(values, name)
|
||||
return invoke(() => value.apply(undefined, converted), name)
|
||||
}
|
||||
return [name, fn<R>(protos, name, value.length, impl)] as const
|
||||
return [name, fn<R>(builtins, name, value.length, impl)] as const
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { Effect } from "effect"
|
||||
import { fn, type Method, methods, receiver } from "./native.js"
|
||||
import { AsyncIteratorSymbol, type GeneratorRequestKind, IteratorSymbol } from "./model.js"
|
||||
import { define, hidden, ProgramGenerator } from "./objects.js"
|
||||
import type { PromiseRuntime } from "./promises.js"
|
||||
import type { Runner } from "./runner.js"
|
||||
import { define, hidden, GeneratorObj } from "./objects.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
|
||||
/** `next`/`return`/`throw` on the generator prototypes; async generators answer with promises. */
|
||||
export const generatorGlobals = <R>(runner: Runner<R>, promises: PromiseRuntime<R>): void => {
|
||||
const protos = runner.prototypes
|
||||
export const generatorGlobals = <R>(ctx: Interpreter<R>): void => {
|
||||
const builtins = ctx.builtins
|
||||
const install = (asynchronous: boolean) => {
|
||||
const proto = asynchronous ? protos.AsyncGenerator : protos.Generator
|
||||
const proto = asynchronous ? builtins.AsyncGenerator : builtins.Generator
|
||||
const label = asynchronous ? "AsyncGenerator" : "Generator"
|
||||
const request = (kind: GeneratorRequestKind): Method => [
|
||||
kind,
|
||||
1,
|
||||
(thisValue: unknown, args: Array<unknown>) => {
|
||||
const generator = receiver(ProgramGenerator, thisValue, `${label}.prototype.${kind}`)
|
||||
const generator = receiver(GeneratorObj, thisValue, `${label}.prototype.${kind}`)
|
||||
const requested = generator.request(kind, args[0]) as Effect.Effect<unknown, unknown, R>
|
||||
return generator.asynchronous ? promises.create(requested) : requested
|
||||
return generator.asynchronous ? ctx.pending.create(requested) : requested
|
||||
},
|
||||
]
|
||||
methods(protos, proto, [request("next"), request("return"), request("throw")])
|
||||
methods(builtins, proto, [request("next"), request("return"), request("throw")])
|
||||
define(
|
||||
asynchronous ? protos.AsyncIterator : protos.Iterator,
|
||||
asynchronous ? builtins.AsyncIterator : builtins.Iterator,
|
||||
asynchronous ? AsyncIteratorSymbol : IteratorSymbol,
|
||||
fn(protos, asynchronous ? "[Symbol.asyncIterator]" : "[Symbol.iterator]", 0, (thisValue) => thisValue),
|
||||
fn(builtins, asynchronous ? "[Symbol.asyncIterator]" : "[Symbol.iterator]", 0, (thisValue) => thisValue),
|
||||
hidden,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { arrayGlobal } from "../stdlib/array.js"
|
||||
import { textDecoderGlobal, textEncoderGlobal, uint8ArrayGlobal } from "../stdlib/bytes.js"
|
||||
import { mapGlobal, setGlobal } from "../stdlib/collections.js"
|
||||
import { consoleGlobal } from "../stdlib/console.js"
|
||||
import { dateGlobal } from "../stdlib/date.js"
|
||||
@@ -18,25 +19,16 @@ import { errorTypes } from "./intrinsics.js"
|
||||
import { constants, constructor, native } from "./native.js"
|
||||
import { AsyncIteratorSymbol, IteratorSymbol, typeError } from "./model.js"
|
||||
import { generatorGlobals } from "./generators.js"
|
||||
import { promiseGlobal, type PromiseRuntime } from "./promises.js"
|
||||
import type { Runner } from "./runner.js"
|
||||
|
||||
/** What the built-in globals need from the interpreter that owns them. */
|
||||
export type Host<R> = {
|
||||
readonly runner: Runner<R>
|
||||
readonly promises: PromiseRuntime<R>
|
||||
readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||
readonly logs: Array<string>
|
||||
}
|
||||
import { promiseGlobal } from "./promises.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
|
||||
// Function.prototype.constructor exists so `fn.constructor === Function` holds; dynamic code is unsupported.
|
||||
const functionGlobal = <R>(runner: Runner<R>) => {
|
||||
const functionGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const reject = () =>
|
||||
Effect.sync(() => {
|
||||
throw typeError("The Function constructor is not supported; write the function inline.")
|
||||
})
|
||||
return constructor<R>(runner.prototypes, runner.prototypes.Function, {
|
||||
return constructor<R>(ctx.builtins, ctx.builtins.Function, {
|
||||
name: "Function",
|
||||
length: 1,
|
||||
call: reject,
|
||||
@@ -44,8 +36,8 @@ const functionGlobal = <R>(runner: Runner<R>) => {
|
||||
})
|
||||
}
|
||||
|
||||
const symbolGlobal = <R>(runner: Runner<R>) => {
|
||||
const symbol = native<R>(runner.prototypes, {
|
||||
const symbolGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const symbol = native<R>(ctx.builtins, {
|
||||
name: "Symbol",
|
||||
call: () =>
|
||||
Effect.sync(() => {
|
||||
@@ -57,52 +49,54 @@ const symbolGlobal = <R>(runner: Runner<R>) => {
|
||||
return symbol
|
||||
}
|
||||
|
||||
type Factory = <R>(host: Host<R>) => unknown
|
||||
type Factory = <R>(ctx: Interpreter<R>) => unknown
|
||||
|
||||
// A table rather than a list so the names are known before any runtime exists.
|
||||
const table: Record<string, Factory> = {
|
||||
tools: () => new ToolReference([]),
|
||||
search: (host) =>
|
||||
native(host.runner.prototypes, { name: "search", call: (_, args) => host.search(args), callback: false }),
|
||||
search: (ctx) => native(ctx.builtins, { name: "search", call: (_, args) => ctx.tools.search(args), callback: false }),
|
||||
undefined: () => undefined,
|
||||
NaN: () => NaN,
|
||||
Infinity: () => Infinity,
|
||||
Object: (host) => objectGlobal(host.runner, host.toolKeys),
|
||||
Function: (host) => functionGlobal(host.runner),
|
||||
Array: (host) => arrayGlobal(host.runner),
|
||||
Math: (host) => mathGlobal(host.runner),
|
||||
JSON: (host) => jsonGlobal(host.runner),
|
||||
console: (host) => consoleGlobal(host.runner, host.logs),
|
||||
Promise: (host) => promiseGlobal(host.runner, host.promises),
|
||||
Symbol: (host) => symbolGlobal(host.runner),
|
||||
Number: (host) => numberGlobal(host.runner),
|
||||
String: (host) => stringGlobal(host.runner),
|
||||
Boolean: (host) => booleanGlobal(host.runner),
|
||||
parseInt: (host) => coercion(host.runner, "parseInt", 2),
|
||||
parseFloat: (host) => coercion(host.runner, "parseFloat"),
|
||||
isFinite: (host) => coercion(host.runner, "isFinite"),
|
||||
isNaN: (host) => coercion(host.runner, "isNaN"),
|
||||
Date: (host) => dateGlobal(host.runner),
|
||||
RegExp: (host) => regexpGlobal(host.runner),
|
||||
Map: (host) => mapGlobal(host.runner),
|
||||
Set: (host) => setGlobal(host.runner),
|
||||
URL: (host) => urlGlobal(host.runner),
|
||||
URLSearchParams: (host) => urlSearchParamsGlobal(host.runner),
|
||||
encodeURI: (host) => uriGlobal(host.runner, "encodeURI"),
|
||||
encodeURIComponent: (host) => uriGlobal(host.runner, "encodeURIComponent"),
|
||||
decodeURI: (host) => uriGlobal(host.runner, "decodeURI"),
|
||||
decodeURIComponent: (host) => uriGlobal(host.runner, "decodeURIComponent"),
|
||||
atob: (host) => base64Global(host.runner, "atob"),
|
||||
btoa: (host) => base64Global(host.runner, "btoa"),
|
||||
crypto: (host) => cryptoGlobal(host.runner),
|
||||
...Object.fromEntries(errorTypes.map((type) => [type, <R>(host: Host<R>) => errorGlobal(type, host.runner)])),
|
||||
Object: (ctx) => objectGlobal(ctx),
|
||||
Function: (ctx) => functionGlobal(ctx),
|
||||
Array: (ctx) => arrayGlobal(ctx),
|
||||
Math: (ctx) => mathGlobal(ctx),
|
||||
JSON: (ctx) => jsonGlobal(ctx),
|
||||
console: (ctx) => consoleGlobal(ctx),
|
||||
Promise: (ctx) => promiseGlobal(ctx),
|
||||
Symbol: (ctx) => symbolGlobal(ctx),
|
||||
Number: (ctx) => numberGlobal(ctx),
|
||||
String: (ctx) => stringGlobal(ctx),
|
||||
Boolean: (ctx) => booleanGlobal(ctx),
|
||||
parseInt: (ctx) => coercion(ctx, "parseInt", 2),
|
||||
parseFloat: (ctx) => coercion(ctx, "parseFloat"),
|
||||
isFinite: (ctx) => coercion(ctx, "isFinite"),
|
||||
isNaN: (ctx) => coercion(ctx, "isNaN"),
|
||||
Date: (ctx) => dateGlobal(ctx),
|
||||
RegExp: (ctx) => regexpGlobal(ctx),
|
||||
Map: (ctx) => mapGlobal(ctx),
|
||||
Set: (ctx) => setGlobal(ctx),
|
||||
URL: (ctx) => urlGlobal(ctx),
|
||||
URLSearchParams: (ctx) => urlSearchParamsGlobal(ctx),
|
||||
Uint8Array: (ctx) => uint8ArrayGlobal(ctx),
|
||||
TextEncoder: (ctx) => textEncoderGlobal(ctx),
|
||||
TextDecoder: (ctx) => textDecoderGlobal(ctx),
|
||||
encodeURI: (ctx) => uriGlobal(ctx, "encodeURI"),
|
||||
encodeURIComponent: (ctx) => uriGlobal(ctx, "encodeURIComponent"),
|
||||
decodeURI: (ctx) => uriGlobal(ctx, "decodeURI"),
|
||||
decodeURIComponent: (ctx) => uriGlobal(ctx, "decodeURIComponent"),
|
||||
atob: (ctx) => base64Global(ctx, "atob"),
|
||||
btoa: (ctx) => base64Global(ctx, "btoa"),
|
||||
crypto: (ctx) => cryptoGlobal(ctx),
|
||||
...Object.fromEntries(errorTypes.map((type) => [type, <R>(ctx: Interpreter<R>) => errorGlobal(type, ctx)])),
|
||||
}
|
||||
|
||||
/** Names bound in every program before extensions apply. */
|
||||
export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
|
||||
|
||||
/** The immutable global bindings of every program, in declaration order. */
|
||||
export const globals = <R>(host: Host<R>): ReadonlyArray<readonly [string, unknown]> => {
|
||||
generatorGlobals(host.runner, host.promises)
|
||||
return Object.entries(table).map(([name, factory]) => [name, factory(host)] as const)
|
||||
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, unknown]> => {
|
||||
generatorGlobals(ctx)
|
||||
return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const)
|
||||
}
|
||||
|
||||
+156
-144
@@ -43,7 +43,7 @@ import type {
|
||||
} from "acorn"
|
||||
import { Cause, Deferred, Effect, Exit } from "effect"
|
||||
import { toProgram } from "../data.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { ToolReference, type ToolRuntime } from "../tool-runtime.js"
|
||||
import {
|
||||
type AstNode,
|
||||
AsyncIteratorSymbol,
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
IteratorSymbol,
|
||||
OptionalShortCircuit,
|
||||
invalidData,
|
||||
ProgramThrow,
|
||||
Throw,
|
||||
rangeError,
|
||||
type StatementResult,
|
||||
typeError,
|
||||
@@ -62,8 +62,8 @@ import {
|
||||
} from "./model.js"
|
||||
import { checkStringLength } from "./limits.js"
|
||||
import { locate, materialize } from "./errors.js"
|
||||
import type { Prototypes } from "./intrinsics.js"
|
||||
import { globals, type Host } from "./globals.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { globals } from "./globals.js"
|
||||
import {
|
||||
assign,
|
||||
Callable,
|
||||
@@ -72,23 +72,24 @@ import {
|
||||
has,
|
||||
hasPrototype,
|
||||
keys,
|
||||
NativeFunction,
|
||||
Native,
|
||||
parseArrayIndex,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramFunction,
|
||||
ProgramGenerator,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramSet,
|
||||
ProgramURLSearchParams,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
Fn,
|
||||
GeneratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
SetObj,
|
||||
URLSearchParamsObj,
|
||||
record,
|
||||
remove,
|
||||
set,
|
||||
} from "./objects.js"
|
||||
import { preserveConsumerError, type Runner } from "./runner.js"
|
||||
import { PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js"
|
||||
import { preserveConsumerError } from "./callback.js"
|
||||
import { Pending, resolvePromise, resolvePromiseValue } from "./promises.js"
|
||||
import { containsOpaqueReference, describeValue, rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import { ScopeStack } from "./scope.js"
|
||||
import { constructRegExp } from "../stdlib/regexp.js"
|
||||
@@ -129,7 +130,7 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean =>
|
||||
throw typeError("The right-hand side of 'instanceof' is not callable.", node)
|
||||
}
|
||||
const prototype = get(rhs, "prototype")
|
||||
if (!(prototype instanceof ProgramObject)) {
|
||||
if (!(prototype instanceof Obj)) {
|
||||
throw typeError("The right-hand side of 'instanceof' has no 'prototype' object.", node)
|
||||
}
|
||||
return hasPrototype(lhs, prototype)
|
||||
@@ -220,14 +221,14 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
|
||||
}
|
||||
|
||||
type CustomIterator = {
|
||||
iterator: ProgramObject
|
||||
iterator: Obj
|
||||
next: unknown
|
||||
asynchronous: boolean
|
||||
}
|
||||
|
||||
/** A resolved member: the object to read through and the receiver that inherited accessors see. */
|
||||
type MemberReference = {
|
||||
target: ProgramObject
|
||||
target: Obj
|
||||
key: PropertyKey
|
||||
receiver: unknown
|
||||
}
|
||||
@@ -249,29 +250,28 @@ type GeneratorState = {
|
||||
}
|
||||
|
||||
/** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */
|
||||
export class Runtime<R> {
|
||||
readonly runner: Runner<R>
|
||||
export class Interpreter<R> {
|
||||
readonly tools: ToolRuntime<R>
|
||||
readonly pending: Pending<R>
|
||||
readonly builtins: Builtins
|
||||
readonly logs: Array<string>
|
||||
private readonly root: Frame<R>
|
||||
|
||||
constructor(
|
||||
readonly executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||
readonly promises: PromiseRuntime<R>,
|
||||
readonly prototypes: Prototypes,
|
||||
readonly logs: Array<string> = [],
|
||||
extraGlobals: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]> = () => [],
|
||||
) {
|
||||
constructor(options: {
|
||||
readonly tools: ToolRuntime<R>
|
||||
readonly pending: Pending<R>
|
||||
readonly builtins: Builtins
|
||||
readonly logs?: Array<string>
|
||||
readonly globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>
|
||||
}) {
|
||||
this.tools = options.tools
|
||||
this.pending = options.pending
|
||||
this.builtins = options.builtins
|
||||
this.logs = options.logs ?? []
|
||||
const globalScope = new Map<string, Binding>()
|
||||
// Calling back into the program never reads frame state, so any frame serves; the root is always alive.
|
||||
this.root = new Frame(this, new ScopeStack([globalScope]))
|
||||
this.runner = {
|
||||
invokeCallable: (callable, thisValue, args) => this.root.invokeCallable(callable, thisValue, args),
|
||||
settlePromise: (promise) => this.root.settlePromise(promise),
|
||||
syncIterator: (value) => this.root.syncIterator(value),
|
||||
prototypes,
|
||||
}
|
||||
for (const [name, value] of [...globals(this), ...extraGlobals(this)]) {
|
||||
for (const [name, value] of [...globals(this), ...(options.globals?.(this) ?? [])]) {
|
||||
globalScope.set(name, { mutable: false, value })
|
||||
}
|
||||
}
|
||||
@@ -279,6 +279,18 @@ export class Runtime<R> {
|
||||
run(program: Program): Effect.Effect<unknown, unknown, R> {
|
||||
return this.root.run(program)
|
||||
}
|
||||
|
||||
call(callable: unknown, thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||
return this.root.call(callable, thisValue, args)
|
||||
}
|
||||
|
||||
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
|
||||
return this.root.await(promise)
|
||||
}
|
||||
|
||||
iterate(value: unknown) {
|
||||
return this.root.iterate(value)
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CALL_DEPTH = 10_000
|
||||
@@ -289,7 +301,7 @@ class Frame<R> {
|
||||
private generatorAsync = false
|
||||
|
||||
constructor(
|
||||
private readonly runtime: Runtime<R>,
|
||||
private readonly ctx: Interpreter<R>,
|
||||
private scopes: ScopeStack,
|
||||
/** Nested call depth; resets when an await resumes, since the continuation runs from the job queue. */
|
||||
private depth = 0,
|
||||
@@ -322,7 +334,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// The implicit async body adopts returned promises before copy-out.
|
||||
value = yield* resolvePromiseValue(self.runtime.runner, value)
|
||||
value = yield* resolvePromiseValue(self.ctx, value)
|
||||
return value
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
||||
}
|
||||
@@ -331,16 +343,16 @@ class Frame<R> {
|
||||
private createToolCallPromise(
|
||||
path: ReadonlyArray<string>,
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<ProgramPromise, never, R> {
|
||||
return this.runtime.promises.create(Effect.suspend(() => this.runtime.executeTool(path, args)))
|
||||
): Effect.Effect<PromiseObj, never, R> {
|
||||
return this.ctx.pending.create(Effect.suspend(() => this.ctx.tools.execute(path, args)))
|
||||
}
|
||||
|
||||
// Fiber exits make settlement idempotent; yielding prevents inline continuation.
|
||||
settlePromise(promise: ProgramPromise): Effect.Effect<unknown, unknown, never> {
|
||||
const promises = this.runtime.promises
|
||||
await(promise: PromiseObj): Effect.Effect<unknown, unknown, never> {
|
||||
const pending = this.ctx.pending
|
||||
return Effect.suspend(() => {
|
||||
promises.markObserved(promise)
|
||||
return Effect.flatMap(promises.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit))
|
||||
pending.markObserved(promise)
|
||||
return Effect.flatMap(pending.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -414,9 +426,9 @@ class Frame<R> {
|
||||
private createFunction(
|
||||
node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression,
|
||||
name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? ""),
|
||||
): ProgramFunction {
|
||||
return new ProgramFunction(
|
||||
this.runtime.prototypes.Function,
|
||||
): Fn {
|
||||
return new Fn(
|
||||
this.ctx.builtins.Function,
|
||||
name,
|
||||
node.params,
|
||||
node.body,
|
||||
@@ -626,7 +638,7 @@ class Frame<R> {
|
||||
const right = yield* self.evaluateExpression(node.right)
|
||||
|
||||
const iterator = yield* self.customIterator(right, node, awaiting)
|
||||
const cursor = iterator === undefined ? yield* self.syncIterator(right, node) : undefined
|
||||
const cursor = iterator === undefined ? yield* self.iterate(right, node) : undefined
|
||||
if (iterator === undefined && cursor === undefined) {
|
||||
throw invalidData(
|
||||
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`,
|
||||
@@ -695,9 +707,9 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
private awaitValue(value: unknown): Effect.Effect<unknown, unknown, R> {
|
||||
return Effect.flatMap(resolvePromise(this.runtime.runner, this.runtime.promises, value), (promise) =>
|
||||
return Effect.flatMap(resolvePromise(this.ctx, value), (promise) =>
|
||||
Effect.ensuring(
|
||||
this.settlePromise(promise),
|
||||
this.await(promise),
|
||||
Effect.sync(() => (this.depth = 0)),
|
||||
),
|
||||
)
|
||||
@@ -720,27 +732,29 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
syncIterator(value: unknown, node?: AstNode) {
|
||||
iterate(value: unknown, node?: AstNode) {
|
||||
const iterator =
|
||||
value instanceof ProgramArray
|
||||
value instanceof Arr
|
||||
? value.items[Symbol.iterator]()
|
||||
: typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof ProgramMap
|
||||
: value instanceof MapObj
|
||||
? value.map.entries()
|
||||
: value instanceof ProgramSet
|
||||
: value instanceof SetObj
|
||||
? value.set.values()
|
||||
: value instanceof ProgramURLSearchParams
|
||||
: value instanceof URLSearchParamsObj
|
||||
? value.params.entries()
|
||||
: undefined
|
||||
: value instanceof Bytes
|
||||
? value.bytes.values()
|
||||
: undefined
|
||||
if (iterator !== undefined) {
|
||||
const proto = this.runtime.prototypes.Array
|
||||
const proto = this.ctx.builtins.Array
|
||||
return Effect.succeed({
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return {
|
||||
done: Boolean(step.done),
|
||||
value: Array.isArray(step.value) ? new ProgramArray(proto, step.value) : step.value,
|
||||
value: Array.isArray(step.value) ? new Arr(proto, step.value) : step.value,
|
||||
}
|
||||
}),
|
||||
close: Effect.void,
|
||||
@@ -758,13 +772,13 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
|
||||
if (!(value instanceof ProgramObject)) return Effect.undefined
|
||||
if (!(value instanceof Obj)) return Effect.undefined
|
||||
const asyncMethod = allowAsync ? get(value, AsyncIteratorSymbol) : undefined
|
||||
const method = asyncMethod ?? get(value, IteratorSymbol)
|
||||
if (method === undefined || method === null) return Effect.undefined
|
||||
const self = this
|
||||
return Effect.map(
|
||||
this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), value, [], node),
|
||||
this.call(this.requireIteratorMethod(method, "Iterator method", node), value, [], node),
|
||||
(iterator) => {
|
||||
const object = self.requireIteratorObject(iterator, "Iterator method result", node)
|
||||
return {
|
||||
@@ -781,14 +795,14 @@ class Frame<R> {
|
||||
return Effect.gen(function* () {
|
||||
if (iterator.asynchronous) {
|
||||
const object = self.requireIteratorObject(
|
||||
yield* self.awaitValue(yield* self.invokeCallable(iterator.next, iterator.iterator, [], node)),
|
||||
yield* self.awaitValue(yield* self.call(iterator.next, iterator.iterator, [], node)),
|
||||
"Iterator next() result",
|
||||
node,
|
||||
)
|
||||
return { done: Boolean(get(object, "done")), value: get(object, "value") }
|
||||
}
|
||||
|
||||
const called = yield* Effect.exit(self.invokeCallable(iterator.next, iterator.iterator, [], node))
|
||||
const called = yield* Effect.exit(self.call(iterator.next, iterator.iterator, [], node))
|
||||
if (!Exit.isSuccess(called)) {
|
||||
if (awaiting) yield* Effect.yieldNow
|
||||
return yield* Effect.failCause(called.cause)
|
||||
@@ -824,14 +838,14 @@ class Frame<R> {
|
||||
const method = self.requireIteratorMethod(close, "Iterator return", node)
|
||||
if (iterator.asynchronous) {
|
||||
self.requireIteratorObject(
|
||||
yield* self.awaitValue(yield* self.invokeCallable(method, iterator.iterator, [], node)),
|
||||
yield* self.awaitValue(yield* self.call(method, iterator.iterator, [], node)),
|
||||
"Iterator return() result",
|
||||
node,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const called = yield* Effect.exit(self.invokeCallable(method, iterator.iterator, [], node))
|
||||
const called = yield* Effect.exit(self.call(method, iterator.iterator, [], node))
|
||||
if (!Exit.isSuccess(called)) {
|
||||
if (awaiting) yield* Effect.yieldNow
|
||||
return yield* Effect.failCause(called.cause)
|
||||
@@ -847,8 +861,8 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private requireIteratorObject(value: unknown, context: string, node?: AstNode): ProgramObject {
|
||||
if (value instanceof ProgramObject) return value
|
||||
private requireIteratorObject(value: unknown, context: string, node?: AstNode): Obj {
|
||||
if (value instanceof Obj) return value
|
||||
throw typeError(`${context} must be an object.`, node)
|
||||
}
|
||||
|
||||
@@ -859,9 +873,9 @@ class Frame<R> {
|
||||
|
||||
// for...in over null/undefined iterates nothing, like JS.
|
||||
private enumerableKeys(value: unknown, node: AstNode): Array<string> {
|
||||
if (value instanceof ToolReference) return [...this.runtime.toolKeys(value.path)]
|
||||
if (value instanceof ToolReference) return [...this.ctx.tools.keys(value.path)]
|
||||
if (value === null || value === undefined) return []
|
||||
return keys(enumerableSource(this.runtime.runner, "for...in", value, node))
|
||||
return keys(enumerableSource(this.ctx, "for...in", value, node))
|
||||
}
|
||||
|
||||
private evaluateForInStatement(
|
||||
@@ -951,7 +965,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
private evaluateThrowStatement(node: ThrowStatement): Effect.Effect<StatementResult, unknown, R> {
|
||||
return Effect.flatMap(this.evaluateExpression(node.argument), (value) => Effect.fail(new ProgramThrow(value)))
|
||||
return Effect.flatMap(this.evaluateExpression(node.argument), (value) => Effect.fail(new Throw(value)))
|
||||
}
|
||||
|
||||
private evaluateTryStatement(node: TryStatement): Effect.Effect<StatementResult, unknown, R> {
|
||||
@@ -966,7 +980,7 @@ class Frame<R> {
|
||||
return Effect.failCause(cause)
|
||||
}
|
||||
|
||||
const caught = materialize(self.runtime.runner, Cause.squash(cause))
|
||||
const caught = materialize(self.ctx, Cause.squash(cause))
|
||||
const parameter = handler.param
|
||||
self.scopes.push()
|
||||
return Effect.gen(function* () {
|
||||
@@ -1043,7 +1057,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (!(value instanceof ProgramObject)) {
|
||||
if (!(value instanceof Obj)) {
|
||||
throw typeError(
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
@@ -1053,7 +1067,7 @@ class Frame<R> {
|
||||
const consumed = new Set<PropertyKey>()
|
||||
for (const property of pattern.properties) {
|
||||
if (property.type === "RestElement") {
|
||||
const rest = new ProgramObject(self.runtime.prototypes.Object)
|
||||
const rest = new Obj(self.ctx.builtins.Object)
|
||||
assign(rest, value, consumed)
|
||||
yield* self.declarePattern(property.argument, rest, mutable, property, initialize)
|
||||
continue
|
||||
@@ -1102,7 +1116,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (!(value instanceof ProgramObject)) {
|
||||
if (!(value instanceof Obj)) {
|
||||
throw invalidData(
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
@@ -1112,7 +1126,7 @@ class Frame<R> {
|
||||
const consumed = new Set<PropertyKey>()
|
||||
for (const property of pattern.properties) {
|
||||
if (property.type === "RestElement") {
|
||||
const rest = new ProgramObject(self.runtime.prototypes.Object)
|
||||
const rest = new Obj(self.ctx.builtins.Object)
|
||||
assign(rest, value, consumed)
|
||||
yield* self.assignPattern(property.argument, rest, property)
|
||||
continue
|
||||
@@ -1147,7 +1161,7 @@ class Frame<R> {
|
||||
): Effect.Effect<void, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* self.syncIterator(value, pattern)
|
||||
const cursor = yield* self.iterate(value, pattern)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("Array destructuring requires a supported iterable value.", pattern)
|
||||
}
|
||||
@@ -1157,7 +1171,7 @@ class Frame<R> {
|
||||
if (element === null) continue
|
||||
yield* consume(
|
||||
element.type === "RestElement" ? element.argument : element,
|
||||
element.type === "RestElement" ? new ProgramArray(self.runtime.prototypes.Array) : undefined,
|
||||
element.type === "RestElement" ? new Arr(self.ctx.builtins.Array) : undefined,
|
||||
element,
|
||||
)
|
||||
if (element.type === "RestElement") return
|
||||
@@ -1174,7 +1188,7 @@ class Frame<R> {
|
||||
done = next.done
|
||||
if (!done) rest.push(next.value)
|
||||
}
|
||||
yield* consume(element.argument, new ProgramArray(self.runtime.prototypes.Array, rest), element)
|
||||
yield* consume(element.argument, new Arr(self.ctx.builtins.Array, rest), element)
|
||||
return
|
||||
}
|
||||
const consumed = consume(element, step.done ? undefined : step.value, pattern)
|
||||
@@ -1201,8 +1215,8 @@ class Frame<R> {
|
||||
switch (node.type) {
|
||||
case "Literal": {
|
||||
const regex = node.regex
|
||||
if (regex) return Effect.sync(() => constructRegExp(this.runtime.prototypes, [regex.pattern, regex.flags]))
|
||||
return Effect.sync(() => toProgram(this.runtime.prototypes, node.value, "Literal"))
|
||||
if (regex) return Effect.sync(() => constructRegExp(this.ctx.builtins, [regex.pattern, regex.flags]))
|
||||
return Effect.sync(() => toProgram(this.ctx.builtins, node.value, "Literal"))
|
||||
}
|
||||
case "Identifier":
|
||||
return Effect.sync(() => this.scopes.get(node.name, node))
|
||||
@@ -1263,22 +1277,22 @@ class Frame<R> {
|
||||
return Effect.gen(function* () {
|
||||
const callee = yield* self.evaluateExpression(node.callee)
|
||||
// Globals are built with this interpreter's R; `instanceof` cannot recover the type argument.
|
||||
const construct = callee instanceof NativeFunction ? (callee as NativeFunction<R>).construct : undefined
|
||||
const construct = callee instanceof Native ? (callee as Native<R>).construct : undefined
|
||||
if (construct === undefined) {
|
||||
// `new` itself is supported, so a non-constructible callee is a TypeError like JS rather than
|
||||
// unsupported syntax. Built-ins like Number are real constructors in JS, so do not claim
|
||||
// otherwise; say `new` is unsupported for them and point at the plain call.
|
||||
const name = calleeDescription(node.callee)
|
||||
const message =
|
||||
callee instanceof ProgramFunction
|
||||
callee instanceof Fn
|
||||
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
|
||||
: callee instanceof NativeFunction
|
||||
: callee instanceof Native
|
||||
? `new ${name}(...) is not supported; call ${name}(...) without new instead.`
|
||||
: `${name} is not a constructor.`
|
||||
throw typeError(message, node)
|
||||
}
|
||||
const args = yield* self.evaluateCallArguments(node.arguments)
|
||||
return yield* self.native(() => construct(args, callee as NativeFunction<R>), node)
|
||||
return yield* self.native(() => construct(args, callee as Native<R>), node)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1292,7 +1306,7 @@ class Frame<R> {
|
||||
const rhs = yield* self.evaluateExpression(node.right)
|
||||
if (operator === "instanceof") return instanceofValue(lhs, rhs, node)
|
||||
return toProgram(
|
||||
self.runtime.prototypes,
|
||||
self.ctx.builtins,
|
||||
self.applyBinaryOperator(operator, lhs, rhs, node),
|
||||
"Binary expression result",
|
||||
)
|
||||
@@ -1302,7 +1316,7 @@ class Frame<R> {
|
||||
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
|
||||
if (operator === "===") return lhs === rhs
|
||||
if (operator === "!==") return lhs !== rhs
|
||||
if (operator === "in" && rhs instanceof ProgramObject && !containsOpaqueReference(lhs)) {
|
||||
if (operator === "in" && rhs instanceof Obj && !containsOpaqueReference(lhs)) {
|
||||
return has(rhs, lhs !== null && typeof lhs === "object" ? coerceToString(lhs) : (lhs as PropertyKey))
|
||||
}
|
||||
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
|
||||
@@ -1311,7 +1325,7 @@ class Frame<R> {
|
||||
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
|
||||
// Dates use their default string hint for addition and loose equality, and epoch time elsewhere.
|
||||
const coerceOperand = (operand: unknown): unknown => {
|
||||
if (operand instanceof ProgramDate) {
|
||||
if (operand instanceof DateObj) {
|
||||
return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time
|
||||
}
|
||||
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
|
||||
@@ -1360,7 +1374,7 @@ class Frame<R> {
|
||||
case ">>>":
|
||||
return (l as number) >>> (r as number)
|
||||
case "in":
|
||||
if (!(rhs instanceof ProgramObject)) {
|
||||
if (!(rhs instanceof Obj)) {
|
||||
throw typeError("The 'in' operator requires a data object on the right-hand side.", node)
|
||||
}
|
||||
return has(rhs, coerceOperand(lhs) as PropertyKey)
|
||||
@@ -1396,7 +1410,7 @@ class Frame<R> {
|
||||
throw invalidData("Unary operators require data values.", node)
|
||||
}
|
||||
const operand =
|
||||
value instanceof ProgramDate
|
||||
value instanceof DateObj
|
||||
? value.time
|
||||
: value !== null && typeof value === "object"
|
||||
? coerceToString(value)
|
||||
@@ -1415,7 +1429,7 @@ class Frame<R> {
|
||||
default:
|
||||
throw typeError(`Unsupported unary operator '${operator}'.`, node)
|
||||
}
|
||||
return toProgram(this.runtime.prototypes, result, "Unary expression result")
|
||||
return toProgram(this.ctx.builtins, result, "Unary expression result")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1438,7 +1452,7 @@ class Frame<R> {
|
||||
const current = self.scopes.get(name, left)
|
||||
const rightValue = yield* self.evaluateExpression(node.right)
|
||||
const next = toProgram(
|
||||
self.runtime.prototypes,
|
||||
self.ctx.builtins,
|
||||
self.applyCompoundAssignment(operator, current, rightValue, node),
|
||||
"Assignment result",
|
||||
)
|
||||
@@ -1452,7 +1466,7 @@ class Frame<R> {
|
||||
Effect.map(self.evaluateExpression(node.right), (rightValue) => {
|
||||
if (operator === "=") return { write: true, next: rightValue, result: rightValue }
|
||||
const next = toProgram(
|
||||
self.runtime.prototypes,
|
||||
self.ctx.builtins,
|
||||
self.applyCompoundAssignment(operator, current, rightValue, node),
|
||||
"Assignment result",
|
||||
)
|
||||
@@ -1551,7 +1565,7 @@ class Frame<R> {
|
||||
if ((callable === null || callable === undefined) && node.optional) return OptionalShortCircuit
|
||||
|
||||
const args = yield* self.evaluateCallArguments(node.arguments)
|
||||
return yield* self.invokeCallable(callable, thisValue, args, node, callee)
|
||||
return yield* self.call(callable, thisValue, args, node, callee)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1565,7 +1579,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// The single dispatch for every invocation: call expressions and callbacks share it.
|
||||
invokeCallable(
|
||||
call(
|
||||
callable: unknown,
|
||||
thisValue: unknown,
|
||||
args: Array<unknown>,
|
||||
@@ -1580,9 +1594,9 @@ class Frame<R> {
|
||||
}
|
||||
return yield* self.createToolCallPromise(callable.path, args)
|
||||
}
|
||||
if (callable instanceof ProgramFunction) return yield* self.invokeFunction(callable, args, node)
|
||||
if (callable instanceof NativeFunction) {
|
||||
return yield* self.native(() => (callable as NativeFunction<R>).call(thisValue, args), node)
|
||||
if (callable instanceof Fn) return yield* self.invokeFunction(callable, args, node)
|
||||
if (callable instanceof Native) {
|
||||
return yield* self.native(() => (callable as Native<R>).call(thisValue, args), node)
|
||||
}
|
||||
throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
|
||||
})
|
||||
@@ -1606,7 +1620,7 @@ class Frame<R> {
|
||||
for (const argNode of argNodes) {
|
||||
if (argNode.type === "SpreadElement") {
|
||||
const spread = yield* self.evaluateExpression(argNode.argument)
|
||||
const cursor = yield* self.syncIterator(spread, argNode)
|
||||
const cursor = yield* self.iterate(spread, argNode)
|
||||
if (cursor === undefined) throw typeError("Spread arguments require a synchronous iterable.", argNode)
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -1622,12 +1636,12 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// A callback invoked by a built-in runs below the call that invoked the built-in, so the deeper of the two counts.
|
||||
invokeFunction(fn: ProgramFunction, args: Array<unknown>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
invokeFunction(fn: Fn, args: Array<unknown>, node?: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
const self = this
|
||||
return Effect.flatMap(CallSite, (site) => {
|
||||
const depth = Math.max(self.depth, site.depth) + 1
|
||||
if (depth > MAX_CALL_DEPTH) throw rangeError("Maximum call stack size exceeded", node)
|
||||
const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()]), depth)
|
||||
const invocation = new Frame(this.ctx, new ScopeStack([...fn.capturedScopes, new Map()]), depth)
|
||||
const run = Effect.gen(function* () {
|
||||
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
|
||||
const paramScope = invocation.scopes.current()
|
||||
@@ -1640,7 +1654,7 @@ class Frame<R> {
|
||||
if (parameter.type === "RestElement") {
|
||||
yield* invocation.declarePattern(
|
||||
parameter.argument,
|
||||
new ProgramArray(self.runtime.prototypes.Array, args.slice(index)),
|
||||
new Arr(self.ctx.builtins.Array, args.slice(index)),
|
||||
true,
|
||||
parameter,
|
||||
true,
|
||||
@@ -1661,8 +1675,8 @@ class Frame<R> {
|
||||
})
|
||||
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
|
||||
if (!fn.async) return run
|
||||
return this.runtime.promises.createWithSelf((self) =>
|
||||
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, self)),
|
||||
return this.ctx.pending.createWithSelf((self) =>
|
||||
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.ctx, value, self)),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1671,12 +1685,12 @@ class Frame<R> {
|
||||
invocation: Frame<R>,
|
||||
run: Effect.Effect<unknown, unknown, R>,
|
||||
asynchronous: boolean,
|
||||
): ProgramGenerator {
|
||||
): GeneratorObj {
|
||||
const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
|
||||
invocation.generatorState = state
|
||||
invocation.generatorAsync = asynchronous
|
||||
const protos = this.runtime.prototypes
|
||||
const result = (value: unknown, done: boolean) => record(protos.Object, { value, done })
|
||||
const builtins = this.ctx.builtins
|
||||
const result = (value: unknown, done: boolean) => record(builtins.Object, { value, done })
|
||||
const request = (kind: GeneratorRequestKind, value: unknown) => {
|
||||
const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
|
||||
if (!asynchronous && state.active) return Effect.die(typeError("Generator is already running."))
|
||||
@@ -1687,7 +1701,7 @@ class Frame<R> {
|
||||
if (state.draining) return Deferred.await(request.response)
|
||||
state.draining = true
|
||||
return Effect.andThen(
|
||||
this.runtime.promises.fork(
|
||||
this.ctx.pending.fork(
|
||||
invocation
|
||||
.completeGeneratorRequests(state, true)
|
||||
.pipe(Effect.ensuring(Effect.sync(() => (state.draining = false)))),
|
||||
@@ -1696,12 +1710,12 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
if (state.completed) {
|
||||
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
|
||||
if (kind === "throw") return Effect.fail(new Throw(value))
|
||||
return Effect.succeed(result(kind === "return" ? value : undefined, true))
|
||||
}
|
||||
if (!state.started && kind !== "next") {
|
||||
state.completed = true
|
||||
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
|
||||
if (kind === "throw") return Effect.fail(new Throw(value))
|
||||
return Effect.succeed(result(value, true))
|
||||
}
|
||||
|
||||
@@ -1735,12 +1749,12 @@ class Frame<R> {
|
||||
yield* invocation.completeGeneratorRequests(state, asynchronous)
|
||||
state.completed = true
|
||||
})
|
||||
return Effect.andThen(this.runtime.promises.fork(body), Deferred.await(request.response))
|
||||
return Effect.andThen(this.ctx.pending.fork(body), Deferred.await(request.response))
|
||||
}
|
||||
return Deferred.await(request.response)
|
||||
}
|
||||
const generator = new ProgramGenerator(
|
||||
asynchronous ? protos.AsyncGenerator : protos.Generator,
|
||||
const generator = new GeneratorObj(
|
||||
asynchronous ? builtins.AsyncGenerator : builtins.Generator,
|
||||
asynchronous,
|
||||
request,
|
||||
)
|
||||
@@ -1749,13 +1763,13 @@ class Frame<R> {
|
||||
|
||||
private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
|
||||
const self = this
|
||||
const result = (value: unknown, done: boolean) => record(self.runtime.prototypes.Object, { value, done })
|
||||
const result = (value: unknown, done: boolean) => record(self.ctx.builtins.Object, { value, done })
|
||||
return Effect.gen(function* () {
|
||||
while (true) {
|
||||
const pending = self.dequeueGeneratorRequest(state)
|
||||
if (!pending) return
|
||||
if (pending.kind === "throw") {
|
||||
Deferred.doneUnsafe(pending.response, Exit.fail(new ProgramThrow(pending.value)))
|
||||
Deferred.doneUnsafe(pending.response, Exit.fail(new Throw(pending.value)))
|
||||
continue
|
||||
}
|
||||
if (asynchronous && pending.kind === "return") {
|
||||
@@ -1813,15 +1827,12 @@ class Frame<R> {
|
||||
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
const state = this.generatorState
|
||||
if (!state?.active) throw typeError("Generator has no active request.", node)
|
||||
Deferred.doneUnsafe(
|
||||
state.active.response,
|
||||
Exit.succeed(record(this.runtime.prototypes.Object, { value, done: false })),
|
||||
)
|
||||
Deferred.doneUnsafe(state.active.response, Exit.succeed(record(this.ctx.builtins.Object, { value, done: false })))
|
||||
state.active = undefined
|
||||
return Effect.flatMap(this.takeGeneratorRequest(state), (request) => {
|
||||
state.active = request
|
||||
if (request.kind === "next") return Effect.succeed(request.value)
|
||||
if (request.kind === "throw") return Effect.fail(new ProgramThrow(request.value))
|
||||
if (request.kind === "throw") return Effect.fail(new Throw(request.value))
|
||||
return this.generatorAsync
|
||||
? Effect.flatMap(this.awaitValue(request.value), (value) => Effect.fail(new GeneratorReturn(value)))
|
||||
: Effect.fail(new GeneratorReturn(request.value))
|
||||
@@ -1832,13 +1843,14 @@ class Frame<R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (
|
||||
value instanceof ProgramArray ||
|
||||
value instanceof Arr ||
|
||||
typeof value === "string" ||
|
||||
value instanceof ProgramMap ||
|
||||
value instanceof ProgramSet ||
|
||||
value instanceof ProgramURLSearchParams
|
||||
value instanceof MapObj ||
|
||||
value instanceof SetObj ||
|
||||
value instanceof URLSearchParamsObj ||
|
||||
value instanceof Bytes
|
||||
) {
|
||||
const cursor = yield* self.syncIterator(value, node)
|
||||
const cursor = yield* self.iterate(value, node)
|
||||
if (!cursor) throw typeError("Built-in iterator is unavailable.", node)
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -1852,7 +1864,7 @@ class Frame<R> {
|
||||
yield* cursor.close
|
||||
return yield* Effect.fail(error)
|
||||
}
|
||||
if (error instanceof ProgramThrow) {
|
||||
if (error instanceof Throw) {
|
||||
yield* cursor.close
|
||||
throw typeError("The delegated iterator does not provide a throw() method.", node)
|
||||
}
|
||||
@@ -1871,7 +1883,7 @@ class Frame<R> {
|
||||
yield* self.closeIterator(iterator, node, self.generatorAsync)
|
||||
throw typeError("The delegated iterator does not provide a throw() method.", node)
|
||||
}
|
||||
const called = yield* self.invokeCallable(
|
||||
const called = yield* self.call(
|
||||
self.requireIteratorMethod(method, `Iterator ${kind}`, node),
|
||||
iterator.iterator,
|
||||
[input],
|
||||
@@ -1899,7 +1911,7 @@ class Frame<R> {
|
||||
continue
|
||||
}
|
||||
const error: unknown = Cause.squash(resumed.cause)
|
||||
if (!(error instanceof GeneratorReturn) && !(error instanceof ProgramThrow)) {
|
||||
if (!(error instanceof GeneratorReturn) && !(error instanceof Throw)) {
|
||||
return yield* Effect.failCause(resumed.cause)
|
||||
}
|
||||
kind = error instanceof GeneratorReturn ? "return" : "throw"
|
||||
@@ -1908,15 +1920,15 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<ProgramObject, unknown, R> {
|
||||
const objectValue = new ProgramObject(this.runtime.prototypes.Object)
|
||||
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<Obj, unknown, R> {
|
||||
const objectValue = new Obj(this.ctx.builtins.Object)
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
for (const property of node.properties) {
|
||||
if (property.type === "SpreadElement") {
|
||||
const spread = yield* self.evaluateExpression(property.argument)
|
||||
if (spread === null || spread === undefined) continue
|
||||
assign(objectValue, enumerableSource(self.runtime.runner, "Object spread", spread, property))
|
||||
assign(objectValue, enumerableSource(self.ctx, "Object spread", spread, property))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1951,7 +1963,7 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<ProgramArray, unknown, R> {
|
||||
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<Arr, unknown, R> {
|
||||
const values: Array<unknown> = []
|
||||
|
||||
const self = this
|
||||
@@ -1964,7 +1976,7 @@ class Frame<R> {
|
||||
}
|
||||
if (element.type === "SpreadElement") {
|
||||
const spread = yield* self.evaluateExpression(element.argument)
|
||||
const cursor = yield* self.syncIterator(spread, element)
|
||||
const cursor = yield* self.iterate(spread, element)
|
||||
if (cursor === undefined) throw typeError("Array spread requires a synchronous iterable.", element)
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -1975,7 +1987,7 @@ class Frame<R> {
|
||||
values.push(yield* self.evaluateExpression(element))
|
||||
}
|
||||
}
|
||||
return new ProgramArray(self.runtime.prototypes.Array, values)
|
||||
return new Arr(self.ctx.builtins.Array, values)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1998,7 +2010,7 @@ class Frame<R> {
|
||||
|
||||
if (index < expressions.length) {
|
||||
const raw = yield* self.evaluateExpression(expressions[index])
|
||||
output += coerceToString(toProgram(self.runtime.prototypes, raw, "Template interpolation"))
|
||||
output += coerceToString(toProgram(self.ctx.builtins, raw, "Template interpolation"))
|
||||
checkStringLength(output.length)
|
||||
}
|
||||
}
|
||||
@@ -2046,18 +2058,18 @@ class Frame<R> {
|
||||
return new ToolReference([...objectValue.path, key])
|
||||
}
|
||||
|
||||
if (objectValue instanceof ProgramObject) return { target: objectValue, key, receiver: objectValue }
|
||||
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
|
||||
|
||||
// Primitives read through their wrapper prototype without being boxed; strings own length and indexes.
|
||||
const protos = self.runtime.prototypes
|
||||
const builtins = self.ctx.builtins
|
||||
if (typeof objectValue === "string") {
|
||||
if (key === "length") return { value: objectValue.length }
|
||||
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
|
||||
if (index !== undefined) return { value: objectValue[index] }
|
||||
return { target: protos.String, key, receiver: objectValue }
|
||||
return { target: builtins.String, key, receiver: objectValue }
|
||||
}
|
||||
if (typeof objectValue === "number") return { target: protos.Number, key, receiver: objectValue }
|
||||
if (typeof objectValue === "boolean") return { target: protos.Boolean, key, receiver: objectValue }
|
||||
if (typeof objectValue === "number") return { target: builtins.Number, key, receiver: objectValue }
|
||||
if (typeof objectValue === "boolean") return { target: builtins.Boolean, key, receiver: objectValue }
|
||||
|
||||
if (objectValue === null || objectValue === undefined) {
|
||||
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
|
||||
@@ -2068,7 +2080,7 @@ class Frame<R> {
|
||||
|
||||
private readReference(reference: MemberReference, node: MemberExpression): unknown {
|
||||
// Reject unknown promise properties so a missing await cannot hide.
|
||||
if (reference.target instanceof ProgramPromise && !has(reference.target, reference.key)) {
|
||||
if (reference.target instanceof PromiseObj && !has(reference.target, reference.key)) {
|
||||
throw invalidData(
|
||||
"This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.",
|
||||
node.object,
|
||||
@@ -2078,7 +2090,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// Accessors throw without a location; the member or pattern that read them supplies it.
|
||||
private readProperty(target: ProgramObject, key: PropertyKey, node: AstNode, receiver: unknown = target): unknown {
|
||||
private readProperty(target: Obj, key: PropertyKey, node: AstNode, receiver: unknown = target): unknown {
|
||||
try {
|
||||
return get(target, key, receiver)
|
||||
} catch (error) {
|
||||
@@ -2138,13 +2150,13 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private assignToReference(target: ProgramObject, key: PropertyKey, next: unknown, node: AstNode): void {
|
||||
private assignToReference(target: Obj, key: PropertyKey, next: unknown, node: AstNode): void {
|
||||
const written = (() => {
|
||||
try {
|
||||
rejectCircularInsertion(
|
||||
target,
|
||||
next,
|
||||
target instanceof ProgramArray ? "Array assignment result" : "Object assignment result",
|
||||
target instanceof Arr ? "Array assignment result" : "Object assignment result",
|
||||
)
|
||||
return set(target, key, next)
|
||||
} catch (error) {
|
||||
@@ -2152,7 +2164,7 @@ class Frame<R> {
|
||||
}
|
||||
})()
|
||||
if (written) return
|
||||
if (target instanceof ProgramArray && key === "length") throw rangeError("Invalid array length", node)
|
||||
if (target instanceof Arr && key === "length") throw rangeError("Invalid array length", node)
|
||||
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define, hidden, NativeFunction, ProgramArray, ProgramError, ProgramObject } from "./objects.js"
|
||||
import { define, hidden, Native, Arr, ErrorObj, Obj } from "./objects.js"
|
||||
|
||||
export const errorTypes = [
|
||||
"Error",
|
||||
@@ -29,6 +29,9 @@ const builtins = [
|
||||
"Set",
|
||||
"URL",
|
||||
"URLSearchParams",
|
||||
"Uint8Array",
|
||||
"TextEncoder",
|
||||
"TextDecoder",
|
||||
"Promise",
|
||||
"Iterator",
|
||||
"AsyncIterator",
|
||||
@@ -40,24 +43,24 @@ const builtins = [
|
||||
* The built-in prototype objects of one runtime, allocated empty in dependency order. The globals populate them
|
||||
* and attach their constructors when the runtime is built.
|
||||
*/
|
||||
export type Prototypes = Readonly<Record<(typeof builtins)[number] | ErrorType, ProgramObject>>
|
||||
export type Builtins = Readonly<Record<(typeof builtins)[number] | ErrorType, Obj>>
|
||||
|
||||
export const createErrorValue = (prototype: ProgramObject, message: string | undefined): ProgramError => {
|
||||
const value = new ProgramError(prototype)
|
||||
export const createErrorValue = (prototype: Obj, message: string | undefined): ErrorObj => {
|
||||
const value = new ErrorObj(prototype)
|
||||
if (message !== undefined) define(value, "message", message, hidden)
|
||||
return value
|
||||
}
|
||||
|
||||
export const createPrototypes = (): Prototypes => {
|
||||
const object = new ProgramObject(null)
|
||||
export const createBuiltins = (): Builtins => {
|
||||
const object = new Obj(null)
|
||||
// Function.prototype is itself callable and returns undefined.
|
||||
const fn = new NativeFunction(object, { name: "", call: () => Effect.undefined })
|
||||
const plain = () => new ProgramObject(object)
|
||||
const fn = new Native(object, { name: "", call: () => Effect.undefined })
|
||||
const plain = () => new Obj(object)
|
||||
const error = plain()
|
||||
define(error, "name", "Error", hidden)
|
||||
define(error, "message", "", hidden)
|
||||
const derived = (type: ErrorType) => {
|
||||
const proto = new ProgramObject(error)
|
||||
const proto = new Obj(error)
|
||||
define(proto, "name", type, hidden)
|
||||
define(proto, "message", "", hidden)
|
||||
return proto
|
||||
@@ -67,7 +70,7 @@ export const createPrototypes = (): Prototypes => {
|
||||
return {
|
||||
Object: object,
|
||||
Function: fn,
|
||||
Array: new ProgramArray(object),
|
||||
Array: new Arr(object),
|
||||
String: plain(),
|
||||
Number: plain(),
|
||||
Boolean: plain(),
|
||||
@@ -77,11 +80,14 @@ export const createPrototypes = (): Prototypes => {
|
||||
Set: plain(),
|
||||
URL: plain(),
|
||||
URLSearchParams: plain(),
|
||||
Uint8Array: plain(),
|
||||
TextEncoder: plain(),
|
||||
TextDecoder: plain(),
|
||||
Promise: plain(),
|
||||
Iterator: iterator,
|
||||
AsyncIterator: asyncIterator,
|
||||
Generator: new ProgramObject(iterator),
|
||||
AsyncGenerator: new ProgramObject(asyncIterator),
|
||||
Generator: new Obj(iterator),
|
||||
AsyncGenerator: new Obj(asyncIterator),
|
||||
Error: error,
|
||||
TypeError: derived("TypeError"),
|
||||
RangeError: derived("RangeError"),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Node } from "acorn"
|
||||
import { Context } from "effect"
|
||||
import type { ErrorType } from "./intrinsics.js"
|
||||
import type { DiagnosticKind } from "../codemode.js"
|
||||
import type { ProgramError } from "./objects.js"
|
||||
import type { ErrorObj } from "./objects.js"
|
||||
|
||||
/** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */
|
||||
export type AstNode = Node
|
||||
@@ -30,7 +30,7 @@ export const AsyncIteratorSymbol: unique symbol = Symbol("codemode.async-iterato
|
||||
export const IteratorSymbol: unique symbol = Symbol("codemode.iterator")
|
||||
export const IteratorSymbols = [AsyncIteratorSymbol, IteratorSymbol] as const
|
||||
|
||||
export class ProgramThrow {
|
||||
export class Throw {
|
||||
constructor(readonly value: unknown) {}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-sho
|
||||
*/
|
||||
export class PendingThrow {
|
||||
node?: AstNode
|
||||
value?: ProgramError
|
||||
value?: ErrorObj
|
||||
|
||||
constructor(
|
||||
/** The JS error class a program sees when it catches this failure. */
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Prototypes } from "./intrinsics.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { typeError } from "./model.js"
|
||||
import { type Callable, define, frozen, hidden, NativeFunction, type NativeOptions, ProgramObject } from "./objects.js"
|
||||
import { type Callable, define, frozen, hidden, Native, type NativeOptions, Obj } from "./objects.js"
|
||||
import { describeValue } from "./references.js"
|
||||
|
||||
/** A native function body: a plain value, a thrown `PendingThrow`, or an Effect. */
|
||||
export type Impl = (thisValue: unknown, args: Array<unknown>) => unknown
|
||||
|
||||
// The dispatch in `Frame.invokeCallable` suspends every native call, so a synchronous throw here is a defect.
|
||||
// The dispatch in `Frame.call` suspends every native call, so a synchronous throw here is a defect.
|
||||
const lift =
|
||||
<R>(impl: Impl) =>
|
||||
(thisValue: unknown, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
@@ -15,29 +15,25 @@ const lift =
|
||||
return Effect.isEffect(result) ? (result as Effect.Effect<unknown, unknown, R>) : Effect.succeed(result)
|
||||
}
|
||||
|
||||
export const native = <R>(protos: Prototypes, options: NativeOptions<R>): NativeFunction<R> =>
|
||||
new NativeFunction<R>(protos.Function, options)
|
||||
export const native = <R>(builtins: Builtins, options: NativeOptions<R>): Native<R> =>
|
||||
new Native<R>(builtins.Function, options)
|
||||
|
||||
export const fn = <R>(protos: Prototypes, name: string, length: number, impl: Impl): NativeFunction<R> =>
|
||||
native<R>(protos, { name, length, call: lift(impl) })
|
||||
export const fn = <R>(builtins: Builtins, name: string, length: number, impl: Impl): Native<R> =>
|
||||
native<R>(builtins, { name, length, call: lift(impl) })
|
||||
|
||||
export type Method = readonly [name: string, length: number, impl: Impl]
|
||||
|
||||
export const methods = (protos: Prototypes, target: ProgramObject, table: ReadonlyArray<Method>): void => {
|
||||
for (const [name, length, impl] of table) define(target, name, fn(protos, name, length, impl), hidden)
|
||||
export const methods = (builtins: Builtins, target: Obj, table: ReadonlyArray<Method>): void => {
|
||||
for (const [name, length, impl] of table) define(target, name, fn(builtins, name, length, impl), hidden)
|
||||
}
|
||||
|
||||
export const constants = (target: ProgramObject, table: Record<string, unknown>): void => {
|
||||
export const constants = (target: Obj, table: Record<string, unknown>): void => {
|
||||
for (const [name, value] of Object.entries(table)) define(target, name, value, frozen)
|
||||
}
|
||||
|
||||
/** A constructor wired to its prototype: `C.prototype === proto` and `proto.constructor === C`. */
|
||||
export const constructor = <R>(
|
||||
protos: Prototypes,
|
||||
proto: ProgramObject,
|
||||
options: NativeOptions<R>,
|
||||
): NativeFunction<R> => {
|
||||
const ctor = native<R>(protos, options)
|
||||
export const constructor = <R>(builtins: Builtins, proto: Obj, options: NativeOptions<R>): Native<R> => {
|
||||
const ctor = native<R>(builtins, options)
|
||||
define(ctor, "prototype", proto, frozen)
|
||||
define(proto, "constructor", ctor, hidden)
|
||||
return ctor
|
||||
@@ -50,13 +46,13 @@ export const requiresNew = (name: string) => (): Effect.Effect<never, unknown, n
|
||||
})
|
||||
|
||||
/** The instance prototype for `new` via `newTarget.prototype`, falling back to the built-in's own. */
|
||||
export const prototypeFrom = (newTarget: Callable, fallback: ProgramObject): ProgramObject => {
|
||||
export const prototypeFrom = (newTarget: Callable, fallback: Obj): Obj => {
|
||||
const proto = newTarget.props.get("prototype")
|
||||
return proto !== undefined && "value" in proto && proto.value instanceof ProgramObject ? proto.value : fallback
|
||||
return proto !== undefined && "value" in proto && proto.value instanceof Obj ? proto.value : fallback
|
||||
}
|
||||
|
||||
/** Narrows a method receiver to the built-in it belongs to, or throws the TypeError JS would. */
|
||||
export const receiver = <T extends ProgramObject>(
|
||||
export const receiver = <T extends Obj>(
|
||||
cls: abstract new (...args: never) => T,
|
||||
thisValue: unknown,
|
||||
method: string,
|
||||
|
||||
@@ -34,14 +34,14 @@ export const readonly: Attributes = { writable: false, enumerable: false, config
|
||||
export const frozen: Attributes = { writable: false, enumerable: false, configurable: false }
|
||||
|
||||
/** An object owned by the program: own properties plus a prototype link. */
|
||||
export class ProgramObject {
|
||||
export class Obj {
|
||||
readonly props = new Map<string | symbol, Slot>()
|
||||
constructor(public proto: ProgramObject | null) {}
|
||||
constructor(public proto: Obj | null) {}
|
||||
}
|
||||
|
||||
export class ProgramArray extends ProgramObject {
|
||||
export class Arr extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
readonly items: Array<unknown> = [],
|
||||
) {
|
||||
super(proto)
|
||||
@@ -49,22 +49,22 @@ export class ProgramArray extends ProgramObject {
|
||||
}
|
||||
|
||||
/** An object with the [[ErrorData]] slot: what `Error.prototype.toString` and the host boundary recognize as an error. */
|
||||
export class ProgramError extends ProgramObject {
|
||||
export class ErrorObj extends Obj {
|
||||
/** The interpreter failure this error materialized from, so rethrowing it keeps the diagnostic kind and location. */
|
||||
host?: PendingThrow
|
||||
}
|
||||
|
||||
export abstract class Callable extends ProgramObject {
|
||||
constructor(proto: ProgramObject, name: string, length: number) {
|
||||
export abstract class Callable extends Obj {
|
||||
constructor(proto: Obj, name: string, length: number) {
|
||||
super(proto)
|
||||
define(this, "length", length, readonly)
|
||||
define(this, "name", name, readonly)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramFunction extends Callable {
|
||||
export class Fn extends Callable {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
name: string,
|
||||
readonly parameters: ReadonlyArray<Pattern>,
|
||||
readonly body: BlockStatement | Expression,
|
||||
@@ -90,12 +90,12 @@ export type NativeOptions<R> = {
|
||||
readonly callback?: boolean
|
||||
}
|
||||
|
||||
export class NativeFunction<R = never> extends Callable {
|
||||
export class Native<R = never> extends Callable {
|
||||
readonly call: NativeCall<R>
|
||||
readonly construct: NativeConstruct<R> | undefined
|
||||
readonly callback: boolean
|
||||
|
||||
constructor(proto: ProgramObject, options: NativeOptions<R>) {
|
||||
constructor(proto: Obj, options: NativeOptions<R>) {
|
||||
super(proto, options.name, options.length ?? 0)
|
||||
this.call = options.call
|
||||
this.construct = options.construct
|
||||
@@ -103,18 +103,18 @@ export class NativeFunction<R = never> extends Callable {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramPromise extends ProgramObject {
|
||||
export class PromiseObj extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
readonly fiber: Fiber.Fiber<unknown, unknown>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramGenerator extends ProgramObject {
|
||||
export class GeneratorObj extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
readonly asynchronous: boolean,
|
||||
readonly request: (kind: GeneratorRequestKind, value: unknown) => Effect.Effect<unknown, unknown, unknown>,
|
||||
) {
|
||||
@@ -122,57 +122,67 @@ export class ProgramGenerator extends ProgramObject {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramDate extends ProgramObject {
|
||||
export class DateObj extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
public time: number,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramRegExp extends ProgramObject {
|
||||
export class RegExpObj extends Obj {
|
||||
readonly regex: RegExp
|
||||
constructor(proto: ProgramObject, pattern: string, flags: string) {
|
||||
constructor(proto: Obj, pattern: string, flags: string) {
|
||||
super(proto)
|
||||
this.regex = new RegExp(pattern, flags)
|
||||
define(this, "lastIndex", 0, { writable: true, enumerable: false, configurable: false })
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramMap extends ProgramObject {
|
||||
export class MapObj extends Obj {
|
||||
readonly map = new Map<unknown, unknown>()
|
||||
}
|
||||
|
||||
export class ProgramSet extends ProgramObject {
|
||||
export class SetObj extends Obj {
|
||||
readonly set = new Set<unknown>()
|
||||
}
|
||||
|
||||
export class ProgramURLSearchParams extends ProgramObject {
|
||||
export class URLSearchParamsObj extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
readonly params: URLSearchParams,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramURL extends ProgramObject {
|
||||
readonly searchParams: ProgramURLSearchParams
|
||||
export class URLObj extends Obj {
|
||||
readonly searchParams: URLSearchParamsObj
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
searchParamsProto: ProgramObject,
|
||||
proto: Obj,
|
||||
searchParamsProto: Obj,
|
||||
readonly url: URL,
|
||||
) {
|
||||
super(proto)
|
||||
this.searchParams = new ProgramURLSearchParams(searchParamsProto, url.searchParams)
|
||||
this.searchParams = new URLSearchParamsObj(searchParamsProto, url.searchParams)
|
||||
}
|
||||
}
|
||||
|
||||
/** A `Uint8Array`: the host array does the byte clamping and ignores out-of-range writes, as JS does. */
|
||||
export class Bytes extends Obj {
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly bytes: Uint8Array,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
/** An instance of an extension class: the host object lives in a field no property path reaches. */
|
||||
export class ProgramHandle extends ProgramObject {
|
||||
export class Handle extends Obj {
|
||||
constructor(
|
||||
proto: ProgramObject,
|
||||
proto: Obj,
|
||||
readonly instance: object,
|
||||
) {
|
||||
super(proto)
|
||||
@@ -182,13 +192,14 @@ export class ProgramHandle extends ProgramObject {
|
||||
/** Built-in objects that wrap a host value; data-like, but never plain data. */
|
||||
export const isWrapper = (
|
||||
value: unknown,
|
||||
): value is ProgramDate | ProgramRegExp | ProgramMap | ProgramSet | ProgramURL | ProgramURLSearchParams =>
|
||||
value instanceof ProgramDate ||
|
||||
value instanceof ProgramRegExp ||
|
||||
value instanceof ProgramMap ||
|
||||
value instanceof ProgramSet ||
|
||||
value instanceof ProgramURL ||
|
||||
value instanceof ProgramURLSearchParams
|
||||
): value is DateObj | RegExpObj | MapObj | SetObj | URLObj | URLSearchParamsObj | Bytes =>
|
||||
value instanceof DateObj ||
|
||||
value instanceof RegExpObj ||
|
||||
value instanceof MapObj ||
|
||||
value instanceof SetObj ||
|
||||
value instanceof URLObj ||
|
||||
value instanceof URLSearchParamsObj ||
|
||||
value instanceof Bytes
|
||||
|
||||
const MAX_ARRAY_INDEX = 4_294_967_295
|
||||
|
||||
@@ -201,18 +212,28 @@ export const parseArrayIndex = (key: string | number): number | undefined => {
|
||||
|
||||
const canonical = (key: PropertyKey): string | symbol => (typeof key === "symbol" ? key : String(key))
|
||||
|
||||
const index = (target: ProgramObject, key: string | symbol): number | undefined =>
|
||||
target instanceof ProgramArray && typeof key === "string" ? parseArrayIndex(key) : undefined
|
||||
/** Objects whose integer keys are live elements rather than own property slots. */
|
||||
type Indexed = Arr | Bytes
|
||||
|
||||
const isIndexed = (target: Obj): target is Indexed => target instanceof Arr || target instanceof Bytes
|
||||
|
||||
const elements = (target: Indexed): Array<unknown> | Uint8Array => (target instanceof Arr ? target.items : target.bytes)
|
||||
|
||||
const index = (target: Obj, key: string | symbol): number | undefined =>
|
||||
isIndexed(target) && typeof key === "string" ? parseArrayIndex(key) : undefined
|
||||
|
||||
/** The own property under `key`, including an array's live indexes and `length`. */
|
||||
export const own = (target: ProgramObject, key: PropertyKey): Slot | undefined => {
|
||||
export const own = (target: Obj, key: PropertyKey): Slot | undefined => {
|
||||
const name = canonical(key)
|
||||
if (target instanceof ProgramArray) {
|
||||
if (isIndexed(target)) {
|
||||
const at = index(target, name)
|
||||
if (at !== undefined) {
|
||||
return at in target.items ? { value: target.items[at], ...data } : undefined
|
||||
const items = elements(target)
|
||||
return at in items ? { value: items[at], ...data } : undefined
|
||||
}
|
||||
if (target instanceof Arr && name === "length") {
|
||||
return { value: target.items.length, writable: true, enumerable: false, configurable: false }
|
||||
}
|
||||
if (name === "length") return { value: target.items.length, writable: true, enumerable: false, configurable: false }
|
||||
}
|
||||
return target.props.get(name)
|
||||
}
|
||||
@@ -220,43 +241,44 @@ export const own = (target: ProgramObject, key: PropertyKey): Slot | undefined =
|
||||
const read = (slot: Slot, receiver: unknown): unknown =>
|
||||
"value" in slot ? slot.value : slot.get === undefined ? undefined : slot.get(receiver)
|
||||
|
||||
export const hasOwn = (target: ProgramObject, key: PropertyKey): boolean => own(target, key) !== undefined
|
||||
export const hasOwn = (target: Obj, key: PropertyKey): boolean => own(target, key) !== undefined
|
||||
|
||||
export const getOwn = (target: ProgramObject, key: PropertyKey): unknown => {
|
||||
export const getOwn = (target: Obj, key: PropertyKey): unknown => {
|
||||
const slot = own(target, key)
|
||||
return slot === undefined ? undefined : read(slot, target)
|
||||
}
|
||||
|
||||
/** [[Get]]: walks the prototype chain; accessors see `receiver`, which is the primitive for wrapper prototypes. */
|
||||
export const get = (target: ProgramObject, key: PropertyKey, receiver: unknown = target): unknown => {
|
||||
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
|
||||
export const get = (target: Obj, key: PropertyKey, receiver: unknown = target): unknown => {
|
||||
for (let current: Obj | null = target; current !== null; current = current.proto) {
|
||||
const slot = own(current, key)
|
||||
if (slot !== undefined) return read(slot, receiver)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const has = (target: ProgramObject, key: PropertyKey): boolean => {
|
||||
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
|
||||
export const has = (target: Obj, key: PropertyKey): boolean => {
|
||||
for (let current: Obj | null = target; current !== null; current = current.proto) {
|
||||
if (own(current, key) !== undefined) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const hasPrototype = (value: unknown, proto: ProgramObject): boolean => {
|
||||
for (let current = value instanceof ProgramObject ? value.proto : null; current !== null; current = current.proto) {
|
||||
export const hasPrototype = (value: unknown, proto: Obj): boolean => {
|
||||
for (let current = value instanceof Obj ? value.proto : null; current !== null; current = current.proto) {
|
||||
if (current === proto) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const writeArray = (target: ProgramArray, name: string | symbol, value: unknown): boolean | undefined => {
|
||||
const writeElement = (target: Indexed, name: string | symbol, value: unknown): boolean | undefined => {
|
||||
const at = index(target, name)
|
||||
if (at !== undefined) {
|
||||
target.items[at] = value
|
||||
if (target instanceof Bytes) target.bytes[at] = typeof value === "number" ? value : Number(value)
|
||||
else target.items[at] = value
|
||||
return true
|
||||
}
|
||||
if (name !== "length") return undefined
|
||||
if (!(target instanceof Arr) || name !== "length") return undefined
|
||||
const length = typeof value === "number" ? value : Number(value)
|
||||
if (!Number.isInteger(length) || length < 0) return false
|
||||
checkArrayLength(length)
|
||||
@@ -265,9 +287,9 @@ const writeArray = (target: ProgramArray, name: string | symbol, value: unknown)
|
||||
}
|
||||
|
||||
/** [[Set]]: an inherited setter or read-only property decides before an own data property is created. */
|
||||
export const set = (target: ProgramObject, key: PropertyKey, value: unknown): boolean => {
|
||||
export const set = (target: Obj, key: PropertyKey, value: unknown): boolean => {
|
||||
const name = canonical(key)
|
||||
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
|
||||
for (let current: Obj | null = target; current !== null; current = current.proto) {
|
||||
const slot = own(current, name)
|
||||
if (slot === undefined) continue
|
||||
if (!("value" in slot)) {
|
||||
@@ -277,15 +299,15 @@ export const set = (target: ProgramObject, key: PropertyKey, value: unknown): bo
|
||||
}
|
||||
if (!slot.writable) return false
|
||||
if (current !== target) break
|
||||
if (target instanceof ProgramArray) {
|
||||
const written = writeArray(target, name, value)
|
||||
if (isIndexed(target)) {
|
||||
const written = writeElement(target, name, value)
|
||||
if (written !== undefined) return written
|
||||
}
|
||||
slot.value = value
|
||||
return true
|
||||
}
|
||||
if (target instanceof ProgramArray) {
|
||||
const written = writeArray(target, name, value)
|
||||
if (isIndexed(target)) {
|
||||
const written = writeElement(target, name, value)
|
||||
if (written !== undefined) return written
|
||||
}
|
||||
target.props.set(name, { value, ...data })
|
||||
@@ -293,27 +315,22 @@ export const set = (target: ProgramObject, key: PropertyKey, value: unknown): bo
|
||||
}
|
||||
|
||||
/** [[DefineOwnProperty]] for a data property, ignoring the chain. */
|
||||
export const define = (target: ProgramObject, key: PropertyKey, value: unknown, attrs: Attributes = data): void => {
|
||||
export const define = (target: Obj, key: PropertyKey, value: unknown, attrs: Attributes = data): void => {
|
||||
const name = canonical(key)
|
||||
if (target instanceof ProgramArray && writeArray(target, name, value) !== undefined) return
|
||||
if (isIndexed(target) && writeElement(target, name, value) !== undefined) return
|
||||
target.props.set(name, { value, ...attrs })
|
||||
}
|
||||
|
||||
export const defineAccessor = (
|
||||
target: ProgramObject,
|
||||
key: PropertyKey,
|
||||
get: Getter | undefined,
|
||||
set?: Setter,
|
||||
): void => {
|
||||
export const defineAccessor = (target: Obj, key: PropertyKey, get: Getter | undefined, set?: Setter): void => {
|
||||
target.props.set(canonical(key), { get, set, enumerable: false, configurable: true })
|
||||
}
|
||||
|
||||
export const remove = (target: ProgramObject, key: PropertyKey): boolean => {
|
||||
export const remove = (target: Obj, key: PropertyKey): boolean => {
|
||||
const name = canonical(key)
|
||||
if (target instanceof ProgramArray) {
|
||||
if (isIndexed(target)) {
|
||||
const at = index(target, name)
|
||||
if (at !== undefined) return delete target.items[at]
|
||||
if (name === "length") return false
|
||||
if (at !== undefined) return target instanceof Bytes ? !(at in target.bytes) : delete target.items[at]
|
||||
if (target instanceof Arr && name === "length") return false
|
||||
}
|
||||
const slot = target.props.get(name)
|
||||
if (slot === undefined) return true
|
||||
@@ -323,41 +340,41 @@ export const remove = (target: ProgramObject, key: PropertyKey): boolean => {
|
||||
}
|
||||
|
||||
// JS order: array indexes, integer-like keys ascending, other strings, then symbols.
|
||||
export const ownKeys = (target: ProgramObject): Array<string | symbol> => {
|
||||
export const ownKeys = (target: Obj): Array<string | symbol> => {
|
||||
const strings = [...target.props.keys()].filter((key): key is string => typeof key === "string")
|
||||
const symbols = [...target.props.keys()].filter((key): key is symbol => typeof key === "symbol")
|
||||
return [
|
||||
...(target instanceof ProgramArray ? [...Object.keys(target.items), "length"] : []),
|
||||
...(isIndexed(target) ? Object.keys(elements(target)) : []),
|
||||
...(target instanceof Arr ? ["length"] : []),
|
||||
...strings.filter((key) => parseArrayIndex(key) !== undefined).sort((a, b) => Number(a) - Number(b)),
|
||||
...strings.filter((key) => parseArrayIndex(key) === undefined),
|
||||
...symbols,
|
||||
]
|
||||
}
|
||||
|
||||
const enumerable = (target: ProgramObject, key: string | symbol): boolean => own(target, key)?.enumerable === true
|
||||
const enumerable = (target: Obj, key: string | symbol): boolean => own(target, key)?.enumerable === true
|
||||
|
||||
/** Own enumerable keys, including the iterator symbols; what spread and `Object.assign` copy. */
|
||||
export const enumerableKeys = (target: ProgramObject): Array<string | symbol> =>
|
||||
export const enumerableKeys = (target: Obj): Array<string | symbol> =>
|
||||
ownKeys(target).filter(
|
||||
(key) =>
|
||||
(typeof key === "string" || key === IteratorSymbol || key === AsyncIteratorSymbol) && enumerable(target, key),
|
||||
)
|
||||
|
||||
/** Own enumerable string keys: `Object.keys`. */
|
||||
export const keys = (target: ProgramObject): Array<string> =>
|
||||
export const keys = (target: Obj): Array<string> =>
|
||||
ownKeys(target).filter((key): key is string => typeof key === "string" && enumerable(target, key))
|
||||
|
||||
/** Own enumerable string entries: `Object.entries` and serialization. */
|
||||
export const entries = (target: ProgramObject): Array<[string, unknown]> =>
|
||||
keys(target).map((key) => [key, getOwn(target, key)])
|
||||
export const entries = (target: Obj): Array<[string, unknown]> => keys(target).map((key) => [key, getOwn(target, key)])
|
||||
|
||||
export const record = (proto: ProgramObject, fields: Record<string, unknown>): ProgramObject => {
|
||||
const target = new ProgramObject(proto)
|
||||
export const record = (proto: Obj, fields: Record<string, unknown>): Obj => {
|
||||
const target = new Obj(proto)
|
||||
for (const [key, value] of Object.entries(fields)) define(target, key, value)
|
||||
return target
|
||||
}
|
||||
|
||||
export const assign = (target: ProgramObject, source: ProgramObject, skip?: ReadonlySet<PropertyKey>): void => {
|
||||
export const assign = (target: Obj, source: Obj, skip?: ReadonlySet<PropertyKey>): void => {
|
||||
for (const key of enumerableKeys(source)) {
|
||||
if (skip?.has(key)) continue
|
||||
set(target, key, getOwn(source, key))
|
||||
|
||||
@@ -1,55 +1,46 @@
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
|
||||
import type { Diagnostic } from "../codemode.js"
|
||||
import { MAX_PENDING_PROMISES } from "./limits.js"
|
||||
import { CallSite, ProgramThrow, rangeError, typeError } from "./model.js"
|
||||
import {
|
||||
Callable,
|
||||
define,
|
||||
get,
|
||||
hidden,
|
||||
ProgramArray,
|
||||
ProgramFunction,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
record,
|
||||
} from "./objects.js"
|
||||
import { CallSite, Throw, rangeError, typeError } from "./model.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj, PromiseObj, record } from "./objects.js"
|
||||
import { constructor, fn, methods, native, receiver, requiresNew } from "./native.js"
|
||||
import { createAggregateErrorValue, locate, materialize, normalizeError } from "./errors.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
import { applyCollectionCallback, isSupportedCallback, type Runner } from "./runner.js"
|
||||
import { applyCollectionCallback, isSupportedCallback } from "./callback.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
|
||||
// A `resolve`/`reject` handed to an executor or thenable: calling it settles the capability.
|
||||
const capability = <R>(runner: Runner<R>, name: string, settle: (value: unknown) => void) =>
|
||||
fn(runner.prototypes, name, 1, (_, args) => {
|
||||
const capability = <R>(ctx: Interpreter<R>, name: string, settle: (value: unknown) => void) =>
|
||||
fn(ctx.builtins, name, 1, (_, args) => {
|
||||
settle(args[0])
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Observation only controls rejection reporting; program completion interrupts all promise work.
|
||||
export class PromiseRuntime<R> {
|
||||
private readonly active = new Set<ProgramPromise>()
|
||||
private readonly ids = new WeakMap<ProgramPromise, number>()
|
||||
private readonly observed = new WeakSet<ProgramPromise>()
|
||||
export class Pending<R> {
|
||||
private readonly active = new Set<PromiseObj>()
|
||||
private readonly ids = new WeakMap<PromiseObj, number>()
|
||||
private readonly observed = new WeakSet<PromiseObj>()
|
||||
private readonly failures = new Map<number, Diagnostic>()
|
||||
private nextID = 0
|
||||
|
||||
constructor(
|
||||
private readonly scope: Scope.Scope,
|
||||
private readonly proto: ProgramObject,
|
||||
private readonly proto: Obj,
|
||||
) {}
|
||||
|
||||
// Resolution bodies need the promise's own identity to reject `resolve(promise)` self-resolution.
|
||||
createWithSelf(
|
||||
body: (self: { promise?: ProgramPromise }) => Effect.Effect<unknown, unknown, R>,
|
||||
): Effect.Effect<ProgramPromise, never, R> {
|
||||
const self: { promise?: ProgramPromise } = {}
|
||||
body: (self: { promise?: PromiseObj }) => Effect.Effect<unknown, unknown, R>,
|
||||
): Effect.Effect<PromiseObj, never, R> {
|
||||
const self: { promise?: PromiseObj } = {}
|
||||
return Effect.map(this.create(body(self)), (promise) => {
|
||||
self.promise = promise
|
||||
return promise
|
||||
})
|
||||
}
|
||||
|
||||
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<ProgramPromise, never, R> {
|
||||
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<PromiseObj, never, R> {
|
||||
return Effect.flatMap(CallSite, (site) => {
|
||||
if (this.active.size >= MAX_PENDING_PROMISES) {
|
||||
throw rangeError(
|
||||
@@ -60,7 +51,7 @@ export class PromiseRuntime<R> {
|
||||
const id = this.nextID++
|
||||
const body = Effect.catchDefect(effect, (defect) => Effect.die(locate(defect, site.node)))
|
||||
return Effect.map(Effect.forkIn(body, this.scope, { startImmediately: true }), (fiber) => {
|
||||
const promise = new ProgramPromise(this.proto, fiber)
|
||||
const promise = new PromiseObj(this.proto, fiber)
|
||||
this.active.add(promise)
|
||||
this.ids.set(promise, id)
|
||||
fiber.addObserver((exit) => {
|
||||
@@ -81,14 +72,14 @@ export class PromiseRuntime<R> {
|
||||
}
|
||||
|
||||
// Observation must be recorded when responsibility transfers, before the consumer fiber runs.
|
||||
markObserved(promise: ProgramPromise): void {
|
||||
markObserved(promise: PromiseObj): void {
|
||||
this.observed.add(promise)
|
||||
const id = this.ids.get(promise)
|
||||
this.ids.delete(promise)
|
||||
if (id !== undefined) this.failures.delete(id)
|
||||
}
|
||||
|
||||
await(promise: ProgramPromise): Effect.Effect<Exit.Exit<unknown, unknown>> {
|
||||
await(promise: PromiseObj): Effect.Effect<Exit.Exit<unknown, unknown>> {
|
||||
return Fiber.await(promise.fiber)
|
||||
}
|
||||
|
||||
@@ -113,15 +104,15 @@ export class PromiseRuntime<R> {
|
||||
}
|
||||
|
||||
export const resolvePromiseValue = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
value: unknown,
|
||||
own?: { promise?: ProgramPromise },
|
||||
own?: { promise?: PromiseObj },
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (own?.promise !== undefined && value === own.promise) {
|
||||
return Effect.die(typeError("Chaining cycle detected: a promise cannot resolve with itself."))
|
||||
}
|
||||
if (value instanceof ProgramPromise) return runner.settlePromise(value)
|
||||
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
|
||||
if (value instanceof PromiseObj) return ctx.await(value)
|
||||
if (!(value instanceof Obj)) return Effect.succeed(value)
|
||||
const then = get(value, "then")
|
||||
if (typeofValue(then) !== "function") return Effect.succeed(value)
|
||||
|
||||
@@ -129,62 +120,55 @@ export const resolvePromiseValue = <R>(
|
||||
// Promise resolution invokes a thenable's method in a later job.
|
||||
yield* Effect.yieldNow
|
||||
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const resolve = capability(runner, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)))
|
||||
const reject = capability(runner, "reject", (reason) =>
|
||||
Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(reason))),
|
||||
)
|
||||
const executed = yield* Effect.exit(runner.invokeCallable(then, value, [resolve, reject]))
|
||||
const resolve = capability(ctx, "resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result)))
|
||||
const reject = capability(ctx, "reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(reason))))
|
||||
const executed = yield* Effect.exit(ctx.call(then, value, [resolve, reject]))
|
||||
if (!Exit.isSuccess(executed)) {
|
||||
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
|
||||
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
|
||||
}
|
||||
return yield* resolvePromiseValue(runner, yield* Deferred.await(deferred), own)
|
||||
return yield* resolvePromiseValue(ctx, yield* Deferred.await(deferred), own)
|
||||
})
|
||||
}
|
||||
|
||||
export const resolvePromise = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
value: unknown,
|
||||
): Effect.Effect<ProgramPromise, never, R> => {
|
||||
if (value instanceof ProgramPromise) return Effect.succeed(value)
|
||||
return promises.createWithSelf((self) => resolvePromiseValue(runner, value, self))
|
||||
export const resolvePromise = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<PromiseObj, never, R> => {
|
||||
if (value instanceof PromiseObj) return Effect.succeed(value)
|
||||
return ctx.pending.createWithSelf((self) => resolvePromiseValue(ctx, value, self))
|
||||
}
|
||||
|
||||
const promiseStatics = ["all", "allSettled", "race", "any", "resolve", "reject"] as const
|
||||
|
||||
const invokePromiseMethod = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
ctx: Interpreter<R>,
|
||||
name: (typeof promiseStatics)[number],
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (name === "resolve") {
|
||||
return resolvePromise(runner, promises, args[0])
|
||||
return resolvePromise(ctx, args[0])
|
||||
}
|
||||
if (name === "reject") {
|
||||
return promises.create(Effect.fail(new ProgramThrow(args[0])))
|
||||
return ctx.pending.create(Effect.fail(new Throw(args[0])))
|
||||
}
|
||||
|
||||
return promises.create(
|
||||
return ctx.pending.create(
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(args[0])
|
||||
const cursor = yield* ctx.iterate(args[0])
|
||||
if (cursor === undefined) throw typeError(`Promise.${name} expects an array or other synchronous iterable.`)
|
||||
const items: Array<ProgramPromise> = []
|
||||
const items: Array<PromiseObj> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) break
|
||||
const item = yield* resolvePromise(runner, promises, step.value)
|
||||
promises.markObserved(item)
|
||||
const item = yield* resolvePromise(ctx, step.value)
|
||||
ctx.pending.markObserved(item)
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
if (name === "all") {
|
||||
return new ProgramArray(
|
||||
runner.prototypes.Array,
|
||||
return new Arr(
|
||||
ctx.builtins.Array,
|
||||
yield* settleAfterTurn(
|
||||
Effect.all(
|
||||
items.map((item) => Effect.flatten(promises.await(item))),
|
||||
items.map((item) => Effect.flatten(ctx.pending.await(item))),
|
||||
{ concurrency: "unbounded" },
|
||||
),
|
||||
),
|
||||
@@ -193,39 +177,39 @@ const invokePromiseMethod = <R>(
|
||||
if (name === "allSettled") {
|
||||
const outcomes: Array<unknown> = []
|
||||
for (const item of items) {
|
||||
const exit = yield* promises.await(item)
|
||||
const exit = yield* ctx.pending.await(item)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
outcomes.push(record(runner.prototypes.Object, { status: "fulfilled", value: exit.value }))
|
||||
outcomes.push(record(ctx.builtins.Object, { status: "fulfilled", value: exit.value }))
|
||||
continue
|
||||
}
|
||||
if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
|
||||
outcomes.push(
|
||||
record(runner.prototypes.Object, {
|
||||
record(ctx.builtins.Object, {
|
||||
status: "rejected",
|
||||
reason: materialize(runner, Cause.squash(exit.cause)),
|
||||
reason: materialize(ctx, Cause.squash(exit.cause)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
yield* Effect.yieldNow
|
||||
return new ProgramArray(runner.prototypes.Array, outcomes)
|
||||
return new Arr(ctx.builtins.Array, outcomes)
|
||||
}
|
||||
if (name === "race") {
|
||||
if (items.length === 0) {
|
||||
throw typeError("Promise.race([]) would never settle; provide at least one promise or value.")
|
||||
}
|
||||
return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => promises.await(item)))))
|
||||
return yield* settleAfterTurn(Effect.flatten(Effect.raceAll(items.map((item) => ctx.pending.await(item)))))
|
||||
}
|
||||
const flipped = items.map((item) =>
|
||||
Effect.flatMap(promises.await(item), (exit) => {
|
||||
Effect.flatMap(ctx.pending.await(item), (exit) => {
|
||||
if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
|
||||
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
|
||||
return Effect.succeed(materialize(runner, Cause.squash(exit.cause)))
|
||||
return Effect.succeed(materialize(ctx, Cause.squash(exit.cause)))
|
||||
}),
|
||||
)
|
||||
return yield* settleAfterTurn(
|
||||
Effect.all(flipped, { concurrency: "unbounded" }).pipe(
|
||||
Effect.flatMap((reasons) =>
|
||||
Effect.fail(new ProgramThrow(createAggregateErrorValue(runner, reasons, "All promises were rejected"))),
|
||||
Effect.fail(new Throw(createAggregateErrorValue(ctx, reasons, "All promises were rejected"))),
|
||||
),
|
||||
Effect.catch((error) =>
|
||||
error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
|
||||
@@ -237,41 +221,34 @@ const invokePromiseMethod = <R>(
|
||||
}
|
||||
|
||||
const instanceMethod = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
ctx: Interpreter<R>,
|
||||
name: "then" | "catch" | "finally",
|
||||
thisValue: unknown,
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<ProgramPromise, unknown, R> => {
|
||||
): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
const method = `Promise.prototype.${name}`
|
||||
const promise = receiver(ProgramPromise, thisValue, method)
|
||||
promises.markObserved(promise)
|
||||
const promise = receiver(PromiseObj, thisValue, method)
|
||||
ctx.pending.markObserved(promise)
|
||||
if (name === "finally") {
|
||||
return chainFinally(runner, promises, promise, reactionHandler(args[0], method), method)
|
||||
return chainFinally(ctx, promise, reactionHandler(args[0], method), method)
|
||||
}
|
||||
const onFulfilled = name === "then" ? reactionHandler(args[0], method) : undefined
|
||||
const onRejected = reactionHandler(name === "then" ? args[1] : args[0], method)
|
||||
return chainReaction(runner, promises, promise, onFulfilled, onRejected, method)
|
||||
return chainReaction(ctx, promise, onFulfilled, onRejected, method)
|
||||
}
|
||||
|
||||
const constructPromise = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
executor: unknown,
|
||||
): Effect.Effect<ProgramPromise, unknown, R> => {
|
||||
if (!(executor instanceof ProgramFunction)) {
|
||||
const constructPromise = <R>(ctx: Interpreter<R>, executor: unknown): Effect.Effect<PromiseObj, unknown, R> => {
|
||||
if (!(executor instanceof Fn)) {
|
||||
throw typeError("new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).")
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||
const promise = yield* promises.createWithSelf((self) =>
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, self)),
|
||||
const promise = yield* ctx.pending.createWithSelf((self) =>
|
||||
Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(ctx, value, self)),
|
||||
)
|
||||
const resolve = capability(runner, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)))
|
||||
const reject = capability(runner, "reject", (value) =>
|
||||
Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value))),
|
||||
)
|
||||
const executed = yield* Effect.exit(runner.invokeCallable(executor, undefined, [resolve, reject]))
|
||||
const resolve = capability(ctx, "resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value)))
|
||||
const reject = capability(ctx, "reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new Throw(value))))
|
||||
const executed = yield* Effect.exit(ctx.call(executor, undefined, [resolve, reject]))
|
||||
if (!Exit.isSuccess(executed)) {
|
||||
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
|
||||
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
|
||||
@@ -300,87 +277,85 @@ const reactionHandler = (value: unknown, method: string): Callable | undefined =
|
||||
|
||||
// Teardown bypasses handlers; settled reactions yield once so handlers never run inline.
|
||||
const reactionExit = <R>(
|
||||
promises: PromiseRuntime<R>,
|
||||
source: ProgramPromise,
|
||||
ctx: Interpreter<R>,
|
||||
source: PromiseObj,
|
||||
): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* promises.await(source)
|
||||
const exit = yield* ctx.pending.await(source)
|
||||
if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
|
||||
yield* Effect.yieldNow
|
||||
return exit
|
||||
})
|
||||
|
||||
const chainReaction = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
source: ProgramPromise,
|
||||
ctx: Interpreter<R>,
|
||||
source: PromiseObj,
|
||||
onFulfilled: Callable | undefined,
|
||||
onRejected: Callable | undefined,
|
||||
method: string,
|
||||
): Effect.Effect<ProgramPromise, never, R> => {
|
||||
return promises.createWithSelf((self) =>
|
||||
): Effect.Effect<PromiseObj, never, R> => {
|
||||
return ctx.pending.createWithSelf((self) =>
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* reactionExit(promises, source)
|
||||
const exit = yield* reactionExit(ctx, source)
|
||||
const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
|
||||
if (handler === undefined) return yield* exit
|
||||
const input = Exit.isSuccess(exit) ? exit.value : materialize(runner, Cause.squash(exit.cause))
|
||||
const result = yield* applyCollectionCallback(runner, handler, method)([input])
|
||||
return yield* resolvePromiseValue(runner, result, self)
|
||||
const input = Exit.isSuccess(exit) ? exit.value : materialize(ctx, Cause.squash(exit.cause))
|
||||
const result = yield* applyCollectionCallback(ctx, handler, method)([input])
|
||||
return yield* resolvePromiseValue(ctx, result, self)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const chainFinally = <R>(
|
||||
runner: Runner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
source: ProgramPromise,
|
||||
ctx: Interpreter<R>,
|
||||
source: PromiseObj,
|
||||
cleanup: Callable | undefined,
|
||||
method: string,
|
||||
): Effect.Effect<ProgramPromise, never, R> =>
|
||||
promises.create(
|
||||
): Effect.Effect<PromiseObj, never, R> =>
|
||||
ctx.pending.create(
|
||||
Effect.gen(function* () {
|
||||
const exit = yield* reactionExit(promises, source)
|
||||
const exit = yield* reactionExit(ctx, source)
|
||||
if (cleanup !== undefined) {
|
||||
const result = yield* applyCollectionCallback(runner, cleanup, method)([])
|
||||
const intermediate = yield* promises.create(
|
||||
const result = yield* applyCollectionCallback(ctx, cleanup, method)([])
|
||||
const intermediate = yield* ctx.pending.create(
|
||||
Effect.gen(function* () {
|
||||
yield* runner.settlePromise(yield* resolvePromise(runner, promises, result))
|
||||
yield* ctx.await(yield* resolvePromise(ctx, result))
|
||||
return yield* exit
|
||||
}),
|
||||
)
|
||||
return yield* runner.settlePromise(intermediate)
|
||||
return yield* ctx.await(intermediate)
|
||||
}
|
||||
return yield* exit
|
||||
}),
|
||||
)
|
||||
|
||||
export const promiseGlobal = <R>(runner: Runner<R>, promises: PromiseRuntime<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.Promise
|
||||
const promise = constructor<R>(protos, proto, {
|
||||
export const promiseGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Promise
|
||||
const promise = constructor<R>(builtins, proto, {
|
||||
name: "Promise",
|
||||
length: 1,
|
||||
call: requiresNew("Promise"),
|
||||
construct: (args) => constructPromise(runner, promises, args[0]),
|
||||
construct: (args) => constructPromise(ctx, args[0]),
|
||||
})
|
||||
// Combinators are not callbacks: `[p].map(Promise.resolve)` must ask for an arrow function.
|
||||
for (const name of promiseStatics) {
|
||||
define(
|
||||
promise,
|
||||
name,
|
||||
native<R>(protos, {
|
||||
native<R>(builtins, {
|
||||
name,
|
||||
length: 1,
|
||||
call: (_, args) => invokePromiseMethod(runner, promises, name, args),
|
||||
call: (_, args) => invokePromiseMethod(ctx, name, args),
|
||||
callback: false,
|
||||
}),
|
||||
hidden,
|
||||
)
|
||||
}
|
||||
methods(protos, proto, [
|
||||
["then", 2, (thisValue, args) => instanceMethod(runner, promises, "then", thisValue, args)],
|
||||
["catch", 1, (thisValue, args) => instanceMethod(runner, promises, "catch", thisValue, args)],
|
||||
["finally", 1, (thisValue, args) => instanceMethod(runner, promises, "finally", thisValue, args)],
|
||||
methods(builtins, proto, [
|
||||
["then", 2, (thisValue, args) => instanceMethod(ctx, "then", thisValue, args)],
|
||||
["catch", 1, (thisValue, args) => instanceMethod(ctx, "catch", thisValue, args)],
|
||||
["finally", 1, (thisValue, args) => instanceMethod(ctx, "finally", thisValue, args)],
|
||||
])
|
||||
return promise
|
||||
}
|
||||
|
||||
@@ -5,30 +5,31 @@ import {
|
||||
getOwn,
|
||||
isWrapper,
|
||||
ownKeys,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramGenerator,
|
||||
ProgramHandle,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramRegExp,
|
||||
ProgramSet,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
GeneratorObj,
|
||||
Handle,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "./objects.js"
|
||||
|
||||
/** Values that cannot cross the data boundary. */
|
||||
export const isRuntimeReference = (value: unknown): boolean =>
|
||||
value instanceof Callable ||
|
||||
value instanceof ProgramGenerator ||
|
||||
value instanceof ProgramHandle ||
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof Handle ||
|
||||
value instanceof ToolReference ||
|
||||
value instanceof ProgramPromise ||
|
||||
value instanceof PromiseObj ||
|
||||
isWrapper(value)
|
||||
|
||||
function* childValues(value: object): Generator {
|
||||
if (!(value instanceof ProgramObject)) return
|
||||
if (!(value instanceof Obj)) return
|
||||
for (const key of ownKeys(value)) yield getOwn(value, key)
|
||||
}
|
||||
|
||||
@@ -77,17 +78,18 @@ export const rejectCircularInsertion = (
|
||||
|
||||
export const describeValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) return String(value)
|
||||
if (value instanceof ProgramArray) return "an array"
|
||||
if (value instanceof ProgramPromise) return "an un-awaited Promise"
|
||||
if (value instanceof Arr) return "an array"
|
||||
if (value instanceof PromiseObj) return "an un-awaited Promise"
|
||||
if (value instanceof ToolReference) return "a tool reference"
|
||||
if (value instanceof ProgramDate) return "a Date"
|
||||
if (value instanceof ProgramRegExp) return "a RegExp"
|
||||
if (value instanceof ProgramMap) return "a Map"
|
||||
if (value instanceof ProgramSet) return "a Set"
|
||||
if (value instanceof ProgramURL) return "a URL"
|
||||
if (value instanceof ProgramURLSearchParams) return "a URLSearchParams"
|
||||
if (value instanceof ProgramGenerator) return "a generator"
|
||||
if (value instanceof ProgramHandle) return `a ${value.instance.constructor.name}`
|
||||
if (value instanceof DateObj) return "a Date"
|
||||
if (value instanceof RegExpObj) return "a RegExp"
|
||||
if (value instanceof MapObj) return "a Map"
|
||||
if (value instanceof SetObj) return "a Set"
|
||||
if (value instanceof URLObj) return "a URL"
|
||||
if (value instanceof URLSearchParamsObj) return "a URLSearchParams"
|
||||
if (value instanceof Bytes) return "a Uint8Array"
|
||||
if (value instanceof GeneratorObj) return "a generator"
|
||||
if (value instanceof Handle) return `a ${value.instance.constructor.name}`
|
||||
if (isRuntimeReference(value)) return "a function"
|
||||
if (typeof value === "object") return "a data object"
|
||||
return `a ${typeof value}`
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Effect } from "effect"
|
||||
import { HttpClient } from "effect/unstable/http"
|
||||
import type { Tool, JsonSchema } from "../tool.js"
|
||||
|
||||
/** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */
|
||||
/** A parsed OpenAPI 3.x document. YAML must be parsed by the ctx. */
|
||||
export type Document = Record<string, unknown>
|
||||
|
||||
/** The operation identity handed to auth resolution and errors. */
|
||||
|
||||
@@ -2,14 +2,15 @@ import { Effect } from "effect"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength, MAX_ARRAY_LENGTH } from "../interpreter/limits.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { get, ProgramArray, ProgramGenerator, ProgramObject } from "../interpreter/objects.js"
|
||||
import { get, Arr, GeneratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { compareText } from "../tool-runtime.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const arrayLikeSource = (source: unknown): { readonly length: number; readonly source: ProgramObject } => {
|
||||
if (source instanceof ProgramObject && typeof get(source, "length") === "number") {
|
||||
const arrayLikeSource = (source: unknown): { readonly length: number; readonly source: Obj } => {
|
||||
if (source instanceof Obj && typeof get(source, "length") === "number") {
|
||||
const length = get(source, "length") as number
|
||||
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
|
||||
checkArrayLength(normalized)
|
||||
@@ -20,15 +21,15 @@ const arrayLikeSource = (source: unknown): { readonly length: number; readonly s
|
||||
)
|
||||
}
|
||||
|
||||
const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const arrayFrom = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const source = args[0]
|
||||
const proto = runner.prototypes.Array
|
||||
const proto = ctx.builtins.Array
|
||||
const apply =
|
||||
args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from")
|
||||
args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(ctx, args[1], "Array.from")
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source)
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
if (cursor === undefined) {
|
||||
if (source instanceof ProgramGenerator) {
|
||||
if (source instanceof GeneratorObj) {
|
||||
throw typeError("Array.from expects a synchronous iterable or array-like value.")
|
||||
}
|
||||
const arrayLike = arrayLikeSource(source)
|
||||
@@ -37,13 +38,13 @@ const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<un
|
||||
const item = get(arrayLike.source, index)
|
||||
values.push(apply === undefined ? item : yield* apply([item, index]))
|
||||
}
|
||||
return new ProgramArray(proto, values)
|
||||
return new Arr(proto, values)
|
||||
}
|
||||
const values: Array<unknown> = []
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return new ProgramArray(proto, values)
|
||||
if (step.done) return new Arr(proto, values)
|
||||
values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
|
||||
index += 1
|
||||
}
|
||||
@@ -51,7 +52,7 @@ const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<un
|
||||
}
|
||||
|
||||
export const sortArray = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
target: Array<unknown>,
|
||||
comparator: unknown,
|
||||
name: string,
|
||||
@@ -59,7 +60,7 @@ export const sortArray = <R>(
|
||||
if (comparator === undefined) {
|
||||
return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b))))
|
||||
}
|
||||
const apply = applyCollectionCallback(runner, comparator, name)
|
||||
const apply = applyCollectionCallback(ctx, comparator, name)
|
||||
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
|
||||
if (items.length <= 1) return Effect.succeed(items)
|
||||
const midpoint = Math.floor(items.length / 2)
|
||||
@@ -84,31 +85,31 @@ export const sortArray = <R>(
|
||||
}
|
||||
|
||||
// Array constructs identically with or without new, like JS.
|
||||
export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.Array
|
||||
const wrap = (items: Array<unknown>) => new ProgramArray(proto, items)
|
||||
const construct = (args: Array<unknown>, into: ProgramObject): ProgramArray => {
|
||||
if (args.length !== 1) return new ProgramArray(into, [...args])
|
||||
export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Array
|
||||
const wrap = (items: Array<unknown>) => new Arr(proto, items)
|
||||
const construct = (args: Array<unknown>, into: Obj): Arr => {
|
||||
if (args.length !== 1) return new Arr(into, [...args])
|
||||
const first = args[0]
|
||||
if (typeof first !== "number") return new ProgramArray(into, [first])
|
||||
if (typeof first !== "number") return new Arr(into, [first])
|
||||
if (!Number.isInteger(first) || first < 0 || first > MAX_ARRAY_LENGTH) throw rangeError("Invalid array length.")
|
||||
// Sparse like JS: Array(3) has holes, and combinator loops already skip them.
|
||||
return new ProgramArray(into, new Array(first))
|
||||
return new Arr(into, new Array(first))
|
||||
}
|
||||
const array = constructor<R>(protos, proto, {
|
||||
const array = constructor<R>(builtins, proto, {
|
||||
name: "Array",
|
||||
length: 1,
|
||||
call: (_, args) => Effect.sync(() => construct(args, proto)),
|
||||
construct: (args, newTarget) => Effect.sync(() => construct(args, prototypeFrom(newTarget, proto))),
|
||||
})
|
||||
methods(protos, array, [
|
||||
["isArray", 1, (_, args) => args[0] instanceof ProgramArray],
|
||||
methods(builtins, array, [
|
||||
["isArray", 1, (_, args) => args[0] instanceof Arr],
|
||||
["of", 0, (_, args) => wrap([...args])],
|
||||
["from", 1, (_, args) => arrayFrom(runner, args)],
|
||||
["from", 1, (_, args) => arrayFrom(ctx, args)],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramArray, thisValue, `Array.prototype.${name}`)
|
||||
const self = (thisValue: unknown, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") {
|
||||
@@ -122,7 +123,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
length: number,
|
||||
body: (
|
||||
target: Array<unknown>,
|
||||
receiver: ProgramArray,
|
||||
receiver: Arr,
|
||||
apply: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
args: Array<unknown>,
|
||||
) => Effect.Effect<unknown, unknown, R>,
|
||||
@@ -131,11 +132,11 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
length,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, name)
|
||||
return body(target.items, target, applyCollectionCallback(runner, args[0], `Array.${name}`), args)
|
||||
return body(target.items, target, applyCollectionCallback(ctx, args[0], `Array.${name}`), args)
|
||||
},
|
||||
]
|
||||
|
||||
methods(protos, proto, [
|
||||
methods(builtins, proto, [
|
||||
[
|
||||
"join",
|
||||
1,
|
||||
@@ -203,7 +204,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const joined = self(thisValue, "concat").items.concat(
|
||||
...args.map((item) => (item instanceof ProgramArray ? item.items : item)),
|
||||
...args.map((item) => (item instanceof Arr ? item.items : item)),
|
||||
)
|
||||
checkArrayLength(joined.length)
|
||||
return wrap(joined)
|
||||
@@ -214,7 +215,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
0,
|
||||
(thisValue, args) => {
|
||||
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
|
||||
items.flatMap((item) => (item instanceof ProgramArray && depth > 0 ? flatten(item.items, depth - 1) : [item]))
|
||||
items.flatMap((item) => (item instanceof Arr && depth > 0 ? flatten(item.items, depth - 1) : [item]))
|
||||
const flattened = flatten(self(thisValue, "flat").items, optNumber("flat", args[0], "depth") ?? 1)
|
||||
checkArrayLength(flattened.length)
|
||||
return wrap(flattened)
|
||||
@@ -238,7 +239,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
const length = items.length
|
||||
const holeCount = Array.from({ length }, (_, index) => Object.hasOwn(items, index)).filter((o) => !o).length
|
||||
const itemCount = length - holeCount
|
||||
return Effect.map(sortArray(runner, items, args[0], "Array.sort"), (sorted) => {
|
||||
return Effect.map(sortArray(ctx, items, args[0], "Array.sort"), (sorted) => {
|
||||
sorted.slice(0, itemCount).forEach((item, index) => {
|
||||
items[index] = item
|
||||
})
|
||||
@@ -253,7 +254,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
"toSorted",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
Effect.map(sortArray(runner, self(thisValue, "toSorted").items, args[0], "Array.toSorted"), wrap),
|
||||
Effect.map(sortArray(ctx, self(thisValue, "toSorted").items, args[0], "Array.toSorted"), wrap),
|
||||
],
|
||||
["toReversed", 0, (thisValue) => wrap([...self(thisValue, "toReversed").items].reverse())],
|
||||
[
|
||||
@@ -366,7 +367,7 @@ export const arrayGlobal = <R>(runner: Runner<R>) => {
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const mapped = yield* apply([target[index], index, receiver])
|
||||
if (mapped instanceof ProgramArray) values.push(...mapped.items)
|
||||
if (mapped instanceof Arr) values.push(...mapped.items)
|
||||
else values.push(mapped)
|
||||
}
|
||||
return wrap(values)
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Effect } from "effect"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { rangeError, syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { defineAccessor, get, Arr, Bytes, Obj } from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
/** The bytes a Uint8Array, array, or other iterable of numbers describes; the host array clamps each value. */
|
||||
const collectBytes = <R>(ctx: Interpreter<R>, source: unknown, name: string): Effect.Effect<Uint8Array, unknown, R> => {
|
||||
if (source instanceof Bytes) return Effect.succeed(new Uint8Array(source.bytes))
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
if (cursor === undefined) {
|
||||
throw typeError(
|
||||
`${name} expects a Uint8Array, an array, or an iterable of numbers, received ${describeValue(source)}.`,
|
||||
)
|
||||
}
|
||||
const values: Array<number> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return Uint8Array.from(values)
|
||||
values.push(coerceToNumber(step.value))
|
||||
checkArrayLength(values.length)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const constructBytes = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
|
||||
const source = args[0]
|
||||
if (source !== null && typeof source === "object") {
|
||||
return Effect.map(collectBytes(ctx, source, "new Uint8Array(...)"), (bytes) => new Bytes(proto, bytes))
|
||||
}
|
||||
const length = source === undefined ? 0 : coerceToNumber(source)
|
||||
if (!Number.isInteger(length) || length < 0) throw rangeError(`Invalid typed array length: ${coerceToString(source)}`)
|
||||
checkArrayLength(length)
|
||||
return Effect.succeed(new Bytes(proto, new Uint8Array(length)))
|
||||
}
|
||||
|
||||
export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Uint8Array
|
||||
const wrap = (bytes: Uint8Array) => new Bytes(proto, bytes)
|
||||
const uint8Array = constructor<R>(builtins, proto, {
|
||||
name: "Uint8Array",
|
||||
length: 3,
|
||||
call: requiresNew("Uint8Array"),
|
||||
construct: (args, newTarget) => constructBytes(ctx, args, prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const decode = (name: string, args: Array<unknown>, from: (text: string) => Uint8Array) => {
|
||||
if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`)
|
||||
try {
|
||||
return wrap(from(args[0]))
|
||||
} catch {
|
||||
throw syntaxError(`Uint8Array.${name}: the string is not valid ${name === "fromHex" ? "hex" : "base64"}.`)
|
||||
}
|
||||
}
|
||||
methods(builtins, uint8Array, [
|
||||
["from", 1, (_, args) => Effect.map(collectBytes(ctx, args[0], "Uint8Array.from"), wrap)],
|
||||
["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))],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(Bytes, thisValue, `Uint8Array.prototype.${name}`)
|
||||
const optNumber = (name: string, value: unknown, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number") throw typeError(`Uint8Array.${name} expects ${label} to be a number.`)
|
||||
return value
|
||||
}
|
||||
const wrapAll = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
defineAccessor(proto, "length", (thisValue) => self(thisValue, "length").bytes.length)
|
||||
methods(builtins, proto, [
|
||||
["at", 1, (thisValue, args) => self(thisValue, "at").bytes.at(optNumber("at", args[0], "index") ?? 0)],
|
||||
[
|
||||
"slice",
|
||||
2,
|
||||
(thisValue, args) =>
|
||||
wrap(
|
||||
self(thisValue, "slice").bytes.slice(
|
||||
optNumber("slice", args[0], "start"),
|
||||
optNumber("slice", args[1], "end"),
|
||||
),
|
||||
),
|
||||
],
|
||||
// A view on the same bytes, as in JS: writes through one are visible through the other.
|
||||
[
|
||||
"subarray",
|
||||
2,
|
||||
(thisValue, args) =>
|
||||
wrap(
|
||||
self(thisValue, "subarray").bytes.subarray(
|
||||
optNumber("subarray", args[0], "start"),
|
||||
optNumber("subarray", args[1], "end"),
|
||||
),
|
||||
),
|
||||
],
|
||||
[
|
||||
"set",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "set")
|
||||
const offset = optNumber("set", args[1], "offset") ?? 0
|
||||
return Effect.map(collectBytes(ctx, args[0], "Uint8Array.set"), (source) => {
|
||||
if (!Number.isInteger(offset) || offset < 0 || source.length + offset > target.bytes.length) {
|
||||
throw rangeError("Uint8Array.set: the source does not fit at that offset.")
|
||||
}
|
||||
target.bytes.set(source, offset)
|
||||
return undefined
|
||||
})
|
||||
},
|
||||
],
|
||||
[
|
||||
"fill",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "fill")
|
||||
target.bytes.fill(
|
||||
coerceToNumber(args[0]),
|
||||
optNumber("fill", args[1], "start"),
|
||||
optNumber("fill", args[2], "end"),
|
||||
)
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"reverse",
|
||||
0,
|
||||
(thisValue) => {
|
||||
const target = self(thisValue, "reverse")
|
||||
target.bytes.reverse()
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"indexOf",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
self(thisValue, "indexOf").bytes.indexOf(coerceToNumber(args[0]), optNumber("indexOf", args[1], "start index")),
|
||||
],
|
||||
[
|
||||
"lastIndexOf",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "lastIndexOf").bytes
|
||||
return args[1] === undefined
|
||||
? target.lastIndexOf(coerceToNumber(args[0]))
|
||||
: target.lastIndexOf(coerceToNumber(args[0]), optNumber("lastIndexOf", args[1], "start index"))
|
||||
},
|
||||
],
|
||||
[
|
||||
"includes",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
self(thisValue, "includes").bytes.includes(
|
||||
coerceToNumber(args[0]),
|
||||
optNumber("includes", args[1], "start index"),
|
||||
),
|
||||
],
|
||||
[
|
||||
"join",
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
|
||||
throw typeError("Uint8Array.join expects zero arguments or one string separator.")
|
||||
}
|
||||
const joined = self(thisValue, "join").bytes.join(args.length === 0 ? "," : (args[0] as string))
|
||||
checkStringLength(joined.length)
|
||||
return joined
|
||||
},
|
||||
],
|
||||
["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) => wrapAll(Array.from(self(thisValue, "keys").bytes.keys()))],
|
||||
["values", 0, (thisValue) => wrapAll(Array.from(self(thisValue, "values").bytes.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
wrapAll(Array.from(self(thisValue, "entries").bytes.entries(), ([index, byte]) => wrapAll([index, byte]))),
|
||||
],
|
||||
])
|
||||
return uint8Array
|
||||
}
|
||||
|
||||
export const textEncoderGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.TextEncoder
|
||||
const encoder = new TextEncoder()
|
||||
defineAccessor(proto, "encoding", () => "utf-8")
|
||||
methods(builtins, proto, [
|
||||
["encode", 0, (_, args) => new Bytes(builtins.Uint8Array, encoder.encode(coerceToString(args[0] ?? "")))],
|
||||
])
|
||||
return constructor<R>(builtins, proto, {
|
||||
name: "TextEncoder",
|
||||
call: requiresNew("TextEncoder"),
|
||||
construct: (_, newTarget) => Effect.succeed(new Obj(prototypeFrom(newTarget, proto))),
|
||||
})
|
||||
}
|
||||
|
||||
/** A `TextDecoder` holding the host decoder its label and options configured. */
|
||||
export class TextDecoderObj extends Obj {
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly decoder: TextDecoder,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
// The WHATWG labels for UTF-8, the only encoding CodeMode decodes.
|
||||
const utf8Labels = new Set(["unicode-1-1-utf-8", "unicode11utf8", "unicode20utf8", "utf-8", "utf8", "x-unicode20utf8"])
|
||||
|
||||
export const textDecoderGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.TextDecoder
|
||||
const self = (thisValue: unknown, name: string) =>
|
||||
receiver(TextDecoderObj, thisValue, `TextDecoder.prototype.${name}`)
|
||||
defineAccessor(proto, "encoding", (thisValue) => self(thisValue, "encoding").decoder.encoding)
|
||||
defineAccessor(proto, "fatal", (thisValue) => self(thisValue, "fatal").decoder.fatal)
|
||||
defineAccessor(proto, "ignoreBOM", (thisValue) => self(thisValue, "ignoreBOM").decoder.ignoreBOM)
|
||||
methods(builtins, proto, [
|
||||
[
|
||||
"decode",
|
||||
0,
|
||||
(thisValue, args) => {
|
||||
const decoder = self(thisValue, "decode").decoder
|
||||
if (args[0] === undefined) return ""
|
||||
if (!(args[0] instanceof Bytes)) {
|
||||
throw typeError(`TextDecoder.decode expects a Uint8Array, received ${describeValue(args[0])}.`)
|
||||
}
|
||||
try {
|
||||
return decoder.decode(args[0].bytes)
|
||||
} catch {
|
||||
throw typeError(`TextDecoder.decode: the input is not valid ${decoder.encoding}.`)
|
||||
}
|
||||
},
|
||||
],
|
||||
])
|
||||
return constructor<R>(builtins, proto, {
|
||||
name: "TextDecoder",
|
||||
call: requiresNew("TextDecoder"),
|
||||
construct: (args, newTarget) =>
|
||||
Effect.sync(() => {
|
||||
const label = args[0] === undefined ? "utf-8" : coerceToString(args[0]).trim().toLowerCase()
|
||||
if (!utf8Labels.has(label)) throw rangeError(`The "${label}" encoding is not supported; only UTF-8 is.`)
|
||||
const options = args[1] instanceof Obj ? args[1] : undefined
|
||||
const flag = (name: string) => options !== undefined && Boolean(get(options, name))
|
||||
return new TextDecoderObj(
|
||||
prototypeFrom(newTarget, proto),
|
||||
new TextDecoder("utf-8", { fatal: flag("fatal"), ignoreBOM: flag("ignoreBOM") }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -8,46 +8,46 @@ import {
|
||||
getOwn,
|
||||
hidden,
|
||||
isWrapper,
|
||||
ProgramArray,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramSet,
|
||||
Arr,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
SetObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
|
||||
import {
|
||||
applyCollectionCallback,
|
||||
isSupportedCallback,
|
||||
preserveConsumerError,
|
||||
type Runner,
|
||||
toPrimitiveNumber,
|
||||
toPrimitiveString,
|
||||
} from "../interpreter/runner.js"
|
||||
} from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
const coerceGroupByPropertyKey = <R>(runner: Runner<R>, value: unknown): Effect.Effect<string, unknown, R> => {
|
||||
if (value instanceof ProgramPromise) return Effect.succeed("[object Promise]")
|
||||
const coerceGroupByPropertyKey = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<string, unknown, R> => {
|
||||
if (value instanceof PromiseObj) return Effect.succeed("[object Promise]")
|
||||
if (!isWrapper(value) && isRuntimeReference(value)) {
|
||||
throw invalidData(`Object.groupBy callback must return a data value, received ${describeValue(value)}.`)
|
||||
}
|
||||
return toPrimitiveString(runner, value)
|
||||
return toPrimitiveString(ctx, value)
|
||||
}
|
||||
|
||||
/** `Map.groupBy` and `Object.groupBy`: the same iteration, keyed into a Map or a data object. */
|
||||
export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
|
||||
fn<R>(runner.prototypes, "groupBy", 2, (_, args) => {
|
||||
const protos = runner.prototypes
|
||||
export const groupBy = <R>(ctx: Interpreter<R>, namespace: "Map" | "Object") =>
|
||||
fn<R>(ctx.builtins, "groupBy", 2, (_, args) => {
|
||||
const builtins = ctx.builtins
|
||||
const source = args[0]
|
||||
if (source === null || source === undefined) {
|
||||
throw typeError(`${namespace}.groupBy expects an iterable collection.`)
|
||||
}
|
||||
const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`)
|
||||
const apply = applyCollectionCallback(ctx, args[1], `${namespace}.groupBy`)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source)
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
if (cursor === undefined) {
|
||||
throw typeError(`${namespace}.groupBy expects an iterable collection.`)
|
||||
}
|
||||
if (namespace === "Map") {
|
||||
const result = new ProgramMap(protos.Map)
|
||||
const result = new MapObj(builtins.Map)
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -55,14 +55,14 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
|
||||
const item = step.value
|
||||
const key = yield* preserveConsumerError(cursor, apply([item, index]))
|
||||
const group = result.map.get(key)
|
||||
if (group === undefined) result.map.set(key, new ProgramArray(protos.Array, [item]))
|
||||
else (group as ProgramArray).items.push(item)
|
||||
if (group === undefined) result.map.set(key, new Arr(builtins.Array, [item]))
|
||||
else (group as Arr).items.push(item)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Object.groupBy returns a null-prototype object, so group names never collide with inherited methods.
|
||||
const result = new ProgramObject(null)
|
||||
const result = new Obj(null)
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -70,21 +70,21 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
|
||||
const item = step.value
|
||||
const key = yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value)),
|
||||
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(ctx, value)),
|
||||
)
|
||||
const group = getOwn(result, key)
|
||||
if (group === undefined) define(result, key, new ProgramArray(protos.Array, [item]))
|
||||
else (group as ProgramArray).items.push(item)
|
||||
if (group === undefined) define(result, key, new Arr(builtins.Array, [item]))
|
||||
else (group as Arr).items.push(item)
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const constructMap = <R>(runner: Runner<R>, init: unknown, proto: ProgramObject) => {
|
||||
const target = new ProgramMap(proto)
|
||||
const constructMap = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
|
||||
const target = new MapObj(proto)
|
||||
if (init === undefined || init === null) return Effect.succeed(target)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(init)
|
||||
const cursor = yield* ctx.iterate(init)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("new Map(...) expects an iterable of [key, value] pairs or no argument.")
|
||||
}
|
||||
@@ -94,7 +94,7 @@ const constructMap = <R>(runner: Runner<R>, init: unknown, proto: ProgramObject)
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (!(step.value instanceof ProgramObject)) {
|
||||
if (!(step.value instanceof Obj)) {
|
||||
throw typeError("new Map(...) expects [key, value] pairs as entry objects.")
|
||||
}
|
||||
target.map.set(getOwn(step.value, 0), getOwn(step.value, 1))
|
||||
@@ -104,11 +104,11 @@ const constructMap = <R>(runner: Runner<R>, init: unknown, proto: ProgramObject)
|
||||
})
|
||||
}
|
||||
|
||||
const constructSet = <R>(runner: Runner<R>, init: unknown, proto: ProgramObject) => {
|
||||
const target = new ProgramSet(proto)
|
||||
const constructSet = <R>(ctx: Interpreter<R>, init: unknown, proto: Obj) => {
|
||||
const target = new SetObj(proto)
|
||||
if (init === undefined || init === null) return Effect.succeed(target)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(init)
|
||||
const cursor = yield* ctx.iterate(init)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("new Set(...) expects a synchronous iterable or no argument.")
|
||||
}
|
||||
@@ -120,19 +120,19 @@ const constructSet = <R>(runner: Runner<R>, init: unknown, proto: ProgramObject)
|
||||
})
|
||||
}
|
||||
|
||||
export const mapGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.Map
|
||||
const map = constructor<R>(protos, proto, {
|
||||
export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Map
|
||||
const map = constructor<R>(builtins, proto, {
|
||||
name: "Map",
|
||||
call: requiresNew("Map"),
|
||||
construct: (args, newTarget) => constructMap(runner, args[0], prototypeFrom(newTarget, proto)),
|
||||
construct: (args, newTarget) => constructMap(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
define(map, "groupBy", groupBy(runner, "Map"), hidden)
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramMap, thisValue, `Map.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new ProgramArray(protos.Array, items)
|
||||
defineAccessor(proto, "size", (thisValue) => receiver(ProgramMap, thisValue, "Map.prototype.size").map.size)
|
||||
methods(protos, proto, [
|
||||
define(map, "groupBy", groupBy(ctx, "Map"), hidden)
|
||||
const self = (thisValue: unknown, name: string) => receiver(MapObj, thisValue, `Map.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
defineAccessor(proto, "size", (thisValue) => receiver(MapObj, thisValue, "Map.prototype.size").map.size)
|
||||
methods(builtins, proto, [
|
||||
["get", 1, (thisValue, args) => self(thisValue, "get").map.get(args[0])],
|
||||
["has", 1, (thisValue, args) => self(thisValue, "has").map.has(args[0])],
|
||||
[
|
||||
@@ -165,7 +165,7 @@ export const mapGlobal = <R>(runner: Runner<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "forEach")
|
||||
const apply = applyCollectionCallback(runner, args[0], "Map.forEach")
|
||||
const apply = applyCollectionCallback(ctx, args[0], "Map.forEach")
|
||||
return Effect.gen(function* () {
|
||||
for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
|
||||
return undefined
|
||||
@@ -183,29 +183,29 @@ type SetRecord<R> = {
|
||||
}
|
||||
|
||||
const loadSetRecord = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
source: unknown,
|
||||
name: string,
|
||||
): Effect.Effect<SetRecord<R>, unknown, R> => {
|
||||
if (source instanceof ProgramSet) {
|
||||
if (source instanceof SetObj) {
|
||||
return Effect.succeed({
|
||||
size: source.set.size,
|
||||
has: (item: unknown) => Effect.succeed(source.set.has(item)),
|
||||
keys: () => Effect.succeed(source.set.values()),
|
||||
})
|
||||
}
|
||||
if (source instanceof ProgramMap) {
|
||||
if (source instanceof MapObj) {
|
||||
return Effect.succeed({
|
||||
size: source.map.size,
|
||||
has: (item: unknown) => Effect.succeed(source.map.has(item)),
|
||||
keys: () => Effect.succeed(source.map.keys()),
|
||||
})
|
||||
}
|
||||
if (!(source instanceof ProgramObject)) {
|
||||
if (!(source instanceof Obj)) {
|
||||
throw typeError(`Set.${name} expects a Set-like object.`)
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const size = yield* toPrimitiveNumber(runner, get(source, "size"))
|
||||
const size = yield* toPrimitiveNumber(ctx, get(source, "size"))
|
||||
if (Number.isNaN(size)) {
|
||||
throw typeError(`Set.${name} received a Set-like object with an invalid size.`)
|
||||
}
|
||||
@@ -216,10 +216,10 @@ const loadSetRecord = <R>(
|
||||
}
|
||||
return {
|
||||
size: Math.max(Math.trunc(size), 0),
|
||||
has: (item: unknown) => Effect.map(runner.invokeCallable(has, source, [item]), Boolean),
|
||||
has: (item: unknown) => Effect.map(ctx.call(has, source, [item]), Boolean),
|
||||
keys: () =>
|
||||
Effect.flatMap(runner.invokeCallable(keys, source, []), (result) => {
|
||||
if (result instanceof ProgramArray) return Effect.succeed(result.items)
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result) => {
|
||||
if (result instanceof Arr) return Effect.succeed(result.items)
|
||||
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
|
||||
}),
|
||||
}
|
||||
@@ -227,15 +227,15 @@ const loadSetRecord = <R>(
|
||||
}
|
||||
|
||||
const setOperation = <R>(
|
||||
runner: Runner<R>,
|
||||
target: ProgramSet,
|
||||
ctx: Interpreter<R>,
|
||||
target: SetObj,
|
||||
name: string,
|
||||
source: unknown,
|
||||
): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const other = yield* loadSetRecord(runner, source, name)
|
||||
const other = yield* loadSetRecord(ctx, source, name)
|
||||
const copy = () => {
|
||||
const result = new ProgramSet(runner.prototypes.Set)
|
||||
const result = new SetObj(ctx.builtins.Set)
|
||||
for (const item of target.set.values()) result.set.add(item)
|
||||
return result
|
||||
}
|
||||
@@ -245,7 +245,7 @@ const setOperation = <R>(
|
||||
return result
|
||||
}
|
||||
if (name === "intersection") {
|
||||
const result = new ProgramSet(runner.prototypes.Set)
|
||||
const result = new SetObj(ctx.builtins.Set)
|
||||
if (target.set.size <= other.size) {
|
||||
for (const item of target.set.values()) {
|
||||
if (yield* other.has(item)) result.set.add(item)
|
||||
@@ -302,23 +302,23 @@ const setOperation = <R>(
|
||||
return true
|
||||
})
|
||||
|
||||
export const setGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.Set
|
||||
const set = constructor<R>(protos, proto, {
|
||||
export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Set
|
||||
const set = constructor<R>(builtins, proto, {
|
||||
name: "Set",
|
||||
call: requiresNew("Set"),
|
||||
construct: (args, newTarget) => constructSet(runner, args[0], prototypeFrom(newTarget, proto)),
|
||||
construct: (args, newTarget) => constructSet(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramSet, thisValue, `Set.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new ProgramArray(protos.Array, items)
|
||||
const self = (thisValue: unknown, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const operation = (name: string): Method => [
|
||||
name,
|
||||
1,
|
||||
(thisValue, args) => setOperation(runner, self(thisValue, name), name, args[0]),
|
||||
(thisValue, args) => setOperation(ctx, self(thisValue, name), name, args[0]),
|
||||
]
|
||||
defineAccessor(proto, "size", (thisValue) => receiver(ProgramSet, thisValue, "Set.prototype.size").set.size)
|
||||
methods(protos, proto, [
|
||||
defineAccessor(proto, "size", (thisValue) => receiver(SetObj, thisValue, "Set.prototype.size").set.size)
|
||||
methods(builtins, proto, [
|
||||
["has", 1, (thisValue, args) => self(thisValue, "has").set.has(args[0])],
|
||||
[
|
||||
"add",
|
||||
@@ -350,7 +350,7 @@ export const setGlobal = <R>(runner: Runner<R>) => {
|
||||
1,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "forEach")
|
||||
const apply = applyCollectionCallback(runner, args[0], "Set.forEach")
|
||||
const apply = applyCollectionCallback(ctx, args[0], "Set.forEach")
|
||||
return Effect.gen(function* () {
|
||||
for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
|
||||
return undefined
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import { toData, toProgram } from "../data.js"
|
||||
import type { Prototypes } from "../interpreter/intrinsics.js"
|
||||
import type { Builtins } from "../interpreter/intrinsics.js"
|
||||
import { type Method, methods } from "../interpreter/native.js"
|
||||
import {
|
||||
entries,
|
||||
get,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramMap,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramRegExp,
|
||||
ProgramSet,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js"
|
||||
import type { Runner } from "../interpreter/runner.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
const consoleMethods = ["log", "info", "debug", "warn", "error", "dir", "table"]
|
||||
|
||||
/** Captured console: every method appends one formatted line to `logs`. */
|
||||
export const consoleGlobal = <R>(runner: Runner<R>, logs: Array<string>) => {
|
||||
const protos = runner.prototypes
|
||||
const console = new ProgramObject(protos.Object)
|
||||
export const consoleGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const console = new Obj(builtins.Object)
|
||||
methods(
|
||||
protos,
|
||||
builtins,
|
||||
console,
|
||||
consoleMethods.map(
|
||||
(name): Method => [
|
||||
name,
|
||||
0,
|
||||
(_, args) => {
|
||||
logs.push(formatConsoleMessage(protos, name, args))
|
||||
ctx.logs.push(formatConsoleMessage(builtins, name, args))
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
@@ -43,9 +44,9 @@ export const consoleGlobal = <R>(runner: Runner<R>, logs: Array<string>) => {
|
||||
|
||||
const MAX_CONSOLE_DEPTH = 32
|
||||
|
||||
const formatConsoleMessage = (protos: Prototypes, name: string, args: Array<unknown>): string => {
|
||||
const formatConsoleMessage = (builtins: Builtins, name: string, args: Array<unknown>): string => {
|
||||
if (name === "dir") return args.length === 0 ? "undefined" : formatConsoleArgument(args[0])
|
||||
if (name === "table") return formatConsoleTable(protos, args[0], args[1])
|
||||
if (name === "table") return formatConsoleTable(builtins, args[0], args[1])
|
||||
const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
|
||||
return `${prefix}${args.map((arg) => formatConsoleArgument(arg)).join(" ")}`
|
||||
}
|
||||
@@ -61,14 +62,15 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
if (typeof value === "string") return JSON.stringify(value)
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
if (typeof value !== "object") return String(value)
|
||||
if (value instanceof ProgramPromise) return "[Promise (await it to get its value)]"
|
||||
if (value instanceof ProgramDate) return coerceToString(value)
|
||||
if (value instanceof ProgramRegExp) return coerceToString(value)
|
||||
if (value instanceof ProgramURL) return coerceToString(value)
|
||||
if (value instanceof ProgramURLSearchParams) return coerceToString(value)
|
||||
if (value instanceof PromiseObj) return "[Promise (await it to get its value)]"
|
||||
if (value instanceof DateObj) return coerceToString(value)
|
||||
if (value instanceof RegExpObj) return coerceToString(value)
|
||||
if (value instanceof URLObj) return coerceToString(value)
|
||||
if (value instanceof URLSearchParamsObj) return coerceToString(value)
|
||||
if (value instanceof Bytes) return `Uint8Array(${value.bytes.length}) [${value.bytes.join(",")}]`
|
||||
if (depth > MAX_CONSOLE_DEPTH) return "..."
|
||||
if (seen.has(value)) return "[Circular]"
|
||||
if (value instanceof ProgramMap) {
|
||||
if (value instanceof MapObj) {
|
||||
seen.add(value)
|
||||
try {
|
||||
const items = Array.from(value.map.entries(), ([key, item]) => `[${formatItems([key, item], seen, depth + 1)}]`)
|
||||
@@ -77,7 +79,7 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
seen.delete(value)
|
||||
}
|
||||
}
|
||||
if (value instanceof ProgramSet) {
|
||||
if (value instanceof SetObj) {
|
||||
seen.add(value)
|
||||
try {
|
||||
return `Set(${value.set.size}) [${formatItems([...value.set.values()], seen, depth + 1)}]`
|
||||
@@ -88,8 +90,8 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
if (isRuntimeReference(value)) return "[opaque reference]"
|
||||
seen.add(value)
|
||||
try {
|
||||
if (value instanceof ProgramArray) return `[${formatItems(value.items, seen, depth + 1)}]`
|
||||
if (!(value instanceof ProgramObject)) return "[object Object]"
|
||||
if (value instanceof Arr) return `[${formatItems(value.items, seen, depth + 1)}]`
|
||||
if (!(value instanceof Obj)) return "[object Object]"
|
||||
return `{${entries(value)
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
|
||||
.join(",")}}`
|
||||
@@ -101,10 +103,10 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
const formatItems = (items: Array<unknown>, seen: Set<object>, depth: number): string =>
|
||||
items.map((item) => formatConsoleValue(item, seen, depth)).join(",")
|
||||
|
||||
const formatConsoleTable = (protos: Prototypes, value: unknown, columnsArgument: unknown): string => {
|
||||
const formatConsoleTable = (builtins: Builtins, value: unknown, columnsArgument: unknown): string => {
|
||||
if (value === undefined) return "undefined"
|
||||
if (containsOpaqueReference(value)) return "[opaque reference]"
|
||||
const data = toProgram(protos, value, "console.table argument")
|
||||
const data = toProgram(builtins, value, "console.table argument")
|
||||
const columns = consoleTableColumns(columnsArgument)
|
||||
const rows = consoleTableRows(data, columns)
|
||||
const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values))))
|
||||
@@ -126,17 +128,17 @@ const consoleTableRows = (
|
||||
data: unknown,
|
||||
columns: ReadonlyArray<string> | undefined,
|
||||
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
|
||||
if (data instanceof ProgramArray) {
|
||||
if (data instanceof Arr) {
|
||||
return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
|
||||
}
|
||||
if (data instanceof ProgramObject) {
|
||||
if (data instanceof Obj) {
|
||||
return entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
|
||||
}
|
||||
return [{ index: "0", values: { Value: data } }]
|
||||
}
|
||||
|
||||
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
|
||||
if (value instanceof ProgramObject && !(value instanceof ProgramArray)) {
|
||||
if (value instanceof Obj && !(value instanceof Arr)) {
|
||||
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, get(value, column)]))
|
||||
return Object.fromEntries(entries(value))
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { rangeError } from "../interpreter/model.js"
|
||||
import { ProgramDate, ProgramObject } from "../interpreter/objects.js"
|
||||
import { type Runner, toPrimitive, toPrimitiveNumber } from "../interpreter/runner.js"
|
||||
import { DateObj, Obj } from "../interpreter/objects.js"
|
||||
import { toPrimitive, toPrimitiveNumber } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const constructDate = <R>(runner: Runner<R>, args: Array<unknown>, proto: ProgramObject) => {
|
||||
if (args.length === 0) return Effect.succeed(new ProgramDate(proto, Date.now()))
|
||||
const constructDate = <R>(ctx: Interpreter<R>, args: Array<unknown>, proto: Obj) => {
|
||||
if (args.length === 0) return Effect.succeed(new DateObj(proto, Date.now()))
|
||||
if (args.length === 1) {
|
||||
const arg = args[0]
|
||||
if (arg instanceof ProgramDate) return Effect.succeed(new ProgramDate(proto, arg.time))
|
||||
return Effect.map(toPrimitive(runner, arg, "default"), (value) =>
|
||||
if (arg instanceof DateObj) return Effect.succeed(new DateObj(proto, arg.time))
|
||||
return Effect.map(toPrimitive(ctx, arg, "default"), (value) =>
|
||||
typeof value === "string"
|
||||
? new ProgramDate(proto, Date.parse(value))
|
||||
: new ProgramDate(proto, new Date(coerceToNumber(value)).getTime()),
|
||||
? new DateObj(proto, Date.parse(value))
|
||||
: new DateObj(proto, new Date(coerceToNumber(value)).getTime()),
|
||||
)
|
||||
}
|
||||
const parts = args.map((arg) => coerceToNumber(arg))
|
||||
return Effect.succeed(new ProgramDate(proto, new Date(...(parts as [number, number])).getTime()))
|
||||
return Effect.succeed(new DateObj(proto, new Date(...(parts as [number, number])).getTime()))
|
||||
}
|
||||
|
||||
type Getter = keyof {
|
||||
@@ -66,28 +67,28 @@ const setters: ReadonlyArray<readonly [Setter, number]> = [
|
||||
["setUTCFullYear", 3],
|
||||
]
|
||||
|
||||
export const dateGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.Date
|
||||
const date = constructor<R>(protos, proto, {
|
||||
export const dateGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.Date
|
||||
const date = constructor<R>(builtins, proto, {
|
||||
name: "Date",
|
||||
length: 7,
|
||||
// ISO instead of the host's locale string: date strings are deterministic and must not leak the host timezone.
|
||||
call: () => Effect.sync(() => new Date().toISOString()),
|
||||
construct: (args, newTarget) => constructDate(runner, args, prototypeFrom(newTarget, proto)),
|
||||
construct: (args, newTarget) => constructDate(ctx, args, prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
methods(protos, date, [
|
||||
methods(builtins, date, [
|
||||
["now", 0, () => Date.now()],
|
||||
["parse", 1, (_, args) => Date.parse(coerceToString(args[0]))],
|
||||
["UTC", 7, (_, args) => Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramDate, thisValue, `Date.prototype.${name}`)
|
||||
const iso = (value: ProgramDate) => {
|
||||
const self = (thisValue: unknown, name: string) => receiver(DateObj, thisValue, `Date.prototype.${name}`)
|
||||
const iso = (value: DateObj) => {
|
||||
if (!Number.isFinite(value.time)) throw rangeError("Invalid time value.")
|
||||
return new Date(value.time).toISOString()
|
||||
}
|
||||
methods(protos, proto, [
|
||||
methods(builtins, proto, [
|
||||
["getTime", 0, (thisValue) => self(thisValue, "getTime").time],
|
||||
["valueOf", 0, (thisValue) => self(thisValue, "valueOf").time],
|
||||
["toISOString", 0, (thisValue) => iso(self(thisValue, "toISOString"))],
|
||||
@@ -114,7 +115,7 @@ export const dateGlobal = <R>(runner: Runner<R>) => {
|
||||
// Native setters read the current time before argument coercion, whose callbacks may mutate the Date.
|
||||
const hosted = new Date(target.time)
|
||||
return Effect.map(
|
||||
Effect.forEach(args.slice(0, length), (arg) => toPrimitiveNumber(runner, arg), {
|
||||
Effect.forEach(args.slice(0, length), (arg) => toPrimitiveNumber(ctx, arg), {
|
||||
concurrency: 1,
|
||||
}),
|
||||
(values) => {
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
import { Effect } from "effect"
|
||||
import { methods } from "../interpreter/native.js"
|
||||
import { applyCollectionCallback, type Runner } from "../interpreter/runner.js"
|
||||
import { applyCollectionCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { checkStringLength } from "../interpreter/limits.js"
|
||||
import { syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { fromData, toData, toProgram } from "../data.js"
|
||||
import { Callable, get, keys, ProgramArray, ProgramObject, record, remove, set } from "../interpreter/objects.js"
|
||||
import { Callable, get, keys, Arr, Obj, record, remove, set } from "../interpreter/objects.js"
|
||||
|
||||
export const jsonGlobal = <R>(runner: Runner<R>) => {
|
||||
const json = new ProgramObject(runner.prototypes.Object)
|
||||
methods(runner.prototypes, json, [
|
||||
["parse", 2, (_, args) => parse(runner, args)],
|
||||
["stringify", 3, (_, args) => stringify(runner, args)],
|
||||
export const jsonGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const json = new Obj(ctx.builtins.Object)
|
||||
methods(ctx.builtins, json, [
|
||||
["parse", 2, (_, args) => parse(ctx, args)],
|
||||
["stringify", 3, (_, args) => stringify(ctx, args)],
|
||||
])
|
||||
return json
|
||||
}
|
||||
|
||||
const parse = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const parse = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw typeError("JSON.parse expects a string.")
|
||||
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return fromData(runner.prototypes, JSON.parse(text), "JSON.parse result")
|
||||
return fromData(ctx.builtins, JSON.parse(text), "JSON.parse result")
|
||||
} catch (error) {
|
||||
throw syntaxError(`JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
})()
|
||||
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
|
||||
|
||||
const apply = applyCollectionCallback(runner, args[1], "JSON.parse")
|
||||
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
const apply = applyCollectionCallback(ctx, args[1], "JSON.parse")
|
||||
const visit = (holder: Obj, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = get(holder, key)
|
||||
if (value instanceof ProgramObject) {
|
||||
if (value instanceof Obj) {
|
||||
for (const name of keys(value)) {
|
||||
const revived = yield* visit(value, name)
|
||||
if (revived === undefined) remove(value, name)
|
||||
@@ -42,17 +43,17 @@ const parse = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<unknow
|
||||
}
|
||||
return yield* apply([key, value])
|
||||
})
|
||||
return visit(record(runner.prototypes.Object, { "": parsed }), "")
|
||||
return visit(record(ctx.builtins.Object, { "": parsed }), "")
|
||||
}
|
||||
|
||||
const stringify = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const stringify = <R>(ctx: Interpreter<R>, args: Array<unknown>): Effect.Effect<unknown, unknown, R> => {
|
||||
const space = args[2]
|
||||
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
|
||||
const replacer = args[1]
|
||||
|
||||
if (typeofValue(replacer) !== "function") {
|
||||
const properties =
|
||||
replacer instanceof ProgramArray
|
||||
replacer instanceof Arr
|
||||
? replacer.items
|
||||
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
|
||||
.map(String)
|
||||
@@ -64,20 +65,20 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<un
|
||||
}
|
||||
|
||||
// Validate up front; the replacer walk below reads the original value.
|
||||
toProgram(runner.prototypes, args[0], "JSON.stringify value")
|
||||
const apply = applyCollectionCallback(runner, replacer, "JSON.stringify")
|
||||
toProgram(ctx.builtins, args[0], "JSON.stringify value")
|
||||
const apply = applyCollectionCallback(ctx, replacer, "JSON.stringify")
|
||||
const stack = new Set<object>()
|
||||
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
const visit = (holder: Obj, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = yield* apply([key, yield* toJSONValue(runner, get(holder, key), key)])
|
||||
const value = yield* apply([key, yield* toJSONValue(ctx, get(holder, key), key)])
|
||||
if (value === undefined || typeofValue(value) === "function") return undefined
|
||||
toProgram(runner.prototypes, value, "JSON.stringify replacer result")
|
||||
toProgram(ctx.builtins, value, "JSON.stringify replacer result")
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : null
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
||||
if (!(value instanceof ProgramObject)) return {}
|
||||
if (!(value instanceof Obj)) return {}
|
||||
if (stack.has(value)) throw typeError("Converting circular structure to JSON.")
|
||||
stack.add(value)
|
||||
if (value instanceof ProgramArray) {
|
||||
if (value instanceof Arr) {
|
||||
const result: Array<unknown> = []
|
||||
for (let index = 0; index < value.items.length; index += 1) {
|
||||
result.push((yield* visit(value, String(index))) ?? null)
|
||||
@@ -94,14 +95,14 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>): Effect.Effect<un
|
||||
return result
|
||||
})
|
||||
|
||||
return Effect.map(visit(record(runner.prototypes.Object, { "": args[0] }), ""), (value) =>
|
||||
return Effect.map(visit(record(ctx.builtins.Object, { "": args[0] }), ""), (value) =>
|
||||
JSON.stringify(value, null, indent),
|
||||
)
|
||||
}
|
||||
|
||||
// SerializeJSONProperty step 2: a callable `toJSON` decides the value, as Date and URL define.
|
||||
const toJSONValue = <R>(runner: Runner<R>, value: unknown, key: string) => {
|
||||
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
|
||||
const toJSONValue = <R>(ctx: Interpreter<R>, value: unknown, key: string) => {
|
||||
if (!(value instanceof Obj)) return Effect.succeed(value)
|
||||
const toJSON = get(value, "toJSON")
|
||||
return toJSON instanceof Callable ? runner.invokeCallable(toJSON, value, [key]) : Effect.succeed(value)
|
||||
return toJSON instanceof Callable ? ctx.call(toJSON, value, [key]) : Effect.succeed(value)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Effect } from "effect"
|
||||
import { constants, type Method, methods } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { ProgramObject } from "../interpreter/objects.js"
|
||||
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { Obj } from "../interpreter/objects.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
// Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types.
|
||||
declare global {
|
||||
@@ -40,9 +41,9 @@ const variadic = (name: string, op: (...values: Array<number>) => number): Metho
|
||||
),
|
||||
]
|
||||
|
||||
export const mathGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const math = new ProgramObject(protos.Object)
|
||||
export const mathGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const math = new Obj(builtins.Object)
|
||||
constants(math, {
|
||||
PI: Math.PI,
|
||||
E: Math.E,
|
||||
@@ -53,7 +54,7 @@ export const mathGlobal = <R>(runner: Runner<R>) => {
|
||||
SQRT2: Math.SQRT2,
|
||||
SQRT1_2: Math.SQRT1_2,
|
||||
})
|
||||
methods(protos, math, [
|
||||
methods(builtins, math, [
|
||||
["random", 0, () => Math.random()],
|
||||
variadic("max", Math.max),
|
||||
variadic("min", Math.min),
|
||||
@@ -95,7 +96,7 @@ export const mathGlobal = <R>(runner: Runner<R>) => {
|
||||
1,
|
||||
(_, args) =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(args[0])
|
||||
const cursor = yield* ctx.iterate(args[0])
|
||||
if (cursor === undefined) {
|
||||
throw typeError("Math.sumPrecise expects a synchronous iterable.")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { constructor, constants, methods } from "../interpreter/native.js"
|
||||
import { rangeError, typeError } from "../interpreter/model.js"
|
||||
import type { Runner } from "../interpreter/runner.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coercion, coerceToString } from "./value.js"
|
||||
|
||||
export const numberGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const number = constructor<R>(protos, protos.Number, {
|
||||
export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const number = constructor<R>(builtins, builtins.Number, {
|
||||
name: "Number",
|
||||
length: 1,
|
||||
call: coercion(runner, "Number").call,
|
||||
call: coercion(ctx, "Number").call,
|
||||
})
|
||||
constants(number, {
|
||||
MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER,
|
||||
@@ -20,7 +20,7 @@ export const numberGlobal = <R>(runner: Runner<R>) => {
|
||||
POSITIVE_INFINITY: Number.POSITIVE_INFINITY,
|
||||
NEGATIVE_INFINITY: Number.NEGATIVE_INFINITY,
|
||||
})
|
||||
methods(protos, number, [
|
||||
methods(builtins, number, [
|
||||
["isInteger", 1, (_, args) => Number.isInteger(args[0])],
|
||||
["isFinite", 1, (_, args) => Number.isFinite(args[0])],
|
||||
["isNaN", 1, (_, args) => Number.isNaN(args[0])],
|
||||
@@ -48,7 +48,7 @@ export const numberGlobal = <R>(runner: Runner<R>) => {
|
||||
if (typeof arg !== "number") throw typeError(`Number.${name} expects a number argument.`)
|
||||
return arg
|
||||
}
|
||||
methods(protos, protos.Number, [
|
||||
methods(builtins, builtins.Number, [
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
|
||||
[
|
||||
"toExponential",
|
||||
@@ -81,18 +81,18 @@ export const numberGlobal = <R>(runner: Runner<R>) => {
|
||||
return number
|
||||
}
|
||||
|
||||
export const booleanGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const boolean = constructor<R>(protos, protos.Boolean, {
|
||||
export const booleanGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const boolean = constructor<R>(builtins, builtins.Boolean, {
|
||||
name: "Boolean",
|
||||
length: 1,
|
||||
call: coercion(runner, "Boolean").call,
|
||||
call: coercion(ctx, "Boolean").call,
|
||||
})
|
||||
const self = (thisValue: unknown, name: string): boolean => {
|
||||
if (typeof thisValue === "boolean") return thisValue
|
||||
throw typeError(`Boolean.prototype.${name} requires that 'this' be a Boolean.`)
|
||||
}
|
||||
methods(protos, protos.Boolean, [
|
||||
methods(builtins, builtins.Boolean, [
|
||||
["toString", 0, (thisValue) => String(self(thisValue, "toString"))],
|
||||
["valueOf", 0, (thisValue) => self(thisValue, "valueOf")],
|
||||
])
|
||||
|
||||
@@ -20,31 +20,28 @@ import {
|
||||
hidden,
|
||||
keys,
|
||||
own,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramError,
|
||||
ProgramObject,
|
||||
ProgramPromise,
|
||||
ProgramRegExp,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
RegExpObj,
|
||||
set,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { groupBy } from "./collections.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
// ToObject for enumeration.
|
||||
export const enumerableSource = <R>(
|
||||
runner: Runner<R>,
|
||||
label: string,
|
||||
value: unknown,
|
||||
node?: AstNode,
|
||||
): ProgramObject => {
|
||||
export const enumerableSource = <R>(ctx: Interpreter<R>, label: string, value: unknown, node?: AstNode): Obj => {
|
||||
if (value === null || value === undefined) {
|
||||
throw typeError(`${label} cannot convert ${describeValue(value)} to an object.`, node)
|
||||
}
|
||||
if (value instanceof ProgramPromise) {
|
||||
if (value instanceof PromiseObj) {
|
||||
throw invalidData(`${label} received an un-awaited Promise; await it before inspecting the result.`, node)
|
||||
}
|
||||
if (value instanceof ToolReference) {
|
||||
@@ -53,25 +50,25 @@ export const enumerableSource = <R>(
|
||||
node,
|
||||
)
|
||||
}
|
||||
if (typeof value === "string") return new ProgramArray(runner.prototypes.Array, [...value])
|
||||
if (value instanceof ProgramObject) return value
|
||||
return new ProgramObject(runner.prototypes.Object)
|
||||
if (typeof value === "string") return new Arr(ctx.builtins.Array, [...value])
|
||||
if (value instanceof Obj) return value
|
||||
return new Obj(ctx.builtins.Object)
|
||||
}
|
||||
|
||||
export const objectAssign = <R>(runner: Runner<R>, args: Array<unknown>): unknown => {
|
||||
export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<unknown>): unknown => {
|
||||
const target = args[0]
|
||||
// JS would box a primitive target; wrappers and primitives cannot hold fields here.
|
||||
if (!(target instanceof ProgramObject)) {
|
||||
if (!(target instanceof Obj)) {
|
||||
throw typeError(`Object.assign expects a data object or array target, received ${describeValue(target)}.`)
|
||||
}
|
||||
const seen = new Set<object>()
|
||||
for (const source of args.slice(1)) {
|
||||
if (source === null || source === undefined) continue
|
||||
const from = enumerableSource(runner, "Object.assign(...)", source)
|
||||
const from = enumerableSource(ctx, "Object.assign(...)", source)
|
||||
for (const key of enumerableKeys(from)) {
|
||||
rejectCircularInsertion(target, getOwn(from, key), "Object.assign result", seen)
|
||||
if (!set(target, key, getOwn(from, key))) {
|
||||
if (target instanceof ProgramArray && key === "length") throw rangeError("Invalid array length")
|
||||
if (target instanceof Arr && key === "length") throw rangeError("Invalid array length")
|
||||
throw typeError(`Cannot assign to read only property '${String(key)}'.`)
|
||||
}
|
||||
}
|
||||
@@ -79,10 +76,10 @@ export const objectAssign = <R>(runner: Runner<R>, args: Array<unknown>): unknow
|
||||
return target
|
||||
}
|
||||
|
||||
const objectFromEntries = <R>(runner: Runner<R>, source: unknown): Effect.Effect<ProgramObject, unknown, R> => {
|
||||
const out = new ProgramObject(runner.prototypes.Object)
|
||||
const objectFromEntries = <R>(ctx: Interpreter<R>, source: unknown): Effect.Effect<Obj, unknown, R> => {
|
||||
const out = new Obj(ctx.builtins.Object)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source)
|
||||
const cursor = yield* ctx.iterate(source)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("Object.fromEntries expects a synchronous iterable of entries.")
|
||||
}
|
||||
@@ -92,7 +89,7 @@ const objectFromEntries = <R>(runner: Runner<R>, source: unknown): Effect.Effect
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (!(step.value instanceof ProgramObject) || containsOpaqueReference(step.value)) {
|
||||
if (!(step.value instanceof Obj) || containsOpaqueReference(step.value)) {
|
||||
throw typeError("Object.fromEntries expects [key, value] entry objects.")
|
||||
}
|
||||
define(out, coerceToString(getOwn(step.value, 0)), getOwn(step.value, 1))
|
||||
@@ -105,11 +102,12 @@ const objectFromEntries = <R>(runner: Runner<R>, source: unknown): Effect.Effect
|
||||
export const classTag = (value: unknown): string => {
|
||||
if (value === null) return "Null"
|
||||
if (value === undefined) return "Undefined"
|
||||
if (value instanceof ProgramArray) return "Array"
|
||||
if (value instanceof Arr) return "Array"
|
||||
if (value instanceof Callable) return "Function"
|
||||
if (value instanceof ProgramError) return "Error"
|
||||
if (value instanceof ProgramDate) return "Date"
|
||||
if (value instanceof ProgramRegExp) return "RegExp"
|
||||
if (value instanceof ErrorObj) return "Error"
|
||||
if (value instanceof DateObj) return "Date"
|
||||
if (value instanceof RegExpObj) return "RegExp"
|
||||
if (value instanceof Bytes) return "Uint8Array"
|
||||
if (typeof value === "string") return "String"
|
||||
if (typeof value === "number") return "Number"
|
||||
if (typeof value === "boolean") return "Boolean"
|
||||
@@ -121,33 +119,30 @@ const propertyKey = (value: unknown): PropertyKey =>
|
||||
|
||||
// Object constructs identically with or without new, like JS. Only `keys` copies its result into the
|
||||
// program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values.
|
||||
export const objectGlobal = <R>(
|
||||
runner: Runner<R>,
|
||||
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||
) => {
|
||||
const protos = runner.prototypes
|
||||
export const objectGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const construct = (args: Array<unknown>): unknown => {
|
||||
const first = args[0]
|
||||
if (first === null || first === undefined) return new ProgramObject(protos.Object)
|
||||
if (first instanceof ProgramObject) return first
|
||||
if (first === null || first === undefined) return new Obj(builtins.Object)
|
||||
if (first instanceof Obj) return first
|
||||
throw typeError(`Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`)
|
||||
}
|
||||
const object = constructor<R>(protos, protos.Object, {
|
||||
const object = constructor<R>(builtins, builtins.Object, {
|
||||
name: "Object",
|
||||
length: 1,
|
||||
call: (_, args) => Effect.sync(() => construct(args)),
|
||||
construct: (args) => Effect.sync(() => construct(args)),
|
||||
})
|
||||
methods(protos, object, [
|
||||
methods(builtins, object, [
|
||||
[
|
||||
"keys",
|
||||
1,
|
||||
(_, args) =>
|
||||
toProgram(
|
||||
protos,
|
||||
builtins,
|
||||
args[0] instanceof ToolReference
|
||||
? [...toolKeys(args[0].path)]
|
||||
: keys(enumerableSource(runner, "Object.keys(...)", args[0])),
|
||||
? [...ctx.tools.keys(args[0].path)]
|
||||
: keys(enumerableSource(ctx, "Object.keys(...)", args[0])),
|
||||
"Object.keys result",
|
||||
),
|
||||
],
|
||||
@@ -155,23 +150,21 @@ export const objectGlobal = <R>(
|
||||
"values",
|
||||
1,
|
||||
(_, args) =>
|
||||
new ProgramArray(
|
||||
protos.Array,
|
||||
entries(enumerableSource(runner, "Object.values(...)", args[0])).map((entry) => entry[1]),
|
||||
new Arr(
|
||||
builtins.Array,
|
||||
entries(enumerableSource(ctx, "Object.values(...)", args[0])).map((entry) => entry[1]),
|
||||
),
|
||||
],
|
||||
[
|
||||
"entries",
|
||||
1,
|
||||
(_, args) =>
|
||||
new ProgramArray(
|
||||
protos.Array,
|
||||
entries(enumerableSource(runner, "Object.entries(...)", args[0])).map(
|
||||
(entry) => new ProgramArray(protos.Array, entry),
|
||||
),
|
||||
new Arr(
|
||||
builtins.Array,
|
||||
entries(enumerableSource(ctx, "Object.entries(...)", args[0])).map((entry) => new Arr(builtins.Array, entry)),
|
||||
),
|
||||
],
|
||||
["hasOwn", 2, (_, args) => hasOwn(enumerableSource(runner, "Object.hasOwn(...)", args[0]), propertyKey(args[1]))],
|
||||
["hasOwn", 2, (_, args) => hasOwn(enumerableSource(ctx, "Object.hasOwn(...)", args[0]), propertyKey(args[1]))],
|
||||
[
|
||||
"is",
|
||||
2,
|
||||
@@ -182,28 +175,27 @@ export const objectGlobal = <R>(
|
||||
return Object.is(args[0], args[1])
|
||||
},
|
||||
],
|
||||
["assign", 2, (_, args) => objectAssign(runner, args)],
|
||||
["fromEntries", 1, (_, args) => objectFromEntries(runner, args[0])],
|
||||
["assign", 2, (_, args) => objectAssign(ctx, args)],
|
||||
["fromEntries", 1, (_, args) => objectFromEntries(ctx, args[0])],
|
||||
])
|
||||
define(object, "groupBy", groupBy(runner, "Object"), hidden)
|
||||
methods(protos, protos.Object, [
|
||||
define(object, "groupBy", groupBy(ctx, "Object"), hidden)
|
||||
methods(builtins, builtins.Object, [
|
||||
[
|
||||
"hasOwnProperty",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
hasOwn(receiver(ProgramObject, thisValue, "Object.prototype.hasOwnProperty"), propertyKey(args[0])),
|
||||
(thisValue, args) => hasOwn(receiver(Obj, thisValue, "Object.prototype.hasOwnProperty"), propertyKey(args[0])),
|
||||
],
|
||||
[
|
||||
"isPrototypeOf",
|
||||
1,
|
||||
(thisValue, args) => hasPrototype(args[0], receiver(ProgramObject, thisValue, "Object.prototype.isPrototypeOf")),
|
||||
(thisValue, args) => hasPrototype(args[0], receiver(Obj, thisValue, "Object.prototype.isPrototypeOf")),
|
||||
],
|
||||
[
|
||||
"propertyIsEnumerable",
|
||||
1,
|
||||
(thisValue, args) =>
|
||||
own(receiver(ProgramObject, thisValue, "Object.prototype.propertyIsEnumerable"), propertyKey(args[0]))
|
||||
?.enumerable === true,
|
||||
own(receiver(Obj, thisValue, "Object.prototype.propertyIsEnumerable"), propertyKey(args[0]))?.enumerable ===
|
||||
true,
|
||||
],
|
||||
["toString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Prototypes } from "../interpreter/intrinsics.js"
|
||||
import type { Builtins } from "../interpreter/intrinsics.js"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { syntaxError, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
getOwn,
|
||||
ProgramArray,
|
||||
ProgramObject,
|
||||
ProgramRegExp,
|
||||
record,
|
||||
set,
|
||||
} from "../interpreter/objects.js"
|
||||
import type { Runner } from "../interpreter/runner.js"
|
||||
import { define, defineAccessor, getOwn, Arr, Obj, RegExpObj, record, set } from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
const flagProperties = [
|
||||
@@ -35,7 +26,7 @@ const escapeRegexHint =
|
||||
export const toHostRegex = (arg: unknown, method: string, extraFlags = ""): RegExp => {
|
||||
// Native parity: an undefined pattern behaves as an empty pattern.
|
||||
if (arg === undefined) return new RegExp("", extraFlags)
|
||||
if (arg instanceof ProgramRegExp) return arg.regex
|
||||
if (arg instanceof RegExpObj) return arg.regex
|
||||
if (typeof arg === "string") {
|
||||
try {
|
||||
return new RegExp(arg, extraFlags)
|
||||
@@ -50,34 +41,30 @@ export const toHostRegex = (arg: unknown, method: string, extraFlags = ""): RegE
|
||||
)
|
||||
}
|
||||
|
||||
export const matchToValue = (protos: Prototypes, match: RegExpMatchArray): ProgramArray => {
|
||||
const result = new ProgramArray(
|
||||
protos.Array,
|
||||
export const matchToValue = (builtins: Builtins, match: RegExpMatchArray): Arr => {
|
||||
const result = new Arr(
|
||||
builtins.Array,
|
||||
Array.from(match, (group) => group),
|
||||
)
|
||||
if (match.index !== undefined) define(result, "index", match.index)
|
||||
if (match.input !== undefined) define(result, "input", match.input)
|
||||
if (match.groups) define(result, "groups", record(protos.Object, match.groups))
|
||||
if (match.indices) define(result, "indices", indicesToValue(protos, match.indices))
|
||||
if (match.groups) define(result, "groups", record(builtins.Object, match.groups))
|
||||
if (match.indices) define(result, "indices", indicesToValue(builtins, match.indices))
|
||||
return result
|
||||
}
|
||||
|
||||
export const constructRegExp = (
|
||||
protos: Prototypes,
|
||||
args: Array<unknown>,
|
||||
proto: ProgramObject = protos.RegExp,
|
||||
): ProgramRegExp => {
|
||||
export const constructRegExp = (builtins: Builtins, args: Array<unknown>, proto: Obj = builtins.RegExp): RegExpObj => {
|
||||
const first = args[0]
|
||||
const pattern = first instanceof ProgramRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first)
|
||||
const pattern = first instanceof RegExpObj ? first.regex.source : first === undefined ? "" : coerceToString(first)
|
||||
const flagsArg = args[1]
|
||||
if (flagsArg !== undefined && typeof flagsArg !== "string") {
|
||||
throw syntaxError(
|
||||
`RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`,
|
||||
)
|
||||
}
|
||||
const flags = flagsArg ?? (first instanceof ProgramRegExp ? first.regex.flags : "")
|
||||
const flags = flagsArg ?? (first instanceof RegExpObj ? first.regex.flags : "")
|
||||
try {
|
||||
return new ProgramRegExp(proto, pattern, flags)
|
||||
return new RegExpObj(proto, pattern, flags)
|
||||
} catch (error) {
|
||||
const reason = regexFailureReason(error)
|
||||
throw syntaxError(
|
||||
@@ -95,16 +82,16 @@ const toLength = (value: unknown): number => {
|
||||
}
|
||||
|
||||
// RegExp constructs identically with or without new, like JS.
|
||||
export const regexpGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.RegExp
|
||||
const regexp = constructor<R>(protos, proto, {
|
||||
export const regexpGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.RegExp
|
||||
const regexp = constructor<R>(builtins, proto, {
|
||||
name: "RegExp",
|
||||
length: 2,
|
||||
call: (_, args) => Effect.sync(() => constructRegExp(protos, args)),
|
||||
construct: (args, newTarget) => Effect.sync(() => constructRegExp(protos, args, prototypeFrom(newTarget, proto))),
|
||||
call: (_, args) => Effect.sync(() => constructRegExp(builtins, args)),
|
||||
construct: (args, newTarget) => Effect.sync(() => constructRegExp(builtins, args, prototypeFrom(newTarget, proto))),
|
||||
})
|
||||
methods(protos, regexp, [
|
||||
methods(builtins, regexp, [
|
||||
[
|
||||
"escape",
|
||||
1,
|
||||
@@ -115,7 +102,7 @@ export const regexpGlobal = <R>(runner: Runner<R>) => {
|
||||
],
|
||||
])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramRegExp, thisValue, `RegExp.prototype.${name}`)
|
||||
const self = (thisValue: unknown, name: string) => receiver(RegExpObj, thisValue, `RegExp.prototype.${name}`)
|
||||
defineAccessor(proto, "source", (thisValue) => self(thisValue, "source").regex.source)
|
||||
defineAccessor(proto, "flags", (thisValue) => self(thisValue, "flags").regex.flags)
|
||||
for (const name of flagProperties) defineAccessor(proto, name, (thisValue) => self(thisValue, name).regex[name])
|
||||
@@ -131,10 +118,10 @@ export const regexpGlobal = <R>(runner: Runner<R>) => {
|
||||
const matched = value.regex.exec(input)
|
||||
if (stateful) set(value, "lastIndex", value.regex.lastIndex)
|
||||
if (name === "test") return matched !== null
|
||||
return matched === null ? null : matchToValue(protos, matched)
|
||||
return matched === null ? null : matchToValue(builtins, matched)
|
||||
},
|
||||
]
|
||||
methods(protos, proto, [
|
||||
methods(builtins, proto, [
|
||||
run("exec"),
|
||||
run("test"),
|
||||
["toString", 0, (thisValue) => coerceToString(self(thisValue, "toString"))],
|
||||
@@ -142,17 +129,17 @@ export const regexpGlobal = <R>(runner: Runner<R>) => {
|
||||
return regexp
|
||||
}
|
||||
|
||||
const indicesToValue = (protos: Prototypes, indices: RegExpIndicesArray): ProgramArray => {
|
||||
const indicesToValue = (builtins: Builtins, indices: RegExpIndicesArray): Arr => {
|
||||
const range = (pair: [number, number] | undefined) =>
|
||||
pair === undefined ? undefined : new ProgramArray(protos.Array, [...pair])
|
||||
const result = new ProgramArray(protos.Array, Array.from(indices, range))
|
||||
pair === undefined ? undefined : new Arr(builtins.Array, [...pair])
|
||||
const result = new Arr(builtins.Array, Array.from(indices, range))
|
||||
const groups = indices.groups
|
||||
define(
|
||||
result,
|
||||
"groups",
|
||||
groups === undefined
|
||||
? undefined
|
||||
: record(protos.Object, Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))),
|
||||
: record(builtins.Object, Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))),
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { toProgram } from "../data.js"
|
||||
import { constructor, type Method, methods } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { ProgramArray, ProgramPromise, ProgramRegExp, record } from "../interpreter/objects.js"
|
||||
import { Arr, PromiseObj, RegExpObj, record } from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, typeofValue } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, isSupportedCallback, type Runner } from "../interpreter/runner.js"
|
||||
import { applyCollectionCallback, isSupportedCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { matchToValue, toHostRegex } from "./regexp.js"
|
||||
import { coerceToNumber, coerceToString, coercion } from "./value.js"
|
||||
|
||||
@@ -26,13 +27,13 @@ const replaceAllNeedsGlobal = (pattern: RegExp) => {
|
||||
}
|
||||
|
||||
const replaceWithCallback = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
value: string,
|
||||
name: "replace" | "replaceAll",
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const protos = runner.prototypes
|
||||
const apply = applyCollectionCallback(runner, args[1], `String.${name}`)
|
||||
const builtins = ctx.builtins
|
||||
const apply = applyCollectionCallback(ctx, args[1], `String.${name}`)
|
||||
const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
|
||||
const collect = (...callbackArgs: Array<unknown>): string => {
|
||||
const match = callbackArgs[0]
|
||||
@@ -42,13 +43,13 @@ const replaceWithCallback = <R>(
|
||||
if (typeof match !== "string" || typeof offset !== "number") {
|
||||
throw typeError(`String.${name} produced an invalid replacement match.`)
|
||||
}
|
||||
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(protos.Object, groups as Record<string, unknown>)
|
||||
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(builtins.Object, groups as Record<string, unknown>)
|
||||
matches.push({ match, offset, args: callbackArgs })
|
||||
return match
|
||||
}
|
||||
|
||||
const pattern = args[0]
|
||||
if (pattern instanceof ProgramRegExp) {
|
||||
if (pattern instanceof RegExpObj) {
|
||||
if (name === "replaceAll") replaceAllNeedsGlobal(pattern.regex)
|
||||
if (name === "replace") value.replace(pattern.regex, collect)
|
||||
else value.replaceAll(pattern.regex, collect)
|
||||
@@ -65,9 +66,9 @@ const replaceWithCallback = <R>(
|
||||
const replacement = yield* apply(match.args)
|
||||
output.push(
|
||||
value.slice(end, match.offset),
|
||||
replacement instanceof ProgramPromise
|
||||
replacement instanceof PromiseObj
|
||||
? "[object Promise]"
|
||||
: coerceToString(toProgram(protos, replacement, `String.${name} replacer result`)),
|
||||
: coerceToString(toProgram(builtins, replacement, `String.${name} replacer result`)),
|
||||
)
|
||||
end = match.offset + match.match.length
|
||||
}
|
||||
@@ -76,12 +77,12 @@ const replaceWithCallback = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const string = constructor<R>(protos, protos.String, {
|
||||
export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const string = constructor<R>(builtins, builtins.String, {
|
||||
name: "String",
|
||||
length: 1,
|
||||
call: coercion(runner, "String").call,
|
||||
call: coercion(ctx, "String").call,
|
||||
})
|
||||
const codeUnits = (name: string, op: (...codes: Array<number>) => string): Method => [
|
||||
name,
|
||||
@@ -96,7 +97,7 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
}),
|
||||
),
|
||||
]
|
||||
methods(protos, string, [
|
||||
methods(builtins, string, [
|
||||
codeUnits("fromCharCode", String.fromCharCode),
|
||||
codeUnits("fromCodePoint", String.fromCodePoint),
|
||||
])
|
||||
@@ -118,7 +119,7 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
const optStr = (name: string, args: Array<unknown>, index: number): string | undefined =>
|
||||
args[index] === undefined ? undefined : str(name, args, index)
|
||||
const rejectRegex = (name: string, args: Array<unknown>): void => {
|
||||
if (args[0] instanceof ProgramRegExp) {
|
||||
if (args[0] instanceof RegExpObj) {
|
||||
throw typeError(
|
||||
`String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`,
|
||||
)
|
||||
@@ -131,13 +132,13 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
]
|
||||
const replace = (name: "replace" | "replaceAll") =>
|
||||
simple(name, 2, (value, args) => {
|
||||
if (isSupportedCallback(args[1])) return replaceWithCallback(runner, value, name, args)
|
||||
if (isSupportedCallback(args[1])) return replaceWithCallback(ctx, value, name, args)
|
||||
if (typeofValue(args[1]) === "function") {
|
||||
throw typeError(
|
||||
`String.${name} cannot use this callable as a replacer; wrap it in an arrow function, e.g. (match) => tools.ns.tool(match).`,
|
||||
)
|
||||
}
|
||||
if (args[0] instanceof ProgramRegExp) {
|
||||
if (args[0] instanceof RegExpObj) {
|
||||
const pattern = args[0].regex
|
||||
const replacement = str(name, args, 1)
|
||||
if (name === "replaceAll") replaceAllNeedsGlobal(pattern)
|
||||
@@ -147,7 +148,7 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
return value.replaceAll(str(name, args, 0), str(name, args, 1))
|
||||
})
|
||||
|
||||
methods(protos, protos.String, [
|
||||
methods(builtins, builtins.String, [
|
||||
simple("toString", 0, (value) => value),
|
||||
simple("valueOf", 0, (value) => value),
|
||||
simple("toLowerCase", 0, (value) => value.toLowerCase()),
|
||||
@@ -170,7 +171,7 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
}
|
||||
}),
|
||||
simple("split", 2, (value, args) => {
|
||||
const wrap = (parts: Array<string>) => new ProgramArray(protos.Array, parts)
|
||||
const wrap = (parts: Array<string>) => new Arr(builtins.Array, parts)
|
||||
// Native: an undefined separator returns the whole string, not a split on "undefined",
|
||||
// unless the limit truncates to zero.
|
||||
const requestedLimit = optNum("split", args, 1)
|
||||
@@ -178,7 +179,7 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
return wrap(requestedLimit !== undefined && requestedLimit >>> 0 === 0 ? [] : [value])
|
||||
}
|
||||
const parts =
|
||||
args[0] instanceof ProgramRegExp
|
||||
args[0] instanceof RegExpObj
|
||||
? value.split(args[0].regex, requestedLimit)
|
||||
: value.split(str("split", args, 0), requestedLimit === undefined ? undefined : requestedLimit >>> 0)
|
||||
checkArrayLength(parts.length)
|
||||
@@ -208,8 +209,8 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
const matched = value.match(pattern)
|
||||
if (matched === null) return null
|
||||
// Preserve the own `index` and `groups` properties on non-global matches.
|
||||
if (pattern.global) return toProgram(protos, matched, "String.match result")
|
||||
return matchToValue(protos, matched)
|
||||
if (pattern.global) return toProgram(builtins, matched, "String.match result")
|
||||
return matchToValue(builtins, matched)
|
||||
}),
|
||||
simple("matchAll", 1, (value, args) => {
|
||||
const pattern = toHostRegex(args[0], "matchAll", "g")
|
||||
@@ -221,9 +222,9 @@ export const stringGlobal = <R>(runner: Runner<R>) => {
|
||||
const matches: Array<unknown> = []
|
||||
for (const match of value.matchAll(pattern)) {
|
||||
checkArrayLength(matches.length + 1)
|
||||
matches.push(matchToValue(protos, match))
|
||||
matches.push(matchToValue(builtins, match))
|
||||
}
|
||||
return new ProgramArray(protos.Array, matches)
|
||||
return new Arr(builtins.Array, matches)
|
||||
}),
|
||||
simple("search", 1, (value, args) => value.search(toHostRegex(args[0], "search"))),
|
||||
simple("repeat", 1, (value, args) => {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { Effect } from "effect"
|
||||
import { toProgram, ToolRuntimeError } from "../data.js"
|
||||
import type { Prototypes } from "../interpreter/intrinsics.js"
|
||||
import type { Builtins } from "../interpreter/intrinsics.js"
|
||||
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { PendingThrow, typeError, uriError } from "../interpreter/model.js"
|
||||
import {
|
||||
defineAccessor,
|
||||
entries,
|
||||
isWrapper,
|
||||
ProgramArray,
|
||||
ProgramObject,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
} from "../interpreter/objects.js"
|
||||
import { defineAccessor, entries, isWrapper, Arr, Obj, URLObj, URLSearchParamsObj } from "../interpreter/objects.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
const urlProperties = [
|
||||
@@ -30,8 +23,8 @@ const urlProperties = [
|
||||
"hash",
|
||||
] as const
|
||||
|
||||
export const uriArgument = (protos: Prototypes, value: unknown, label: string): string =>
|
||||
coerceToString(toProgram(protos, value, label))
|
||||
export const uriArgument = (builtins: Builtins, value: unknown, label: string): string =>
|
||||
coerceToString(toProgram(builtins, value, label))
|
||||
|
||||
type UriFunction = "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent"
|
||||
|
||||
@@ -42,9 +35,9 @@ const uriFunctions: Record<UriFunction, (value: string) => string> = {
|
||||
decodeURIComponent,
|
||||
}
|
||||
|
||||
export const uriGlobal = <R>(runner: Runner<R>, name: UriFunction) =>
|
||||
fn<R>(runner.prototypes, name, 1, (_, args) => {
|
||||
const value = uriArgument(runner.prototypes, args[0], `${name} input`)
|
||||
export const uriGlobal = <R>(ctx: Interpreter<R>, name: UriFunction) =>
|
||||
fn<R>(ctx.builtins, name, 1, (_, args) => {
|
||||
const value = uriArgument(ctx.builtins, args[0], `${name} input`)
|
||||
try {
|
||||
return uriFunctions[name](value)
|
||||
} catch (error) {
|
||||
@@ -52,25 +45,25 @@ export const uriGlobal = <R>(runner: Runner<R>, name: UriFunction) =>
|
||||
}
|
||||
})
|
||||
|
||||
const urlArgument = (protos: Prototypes, value: unknown, label: string): string =>
|
||||
value instanceof ProgramURL ? value.url.href : uriArgument(protos, value, label)
|
||||
const urlArgument = (builtins: Builtins, value: unknown, label: string): string =>
|
||||
value instanceof URLObj ? value.url.href : uriArgument(builtins, value, label)
|
||||
|
||||
export const urlGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.URL
|
||||
const construct = (args: Array<unknown>, into: ProgramObject): ProgramURL => {
|
||||
export const urlGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.URL
|
||||
const construct = (args: Array<unknown>, into: Obj): URLObj => {
|
||||
if (args.length === 0) {
|
||||
throw typeError("new URL(...) requires a URL string and an optional base URL.")
|
||||
}
|
||||
const input = urlArgument(protos, args[0], "new URL input")
|
||||
const base = args[1] === undefined ? undefined : urlArgument(protos, args[1], "new URL base")
|
||||
const input = urlArgument(builtins, args[0], "new URL input")
|
||||
const base = args[1] === undefined ? undefined : urlArgument(builtins, args[1], "new URL base")
|
||||
try {
|
||||
return new ProgramURL(into, protos.URLSearchParams, new URL(input, base))
|
||||
return new URLObj(into, builtins.URLSearchParams, new URL(input, base))
|
||||
} catch {
|
||||
throw typeError(`new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`)
|
||||
}
|
||||
}
|
||||
const url = constructor<R>(protos, proto, {
|
||||
const url = constructor<R>(builtins, proto, {
|
||||
name: "URL",
|
||||
length: 1,
|
||||
call: requiresNew("URL"),
|
||||
@@ -81,19 +74,19 @@ export const urlGlobal = <R>(runner: Runner<R>) => {
|
||||
1,
|
||||
(_, args) => {
|
||||
if (args.length === 0) throw typeError(`URL.${name} requires a URL argument.`)
|
||||
const input = urlArgument(protos, args[0], `URL.${name} input`)
|
||||
const base = args[1] === undefined ? undefined : urlArgument(protos, args[1], `URL.${name} base`)
|
||||
const input = urlArgument(builtins, args[0], `URL.${name} input`)
|
||||
const base = args[1] === undefined ? undefined : urlArgument(builtins, args[1], `URL.${name} base`)
|
||||
try {
|
||||
const parsed = new URL(input, base)
|
||||
return name === "canParse" ? true : new ProgramURL(proto, protos.URLSearchParams, parsed)
|
||||
return name === "canParse" ? true : new URLObj(proto, builtins.URLSearchParams, parsed)
|
||||
} catch {
|
||||
return name === "canParse" ? false : null
|
||||
}
|
||||
},
|
||||
]
|
||||
methods(protos, url, [parse("canParse"), parse("parse")])
|
||||
methods(builtins, url, [parse("canParse"), parse("parse")])
|
||||
|
||||
const self = (thisValue: unknown, name: string) => receiver(ProgramURL, thisValue, `URL.prototype.${name}`)
|
||||
const self = (thisValue: unknown, name: string) => receiver(URLObj, thisValue, `URL.prototype.${name}`)
|
||||
for (const name of urlProperties) {
|
||||
defineAccessor(
|
||||
proto,
|
||||
@@ -104,7 +97,11 @@ export const urlGlobal = <R>(runner: Runner<R>) => {
|
||||
: (thisValue, value) => {
|
||||
const target = self(thisValue, name)
|
||||
try {
|
||||
;(target.url as unknown as Record<string, string>)[name] = uriArgument(protos, value, `URL.${name} value`)
|
||||
;(target.url as unknown as Record<string, string>)[name] = uriArgument(
|
||||
builtins,
|
||||
value,
|
||||
`URL.${name} value`,
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof PendingThrow || error instanceof ToolRuntimeError) throw error
|
||||
throw typeError(`URL.${name} received an invalid value.`)
|
||||
@@ -113,16 +110,16 @@ export const urlGlobal = <R>(runner: Runner<R>) => {
|
||||
)
|
||||
}
|
||||
defineAccessor(proto, "searchParams", (thisValue) => self(thisValue, "searchParams").searchParams)
|
||||
methods(protos, proto, [
|
||||
methods(builtins, proto, [
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").url.href],
|
||||
["toJSON", 0, (thisValue) => self(thisValue, "toJSON").url.href],
|
||||
])
|
||||
return url
|
||||
}
|
||||
|
||||
const readPair = <R>(runner: Runner<R>, value: unknown): Effect.Effect<Array<string>, unknown, R> =>
|
||||
const readPair = <R>(ctx: Interpreter<R>, value: unknown): Effect.Effect<Array<string>, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(value)
|
||||
const cursor = yield* ctx.iterate(value)
|
||||
if (cursor === undefined) {
|
||||
throw typeError("new URLSearchParams(...) expects iterable [name, value] pairs.")
|
||||
}
|
||||
@@ -133,26 +130,26 @@ const readPair = <R>(runner: Runner<R>, value: unknown): Effect.Effect<Array<str
|
||||
items.push(
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => uriArgument(runner.prototypes, step.value, "URLSearchParams pair value")),
|
||||
Effect.sync(() => uriArgument(ctx.builtins, step.value, "URLSearchParams pair value")),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const constructURLSearchParams = <R>(
|
||||
runner: Runner<R>,
|
||||
ctx: Interpreter<R>,
|
||||
init: unknown,
|
||||
proto: ProgramObject,
|
||||
): Effect.Effect<ProgramURLSearchParams, unknown, R> => {
|
||||
const wrap = (params: URLSearchParams) => new ProgramURLSearchParams(proto, params)
|
||||
proto: Obj,
|
||||
): Effect.Effect<URLSearchParamsObj, unknown, R> => {
|
||||
const wrap = (params: URLSearchParams) => new URLSearchParamsObj(proto, params)
|
||||
if (init === undefined) return Effect.succeed(wrap(new URLSearchParams()))
|
||||
if (init instanceof ProgramURLSearchParams) return Effect.succeed(wrap(new URLSearchParams(init.params)))
|
||||
if (init instanceof URLSearchParamsObj) return Effect.succeed(wrap(new URLSearchParams(init.params)))
|
||||
if (typeof init === "string") return Effect.succeed(wrap(new URLSearchParams(init)))
|
||||
if (init === null || typeof init === "number" || typeof init === "boolean") {
|
||||
return Effect.succeed(wrap(new URLSearchParams(coerceToString(init))))
|
||||
}
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(init)
|
||||
const cursor = yield* ctx.iterate(init)
|
||||
if (cursor !== undefined) {
|
||||
const pairs: Array<Array<string>> = []
|
||||
while (true) {
|
||||
@@ -163,14 +160,14 @@ const constructURLSearchParams = <R>(
|
||||
}
|
||||
return wrap(new URLSearchParams(pairs.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])))
|
||||
}
|
||||
pairs.push(yield* preserveConsumerError(cursor, readPair(runner, step.value)))
|
||||
pairs.push(yield* preserveConsumerError(cursor, readPair(ctx, step.value)))
|
||||
}
|
||||
}
|
||||
if (isRuntimeReference(init)) {
|
||||
throw typeError("new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.")
|
||||
}
|
||||
if (isWrapper(init)) return wrap(new URLSearchParams())
|
||||
if (!(init instanceof ProgramObject)) {
|
||||
if (!(init instanceof Obj)) {
|
||||
throw typeError(
|
||||
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
|
||||
)
|
||||
@@ -181,26 +178,26 @@ const constructURLSearchParams = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
export const urlSearchParamsGlobal = <R>(runner: Runner<R>) => {
|
||||
const protos = runner.prototypes
|
||||
const proto = protos.URLSearchParams
|
||||
const searchParams = constructor<R>(protos, proto, {
|
||||
export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const builtins = ctx.builtins
|
||||
const proto = builtins.URLSearchParams
|
||||
const searchParams = constructor<R>(builtins, proto, {
|
||||
name: "URLSearchParams",
|
||||
call: requiresNew("URLSearchParams"),
|
||||
construct: (args, newTarget) => constructURLSearchParams(runner, args[0], prototypeFrom(newTarget, proto)),
|
||||
construct: (args, newTarget) => constructURLSearchParams(ctx, args[0], prototypeFrom(newTarget, proto)),
|
||||
})
|
||||
const self = (thisValue: unknown, name: string) =>
|
||||
receiver(ProgramURLSearchParams, thisValue, `URLSearchParams.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new ProgramArray(protos.Array, items)
|
||||
receiver(URLSearchParamsObj, thisValue, `URLSearchParams.prototype.${name}`)
|
||||
const wrap = (items: Array<unknown>) => new Arr(builtins.Array, items)
|
||||
const arg = (name: string, args: Array<unknown>, index: number): string =>
|
||||
uriArgument(protos, args[index], `URLSearchParams.${name} argument ${index + 1}`)
|
||||
uriArgument(builtins, args[index], `URLSearchParams.${name} argument ${index + 1}`)
|
||||
const requireArgs = (name: string, args: Array<unknown>, count: number): void => {
|
||||
if (args.length < count) {
|
||||
throw typeError(`URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`)
|
||||
}
|
||||
}
|
||||
defineAccessor(proto, "size", (thisValue) => self(thisValue, "size").params.size)
|
||||
methods(protos, proto, [
|
||||
methods(builtins, proto, [
|
||||
[
|
||||
"append",
|
||||
2,
|
||||
@@ -280,7 +277,7 @@ export const urlSearchParamsGlobal = <R>(runner: Runner<R>) => {
|
||||
(thisValue, args) => {
|
||||
requireArgs("forEach", args, 1)
|
||||
const target = self(thisValue, "forEach")
|
||||
const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach")
|
||||
const apply = applyCollectionCallback(ctx, args[0], "URLSearchParams.forEach")
|
||||
return Effect.gen(function* () {
|
||||
for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target])
|
||||
return undefined
|
||||
|
||||
@@ -4,17 +4,18 @@ import { typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
get,
|
||||
isWrapper,
|
||||
type NativeFunction,
|
||||
ProgramArray,
|
||||
ProgramDate,
|
||||
ProgramError,
|
||||
ProgramMap,
|
||||
ProgramRegExp,
|
||||
ProgramSet,
|
||||
ProgramURL,
|
||||
ProgramURLSearchParams,
|
||||
type Native,
|
||||
Arr,
|
||||
Bytes,
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
MapObj,
|
||||
RegExpObj,
|
||||
SetObj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import type { Runner } from "../interpreter/runner.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
|
||||
|
||||
@@ -22,14 +23,14 @@ export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "
|
||||
export const coerceToString = (value: unknown): string => {
|
||||
if (value === null) return "null"
|
||||
if (value === undefined) return "undefined"
|
||||
if (value instanceof ProgramDate)
|
||||
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
|
||||
if (value instanceof ProgramRegExp) return `/${value.regex.source}/${value.regex.flags}`
|
||||
if (value instanceof ProgramMap) return "[object Map]"
|
||||
if (value instanceof ProgramSet) return "[object Set]"
|
||||
if (value instanceof ProgramURL) return value.url.href
|
||||
if (value instanceof ProgramURLSearchParams) return value.params.toString()
|
||||
if (value instanceof ProgramError) {
|
||||
if (value instanceof DateObj) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
|
||||
if (value instanceof RegExpObj) return `/${value.regex.source}/${value.regex.flags}`
|
||||
if (value instanceof MapObj) return "[object Map]"
|
||||
if (value instanceof SetObj) return "[object Set]"
|
||||
if (value instanceof URLObj) return value.url.href
|
||||
if (value instanceof URLSearchParamsObj) return value.params.toString()
|
||||
if (value instanceof Bytes) return value.bytes.join(",")
|
||||
if (value instanceof ErrorObj) {
|
||||
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
|
||||
const name = get(value, "name")
|
||||
const message = get(value, "message")
|
||||
@@ -39,7 +40,7 @@ export const coerceToString = (value: unknown): string => {
|
||||
if (shownName === "") return shownMessage
|
||||
return `${shownName}: ${shownMessage}`
|
||||
}
|
||||
if (value instanceof ProgramArray) {
|
||||
if (value instanceof Arr) {
|
||||
return value.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
}
|
||||
if (typeof value === "object") return "[object Object]"
|
||||
@@ -47,15 +48,16 @@ export const coerceToString = (value: unknown): string => {
|
||||
}
|
||||
|
||||
export const coerceToNumber = (value: unknown): number => {
|
||||
if (value instanceof ProgramDate) return value.time
|
||||
if (value instanceof DateObj) return value.time
|
||||
if (value instanceof Bytes) return Number(coerceToString(value))
|
||||
if (isWrapper(value)) return Number.NaN
|
||||
if (value instanceof ProgramArray) return Number(coerceToString(value))
|
||||
if (value instanceof Arr) return Number(coerceToString(value))
|
||||
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
|
||||
}
|
||||
|
||||
export type Coercion = "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN"
|
||||
|
||||
const coerce = <R>(runner: Runner<R>, name: Coercion, args: Array<unknown>): unknown => {
|
||||
const coerce = <R>(ctx: Interpreter<R>, name: Coercion, args: Array<unknown>): unknown => {
|
||||
// Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the
|
||||
// other coercers match native through the undefined-argument path below.
|
||||
if (args.length === 0) {
|
||||
@@ -72,7 +74,7 @@ const coerce = <R>(runner: Runner<R>, name: Coercion, args: Array<unknown>): unk
|
||||
if (name === "parseInt") return parseInt(coerceToString(raw))
|
||||
return parseFloat(coerceToString(raw))
|
||||
}
|
||||
const value = toProgram(runner.prototypes, raw, `${name} input`)
|
||||
const value = toProgram(ctx.builtins, raw, `${name} input`)
|
||||
if (name === "Number") return coerceToNumber(value)
|
||||
if (name === "Boolean") return Boolean(value)
|
||||
if (name === "isFinite") return Number.isFinite(coerceToNumber(value))
|
||||
@@ -89,7 +91,5 @@ const coerce = <R>(runner: Runner<R>, name: Coercion, args: Array<unknown>): unk
|
||||
}
|
||||
|
||||
/** A global coercion function such as `Number` or `parseInt`. */
|
||||
export const coercion = <R>(runner: Runner<R>, name: Coercion, length = 1): NativeFunction<R> =>
|
||||
fn(runner.prototypes, name, length, (_, args) =>
|
||||
toProgram(runner.prototypes, coerce(runner, name, args), `${name} result`),
|
||||
)
|
||||
export const coercion = <R>(ctx: Interpreter<R>, name: Coercion, length = 1): Native<R> =>
|
||||
fn(ctx.builtins, name, length, (_, args) => toProgram(ctx.builtins, coerce(ctx, name, args), `${name} result`))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { fn, methods } from "../interpreter/native.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { ProgramObject } from "../interpreter/objects.js"
|
||||
import type { Runner } from "../interpreter/runner.js"
|
||||
import { Bytes, Obj } from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies. Invalid input is a
|
||||
// TypeError as well; browsers throw a DOMException named InvalidCharacterError, which CodeMode does not have.
|
||||
export const base64Global = <R>(runner: Runner<R>, name: "atob" | "btoa") =>
|
||||
fn<R>(runner.prototypes, name, 1, (_, args) => {
|
||||
export const base64Global = <R>(ctx: Interpreter<R>, name: "atob" | "btoa") =>
|
||||
fn<R>(ctx.builtins, name, 1, (_, args) => {
|
||||
if (args.length === 0) throw typeError(`${name} requires 1 argument (a string)`)
|
||||
const input = coerceToString(args[0])
|
||||
try {
|
||||
@@ -17,8 +18,21 @@ export const base64Global = <R>(runner: Runner<R>, name: "atob" | "btoa") =>
|
||||
}
|
||||
})
|
||||
|
||||
export const cryptoGlobal = <R>(runner: Runner<R>) => {
|
||||
const object = new ProgramObject(runner.prototypes.Object)
|
||||
methods(runner.prototypes, object, [["randomUUID", 0, () => crypto.randomUUID()]])
|
||||
export const cryptoGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
const object = new Obj(ctx.builtins.Object)
|
||||
methods(ctx.builtins, object, [
|
||||
["randomUUID", 0, () => crypto.randomUUID()],
|
||||
[
|
||||
"getRandomValues",
|
||||
1,
|
||||
(_, args) => {
|
||||
if (!(args[0] instanceof Bytes)) {
|
||||
throw typeError(`crypto.getRandomValues expects a Uint8Array, received ${describeValue(args[0])}.`)
|
||||
}
|
||||
crypto.getRandomValues(args[0].bytes)
|
||||
return args[0]
|
||||
},
|
||||
],
|
||||
])
|
||||
return object
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cause, Effect, Exit, Formatter, Schema } from "effect"
|
||||
import { fromData, toData, ToolRuntimeError } from "./data.js"
|
||||
import type { Prototypes } from "./interpreter/intrinsics.js"
|
||||
import type { Builtins } from "./interpreter/intrinsics.js"
|
||||
import { toolError } from "./tool-error.js"
|
||||
import {
|
||||
decodeInput as decodeToolInput,
|
||||
@@ -309,7 +309,7 @@ export type ToolRuntime<R = never> = {
|
||||
/** Per-execution call state over tools prepared once for the runtime. */
|
||||
export const make = <R>(
|
||||
prepared: Prepared<R>,
|
||||
prototypes: Prototypes,
|
||||
builtins: Builtins,
|
||||
maxToolCalls: number | undefined,
|
||||
hooks?: ToolCallHooks<R>,
|
||||
): ToolRuntime<R> => {
|
||||
@@ -375,7 +375,7 @@ export const make = <R>(
|
||||
}),
|
||||
)
|
||||
return yield* Effect.try({
|
||||
try: () => fromData(prototypes, decodeToolOutput(tool, raw), `Result from tool '${name}'`),
|
||||
try: () => fromData(builtins, decodeToolOutput(tool, raw), `Result from tool '${name}'`),
|
||||
catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -171,6 +171,34 @@ describe("values are converted at the boundary, never shared", () => {
|
||||
expect([...(held[0] as Set<{ z: number }>)][0]).toEqual({ z: 1 })
|
||||
})
|
||||
|
||||
test("bytes cross as copies in both directions; ArrayBuffer comes in as Uint8Array", async () => {
|
||||
const stored = new Uint8Array([1, 2, 3])
|
||||
const target = CodeMode.make({
|
||||
extensions: [
|
||||
Extension.make({
|
||||
name: "bin",
|
||||
globals: {
|
||||
stored: () => stored,
|
||||
buffer: () => stored.buffer,
|
||||
wide: () => new Uint16Array(1),
|
||||
first: (bytes: Uint8Array) => {
|
||||
bytes[0] = 99
|
||||
return bytes.constructor.name
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
expect(
|
||||
await value(
|
||||
`const b = stored(); b[0] = 42; const mine = new Uint8Array([5]); const name = first(mine); return [[...b], [...stored()], [...buffer()], b instanceof Uint8Array, name, mine[0]]`,
|
||||
target,
|
||||
),
|
||||
).toEqual([[42, 2, 3], [1, 2, 3], [1, 2, 3], true, "Uint8Array", 5])
|
||||
expect([...stored]).toEqual([1, 2, 3])
|
||||
expect((await failure(`wide()`, target)).message).toContain("produced a Uint16Array, which the program cannot hold")
|
||||
})
|
||||
|
||||
test("a __proto__ key never reaches the host object", async () => {
|
||||
held.length = 0
|
||||
await value(`keep({ __proto__: { polluted: true }, a: 1 })`)
|
||||
|
||||
@@ -721,6 +721,185 @@ describe("Set", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Uint8Array", () => {
|
||||
test("constructs from a length, an array, an iterable, or another Uint8Array", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const a = new Uint8Array(2)
|
||||
const b = new Uint8Array([1, 2, 300])
|
||||
const c = Uint8Array.from(new Set([7, 8]))
|
||||
const d = new Uint8Array(b)
|
||||
d[0] = 9
|
||||
return [[...a], [...b], [...c], [...d], [...b], new Uint8Array("3").length, [...Uint8Array.of(1, "2")]]
|
||||
`),
|
||||
).toEqual([[0, 0], [1, 2, 44], [7, 8], [9, 2, 44], [1, 2, 44], 3, [1, 2]])
|
||||
expect((await error(`new Uint8Array(-1)`)).message).toContain("Invalid typed array length: -1")
|
||||
expect((await error(`new Uint8Array(1.5)`)).message).toContain("Invalid typed array length")
|
||||
expect((await error(`new Uint8Array(20_000_000)`)).message).toContain("Invalid array length")
|
||||
expect((await error(`Uint8Array.from(/x/)`)).message).toContain("received a RegExp")
|
||||
})
|
||||
|
||||
test("index reads and writes clamp to a byte and ignore out-of-range writes, like JS", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array(3)
|
||||
b[0] = 300
|
||||
b[1] = -1
|
||||
b["2"] = "7"
|
||||
b[5] = 9
|
||||
b.tag = "x"
|
||||
return [b[0], b[1], b[2], b[5], b.length, Object.keys(b), 1 in b, 5 in b, b.tag]
|
||||
`),
|
||||
).toEqual([44, 255, 7, null, 3, ["0", "1", "2", "tag"], true, false, "x"])
|
||||
expect((await error(`delete new Uint8Array(1)[0]`)).message).toContain("Cannot delete property '0'")
|
||||
expect((await error(`Uint8Array.prototype.length`)).message).toContain("incompatible receiver")
|
||||
})
|
||||
|
||||
test("iteration, spread, destructuring, and Array.from", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([1, 2, 3])
|
||||
const [x, ...rest] = b
|
||||
const seen = []
|
||||
for (const byte of b) seen.push(byte)
|
||||
function* g() { yield* b }
|
||||
return [x, rest, seen, [...g()], Array.from(b, (n) => n * 2), new Set(b).size, Array.isArray(b)]
|
||||
`),
|
||||
).toEqual([1, [2, 3], [1, 2, 3], [1, 2, 3], [2, 4, 6], 3, false])
|
||||
})
|
||||
|
||||
test("methods", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = Uint8Array.from([1, 2, 3, 2])
|
||||
const view = b.subarray(1, 3)
|
||||
view.fill(9)
|
||||
const copy = b.slice(0, 2)
|
||||
copy[0] = 5
|
||||
const target = new Uint8Array(4)
|
||||
target.set([1, 2], 2)
|
||||
target.set(b.subarray(0, 1))
|
||||
return [
|
||||
b.at(-1), [...b], [...copy], [...target], b.indexOf(9), b.lastIndexOf(9), b.includes(3), b.includes(2, 1),
|
||||
b.join("-"), b.toString(), [...b.keys()], [...b.values()], [...b.entries()], [...Uint8Array.from([1, 2]).reverse()],
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
2,
|
||||
[1, 9, 9, 2],
|
||||
[5, 9],
|
||||
[1, 0, 1, 2],
|
||||
1,
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
"1-9-9-2",
|
||||
"1,9,9,2",
|
||||
[0, 1, 2, 3],
|
||||
[1, 9, 9, 2],
|
||||
[
|
||||
[0, 1],
|
||||
[1, 9],
|
||||
[2, 9],
|
||||
[3, 2],
|
||||
],
|
||||
[2, 1],
|
||||
])
|
||||
expect((await error(`new Uint8Array(2).set([1, 2, 3])`)).message).toContain("does not fit")
|
||||
expect((await error(`new Uint8Array(2).set([1], 2)`)).message).toContain("does not fit")
|
||||
})
|
||||
|
||||
test("base64 and hex", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
new Uint8Array([104, 105]).toBase64(), new Uint8Array([255, 0]).toHex(),
|
||||
[...Uint8Array.fromBase64("aGk=")], [...Uint8Array.fromHex("ff00")],
|
||||
]
|
||||
`),
|
||||
).toEqual(["aGk=", "ff00", [104, 105], [255, 0]])
|
||||
expect(await error(`Uint8Array.fromBase64("!!!")`)).toMatchObject({
|
||||
message: expect.stringContaining("SyntaxError"),
|
||||
})
|
||||
expect(await error(`Uint8Array.fromHex("zz")`)).toMatchObject({ message: expect.stringContaining("SyntaxError") })
|
||||
})
|
||||
|
||||
test("coercion, console, JSON, and Object.prototype.toString", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const b = new Uint8Array([1, 2])
|
||||
console.log(b, new Uint8Array())
|
||||
return [String(b), b + "", +new Uint8Array([5]), Number.isNaN(Number(b)), b == "1,2", JSON.stringify(b), b.toLocaleString(), typeof b, b instanceof Uint8Array]
|
||||
`),
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "[object Uint8Array]", "object", true])
|
||||
expect((await run(`console.log(new Uint8Array([1, 2]), new Uint8Array())`)).logs).toEqual([
|
||||
"Uint8Array(2) [1,2] Uint8Array(0) []",
|
||||
])
|
||||
})
|
||||
|
||||
test("cannot cross the tool boundary; the error says how to encode it", async () => {
|
||||
expect((await error(`return new Uint8Array(1)`)).message).toContain(
|
||||
"Execution result contains a Uint8Array; pass text instead",
|
||||
)
|
||||
expect((await error(`return { deep: [new Uint8Array(1)] }`)).message).toContain("bytes.toBase64()")
|
||||
})
|
||||
})
|
||||
|
||||
describe("TextEncoder and TextDecoder", () => {
|
||||
test("round-trips UTF-8 and exposes the standard fields", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const encoder = new TextEncoder()
|
||||
const bytes = encoder.encode("héllo ✓")
|
||||
const decoder = new TextDecoder()
|
||||
return [
|
||||
[...bytes], decoder.decode(bytes), decoder.decode(), [...encoder.encode()], encoder.encoding,
|
||||
decoder.encoding, decoder.fatal, decoder.ignoreBOM, new TextDecoder("UTF8", { fatal: true }).fatal,
|
||||
new TextDecoder().decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])),
|
||||
new TextDecoder("utf-8", { ignoreBOM: true }).decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])),
|
||||
new TextDecoder().decode(new Uint8Array([0xff])),
|
||||
encoder instanceof TextEncoder, decoder instanceof TextDecoder,
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
[104, 195, 169, 108, 108, 111, 32, 226, 156, 147],
|
||||
"héllo ✓",
|
||||
"",
|
||||
[],
|
||||
"utf-8",
|
||||
"utf-8",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
"A",
|
||||
"\ufeffA",
|
||||
"\ufffd",
|
||||
true,
|
||||
true,
|
||||
])
|
||||
})
|
||||
|
||||
test("only UTF-8 is supported; fatal decoding rejects malformed input", async () => {
|
||||
expect((await error(`new TextDecoder("latin1")`)).message).toContain('The "latin1" encoding is not supported')
|
||||
expect((await error(`new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xff]))`)).message).toContain(
|
||||
"not valid utf-8",
|
||||
)
|
||||
expect((await error(`new TextDecoder().decode("text")`)).message).toContain(
|
||||
"expects a Uint8Array, received a string",
|
||||
)
|
||||
expect((await error(`TextDecoder()`)).message).toContain("requires 'new'")
|
||||
})
|
||||
|
||||
test("crypto.getRandomValues fills the given bytes in place", async () => {
|
||||
expect(
|
||||
await value(
|
||||
`const b = new Uint8Array(16); const same = crypto.getRandomValues(b) === b; return [same, b.length, b.some ? 0 : Array.from(b).some((n) => n !== 0)]`,
|
||||
),
|
||||
).toEqual([true, 16, true])
|
||||
expect((await error(`crypto.getRandomValues([1])`)).message).toContain("expects a Uint8Array, received an array")
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdlib integration", () => {
|
||||
test("constructor follows own keys, shadowing, writes, and new", async () => {
|
||||
expect(
|
||||
|
||||
@@ -5,19 +5,11 @@ import path from "node:path"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { materialize } from "../../src/interpreter/errors.js"
|
||||
import { executeProgram } from "../../src/interpreter/execute.js"
|
||||
import type { Host } from "../../src/interpreter/globals.js"
|
||||
import { ProgramThrow } from "../../src/interpreter/model.js"
|
||||
import type { Interpreter } from "../../src/interpreter/interpreter.js"
|
||||
import { Throw } from "../../src/interpreter/model.js"
|
||||
import { createErrorValue } from "../../src/interpreter/intrinsics.js"
|
||||
import { constructor, fn, methods } from "../../src/interpreter/native.js"
|
||||
import {
|
||||
Callable,
|
||||
define,
|
||||
get,
|
||||
hidden,
|
||||
ProgramArray,
|
||||
ProgramFunction,
|
||||
ProgramObject,
|
||||
} from "../../src/interpreter/objects.js"
|
||||
import { Callable, define, get, hidden, Arr, Fn, Obj } from "../../src/interpreter/objects.js"
|
||||
import { ToolRuntime } from "../../src/tool-runtime.js"
|
||||
|
||||
export const root = import.meta.dir
|
||||
@@ -50,8 +42,8 @@ export const run = async (file: string): Promise<Outcome> => {
|
||||
// work instead, so drain explicitly.
|
||||
const drain = meta.flags?.includes("async") ? "\nfor (let i = 0; i < 100; i++) await null" : ""
|
||||
const result = await Effect.runPromise(
|
||||
executeProgram(`"use strict";\n${source}${drain}`, prepared, limits, {}, (host) =>
|
||||
harness(host, (error) => {
|
||||
executeProgram(`"use strict";\n${source}${drain}`, prepared, limits, {}, (ctx) =>
|
||||
harness(ctx, (error) => {
|
||||
done ??= { error }
|
||||
}),
|
||||
),
|
||||
@@ -72,33 +64,36 @@ export const run = async (file: string): Promise<Outcome> => {
|
||||
return { status: "pass" }
|
||||
}
|
||||
|
||||
const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyArray<readonly [string, unknown]> => {
|
||||
const protos = host.runner.prototypes
|
||||
const test262Prototype = new ProgramObject(protos.Object)
|
||||
const harness = <R>(
|
||||
ctx: Interpreter<R>,
|
||||
onDone: (error: unknown) => void,
|
||||
): ReadonlyArray<readonly [string, unknown]> => {
|
||||
const builtins = ctx.builtins
|
||||
const test262Prototype = new Obj(builtins.Object)
|
||||
define(test262Prototype, "name", "Test262Error", hidden)
|
||||
const test262 = (args: Array<unknown>) =>
|
||||
createErrorValue(test262Prototype, args[0] === undefined ? "" : String(args[0]))
|
||||
const fail = (message: string) => Effect.fail(new ProgramThrow(test262([message])))
|
||||
const fail = (message: string) => Effect.fail(new Throw(test262([message])))
|
||||
const prefix = (message: unknown) => (message === undefined ? "" : `${String(message)} `)
|
||||
const compare = (a: unknown, b: unknown) =>
|
||||
a instanceof ProgramArray &&
|
||||
b instanceof ProgramArray &&
|
||||
a instanceof Arr &&
|
||||
b instanceof Arr &&
|
||||
a.items.length === b.items.length &&
|
||||
a.items.every((value, i) => Object.is(value, b.items[i]))
|
||||
const test262Error = constructor<R>(protos, test262Prototype, {
|
||||
const test262Error = constructor<R>(builtins, test262Prototype, {
|
||||
name: "Test262Error",
|
||||
call: (_, args) => Effect.succeed(test262(args)),
|
||||
construct: (args) => Effect.succeed(test262(args)),
|
||||
})
|
||||
methods(protos, test262Error, [["thrower", 1, (_, args) => fail(String(args[0]))]])
|
||||
const compareArray = fn<R>(protos, "compareArray", 2, (_, args) => compare(args[0], args[1]))
|
||||
methods(protos, compareArray, [["format", 1, (_, args) => show(args[0])]])
|
||||
const assert = fn<R>(protos, "assert", 2, (_, args) =>
|
||||
methods(builtins, test262Error, [["thrower", 1, (_, args) => fail(String(args[0]))]])
|
||||
const compareArray = fn<R>(builtins, "compareArray", 2, (_, args) => compare(args[0], args[1]))
|
||||
methods(builtins, compareArray, [["format", 1, (_, args) => show(args[0])]])
|
||||
const assert = fn<R>(builtins, "assert", 2, (_, args) =>
|
||||
args[0] === true
|
||||
? Effect.void
|
||||
: fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])),
|
||||
)
|
||||
methods(protos, assert, [
|
||||
methods(builtins, assert, [
|
||||
[
|
||||
"sameValue",
|
||||
3,
|
||||
@@ -130,12 +125,12 @@ const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyAr
|
||||
3,
|
||||
(_, args) => {
|
||||
const expected = args[0] instanceof Callable ? String(get(args[0], "name")) : show(args[0])
|
||||
return host.runner.invokeCallable(args[1], undefined, []).pipe(
|
||||
return ctx.call(args[1], undefined, []).pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onFailure: (cause) => {
|
||||
if (cause.reasons.some(Cause.isInterruptReason)) return Effect.failCause(cause)
|
||||
const thrown = materialize(host.runner, Cause.squash(cause))
|
||||
if (!(thrown instanceof ProgramObject)) return fail(`${prefix(args[2])}Thrown value was not an object!`)
|
||||
const thrown = materialize(ctx, Cause.squash(cause))
|
||||
if (!(thrown instanceof Obj)) return fail(`${prefix(args[2])}Thrown value was not an object!`)
|
||||
const actual = get(thrown, "constructor")
|
||||
if (actual === args[0]) return Effect.void
|
||||
return fail(`${prefix(args[2])}Expected a ${expected} but got a ${show(actual)}`)
|
||||
@@ -151,11 +146,11 @@ const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyAr
|
||||
["assert", assert],
|
||||
["compareArray", compareArray],
|
||||
["Test262Error", test262Error],
|
||||
["$DONE", fn<R>(protos, "$DONE", 1, (_, args) => onDone(args[0]))],
|
||||
["$DONE", fn<R>(builtins, "$DONE", 1, (_, args) => onDone(args[0]))],
|
||||
[
|
||||
"$DONOTEVALUATE",
|
||||
fn<R>(protos, "$DONOTEVALUATE", 0, () =>
|
||||
Effect.fail(new ProgramThrow("Test262: This statement should not be evaluated.")),
|
||||
fn<R>(builtins, "$DONOTEVALUATE", 0, () =>
|
||||
Effect.fail(new Throw("Test262: This statement should not be evaluated.")),
|
||||
),
|
||||
],
|
||||
]
|
||||
@@ -164,10 +159,10 @@ const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyAr
|
||||
const show = (value: unknown): string => {
|
||||
if (typeof value === "string") return JSON.stringify(value)
|
||||
if (Object.is(value, -0)) return "-0"
|
||||
if (value instanceof ProgramArray) return `[${value.items.map(show).join(", ")}]`
|
||||
if (value instanceof ProgramFunction) return "program function"
|
||||
if (value instanceof Arr) return `[${value.items.map(show).join(", ")}]`
|
||||
if (value instanceof Fn) return "program function"
|
||||
if (value instanceof Callable) return String(get(value, "name"))
|
||||
if (!(value instanceof ProgramObject)) return String(value)
|
||||
if (!(value instanceof Obj)) return String(value)
|
||||
const message = get(value, "message")
|
||||
return typeof message === "string" ? `${String(get(value, "name") ?? "object")}: ${message}` : "object"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user