diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index a02c9a0dd4c..d25ad9d5548 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -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 diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index f3c6159c1cb..0e17af69044 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -148,6 +148,6 @@ export const make = = {}>( } 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)), } } diff --git a/packages/codemode/src/data.ts b/packages/codemode/src/data.ts index 50d572d5901..20ae0aeb82a 100644 --- a/packages/codemode/src/data.ts +++ b/packages/codemode/src/data.ts @@ -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, - 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 = {} // 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 diff --git a/packages/codemode/src/interpreter/runner.ts b/packages/codemode/src/interpreter/callback.ts similarity index 63% rename from packages/codemode/src/interpreter/runner.ts rename to packages/codemode/src/interpreter/callback.ts index 93003e55500..4cb17e21942 100644 --- a/packages/codemode/src/interpreter/runner.ts +++ b/packages/codemode/src/interpreter/callback.ts @@ -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 = { @@ -10,18 +10,6 @@ export type IteratorCursor = { readonly close: Effect.Effect } -/** Everything a native function needs from the realm: calling back into the program and its intrinsic objects. */ -export type Runner = { - readonly invokeCallable: ( - callable: unknown, - thisValue: unknown, - args: Array, - ) => Effect.Effect - readonly settlePromise: (promise: ProgramPromise) => Effect.Effect - readonly syncIterator: (value: unknown) => Effect.Effect | undefined, unknown, R> - readonly prototypes: Prototypes -} - export const preserveConsumerError = ( cursor: IteratorCursor, effect: Effect.Effect, @@ -37,39 +25,39 @@ export const preserveConsumerError = ( * default hint as "string", like their `Symbol.toPrimitive`. */ export const toPrimitive = ( - runner: Runner, + ctx: Interpreter, value: unknown, hint: "number" | "string" | "default", ): Effect.Effect => { - 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 = (runner: Runner, value: unknown) => - Effect.map(toPrimitive(runner, value, "string"), coerceToString) +export const toPrimitiveString = (ctx: Interpreter, value: unknown) => + Effect.map(toPrimitive(ctx, value, "string"), coerceToString) -export const toPrimitiveNumber = (runner: Runner, value: unknown) => - Effect.map(toPrimitive(runner, value, "number"), coerceToNumber) +export const toPrimitiveNumber = (ctx: Interpreter, 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 = ( - runner: Runner, + ctx: Interpreter, callback: unknown, name: string, ): ((args: Array) => Effect.Effect) => { @@ -81,5 +69,5 @@ export const applyCollectionCallback = ( } throw typeError(`${name} expects a function callback.`) } - return (callbackArgs) => runner.invokeCallable(callback, undefined, callbackArgs) + return (callbackArgs) => ctx.call(callback, undefined, callbackArgs) } diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts index 29cfff1499d..23c35f5a2ff 100644 --- a/packages/codemode/src/interpreter/errors.ts +++ b/packages/codemode/src/interpreter/errors.ts @@ -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 = (runner: Runner, thrown: unknown): unknown => { - if (thrown instanceof ProgramThrow) return thrown.value - const prototypes = runner.prototypes +export const materialize = (ctx: Interpreter, 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 = ( - runner: Runner, + ctx: Interpreter, errors: Array, 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 = ( - runner: Runner, + ctx: Interpreter, args: Array, - proto: ProgramObject, -): Effect.Effect => + proto: Obj, +): Effect.Effect => 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 = [] 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 = (type: ErrorType, runner: Runner) => { - const protos = runner.prototypes - const prototype = protos[type] +export const errorGlobal = (type: ErrorType, ctx: Interpreter) => { + const builtins = ctx.builtins + const prototype = builtins[type] const construct = (args: Array, 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 = constructor(protos, prototype, { + const ctor: Native = constructor(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 diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts index e0c5368730a..23e1ea0c662 100644 --- a/packages/codemode/src/interpreter/execute.ts +++ b/packages/codemode/src/interpreter/execute.ts @@ -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 = ( code: string, prepared: ToolRuntime.Prepared, limits: ResolvedExecutionLimits, hooks: ToolRuntime.ToolCallHooks, - extraGlobals?: (host: Host) => ReadonlyArray, + globals?: (ctx: Interpreter) => ReadonlyArray, ): Effect.Effect => { if (code.trim().length === 0) { return Effect.succeed({ @@ -30,31 +29,23 @@ export const executeProgram = ( // 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 = [] 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 } | undefined + let returned: { value: DataValue; pending: Pending } | undefined const base = Effect.acquireUseRelease( Scope.make("parallel"), (scope) => Effect.gen(function* () { const program = parseProgram(code) - const promises = new PromiseRuntime(scope, prototypes.Promise) - const value = yield* new Runtime( - tools.execute, - tools.search, - tools.keys, - promises, - prototypes, - logs, - extraGlobals, - ).run(program) + const pending = new Pending(scope, builtins.Promise) + const value = yield* new Interpreter({ 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 = ( 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, diff --git a/packages/codemode/src/interpreter/extensions.ts b/packages/codemode/src/interpreter/extensions.ts index 267dd976b51..496750b96db 100644 --- a/packages/codemode/src/interpreter/extensions.ts +++ b/packages/codemode/src/interpreter/extensions.ts @@ -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 = ( - host: Host, + ctx: Interpreter, extensions: ReadonlyArray, ): ReadonlyArray => { - 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() - const exposed = new Map; proto: ProgramObject }>() + const protoOf = new Map() + const exposed = new Map; proto: Obj }>() const classOf = new Map() - const handles = new WeakMap() + const handles = new WeakMap() const toHost = (value: unknown, label: string, depth = 0, seen = new Set()): 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 = ( 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 = ( 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 = ( 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 = ( 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 = ( // 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 => { - 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 = ( 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 = ( // 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, @@ -194,7 +198,7 @@ export const extensionGlobals = ( const converted = args(values, name) return invoke(() => method.apply(self, converted), name) } - define(target, key, fn(protos, key, method.length, impl), hidden) + define(target, key, fn(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 = ( } } - const expose = (cls: Class): { ctor: NativeFunction; proto: ProgramObject } => { + const expose = (cls: Class): { ctor: Native; 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(protos, proto, { + const ctor = constructor(builtins, proto, { name, length: cls.length, call: (_, values) => { @@ -260,7 +264,7 @@ export const extensionGlobals = ( 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 = ( const converted = args(values, name) return invoke(() => value.apply(undefined, converted), name) } - return [name, fn(protos, name, value.length, impl)] as const + return [name, fn(builtins, name, value.length, impl)] as const }), ) } diff --git a/packages/codemode/src/interpreter/generators.ts b/packages/codemode/src/interpreter/generators.ts index 8dee3cc1965..d33b164ee6a 100644 --- a/packages/codemode/src/interpreter/generators.ts +++ b/packages/codemode/src/interpreter/generators.ts @@ -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 = (runner: Runner, promises: PromiseRuntime): void => { - const protos = runner.prototypes +export const generatorGlobals = (ctx: Interpreter): 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) => { - 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 - 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, ) } diff --git a/packages/codemode/src/interpreter/globals.ts b/packages/codemode/src/interpreter/globals.ts index e0d51c0888f..4c1f0c132d3 100644 --- a/packages/codemode/src/interpreter/globals.ts +++ b/packages/codemode/src/interpreter/globals.ts @@ -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 = { - readonly runner: Runner - readonly promises: PromiseRuntime - readonly search: (args: Array) => Effect.Effect - readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray - readonly logs: Array -} +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 = (runner: Runner) => { +const functionGlobal = (ctx: Interpreter) => { const reject = () => Effect.sync(() => { throw typeError("The Function constructor is not supported; write the function inline.") }) - return constructor(runner.prototypes, runner.prototypes.Function, { + return constructor(ctx.builtins, ctx.builtins.Function, { name: "Function", length: 1, call: reject, @@ -44,8 +36,8 @@ const functionGlobal = (runner: Runner) => { }) } -const symbolGlobal = (runner: Runner) => { - const symbol = native(runner.prototypes, { +const symbolGlobal = (ctx: Interpreter) => { + const symbol = native(ctx.builtins, { name: "Symbol", call: () => Effect.sync(() => { @@ -57,52 +49,54 @@ const symbolGlobal = (runner: Runner) => { return symbol } -type Factory = (host: Host) => unknown +type Factory = (ctx: Interpreter) => unknown // A table rather than a list so the names are known before any runtime exists. const table: Record = { 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, (host: Host) => 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, (ctx: Interpreter) => errorGlobal(type, ctx)])), } /** Names bound in every program before extensions apply. */ export const globalNames: ReadonlySet = new Set(Object.keys(table)) /** The immutable global bindings of every program, in declaration order. */ -export const globals = (host: Host): ReadonlyArray => { - generatorGlobals(host.runner, host.promises) - return Object.entries(table).map(([name, factory]) => [name, factory(host)] as const) +export const globals = (ctx: Interpreter): ReadonlyArray => { + generatorGlobals(ctx) + return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const) } diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/interpreter.ts similarity index 89% rename from packages/codemode/src/interpreter/runtime.ts rename to packages/codemode/src/interpreter/interpreter.ts index ba835295ca7..d40accdb1f7 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/interpreter.ts @@ -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 { - readonly runner: Runner +export class Interpreter { + readonly tools: ToolRuntime + readonly pending: Pending + readonly builtins: Builtins + readonly logs: Array private readonly root: Frame - constructor( - readonly executeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, - readonly search: (args: Array) => Effect.Effect, - readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray, - readonly promises: PromiseRuntime, - readonly prototypes: Prototypes, - readonly logs: Array = [], - extraGlobals: (host: Host) => ReadonlyArray = () => [], - ) { + constructor(options: { + readonly tools: ToolRuntime + readonly pending: Pending + readonly builtins: Builtins + readonly logs?: Array + readonly globals?: (ctx: Interpreter) => ReadonlyArray + }) { + this.tools = options.tools + this.pending = options.pending + this.builtins = options.builtins + this.logs = options.logs ?? [] const globalScope = new Map() // 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 { run(program: Program): Effect.Effect { return this.root.run(program) } + + call(callable: unknown, thisValue: unknown, args: Array): Effect.Effect { + return this.root.call(callable, thisValue, args) + } + + await(promise: PromiseObj): Effect.Effect { + 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 { private generatorAsync = false constructor( - private readonly runtime: Runtime, + private readonly ctx: Interpreter, 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 { } // 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 { private createToolCallPromise( path: ReadonlyArray, args: Array, - ): Effect.Effect { - return this.runtime.promises.create(Effect.suspend(() => this.runtime.executeTool(path, args))) + ): Effect.Effect { + 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 { - const promises = this.runtime.promises + await(promise: PromiseObj): Effect.Effect { + 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 { 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 { 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 { } private awaitValue(value: unknown): Effect.Effect { - 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 { }) } - 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 { } 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 { 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 { 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 { }) } - 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 { // for...in over null/undefined iterates nothing, like JS. private enumerableKeys(value: unknown, node: AstNode): Array { - 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 { } private evaluateThrowStatement(node: ThrowStatement): Effect.Effect { - 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 { @@ -966,7 +980,7 @@ class Frame { 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 { } 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 { const consumed = new Set() 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 { } 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 { const consumed = new Set() 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 { ): Effect.Effect { 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 { 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 { 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 { 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 { 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).construct : undefined + const construct = callee instanceof Native ? (callee as Native).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), node) + return yield* self.native(() => construct(args, callee as Native), node) }) } @@ -1292,7 +1306,7 @@ class Frame { 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 { 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 { // 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 { 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 { 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 { 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 { 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 { 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 { 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 { } // The single dispatch for every invocation: call expressions and callbacks share it. - invokeCallable( + call( callable: unknown, thisValue: unknown, args: Array, @@ -1580,9 +1594,9 @@ class Frame { } 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).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).call(thisValue, args), node) } throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node) }) @@ -1606,7 +1620,7 @@ class Frame { 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 { } // 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, node?: AstNode): Effect.Effect { + invokeFunction(fn: Fn, args: Array, node?: AstNode): Effect.Effect { 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 { 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 { }) 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 { invocation: Frame, run: Effect.Effect, 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() } if (!asynchronous && state.active) return Effect.die(typeError("Generator is already running.")) @@ -1687,7 +1701,7 @@ class Frame { 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 { ) } 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 { 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 { private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect { 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 { private suspendGenerator(value: unknown, node: AstNode): Effect.Effect { 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 { 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 { 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 { 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 { 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 { }) } - private evaluateObjectExpression(node: ObjectExpression): Effect.Effect { - const objectValue = new ProgramObject(this.runtime.prototypes.Object) + private evaluateObjectExpression(node: ObjectExpression): Effect.Effect { + 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 { }) } - private evaluateArrayExpression(node: ArrayExpression): Effect.Effect { + private evaluateArrayExpression(node: ArrayExpression): Effect.Effect { const values: Array = [] const self = this @@ -1964,7 +1976,7 @@ class Frame { } 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 { 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 { 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 { 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 { 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 { } // 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 { }) } - 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 { } })() 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) } diff --git a/packages/codemode/src/interpreter/intrinsics.ts b/packages/codemode/src/interpreter/intrinsics.ts index 95de313ff81..36835fed6df 100644 --- a/packages/codemode/src/interpreter/intrinsics.ts +++ b/packages/codemode/src/interpreter/intrinsics.ts @@ -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> +export type Builtins = Readonly> -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"), diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index 8c29b942200..83334c9a203 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -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. */ diff --git a/packages/codemode/src/interpreter/native.ts b/packages/codemode/src/interpreter/native.ts index f6157612209..5d11d6ceff5 100644 --- a/packages/codemode/src/interpreter/native.ts +++ b/packages/codemode/src/interpreter/native.ts @@ -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 -// 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 = (impl: Impl) => (thisValue: unknown, args: Array): Effect.Effect => { @@ -15,29 +15,25 @@ const lift = return Effect.isEffect(result) ? (result as Effect.Effect) : Effect.succeed(result) } -export const native = (protos: Prototypes, options: NativeOptions): NativeFunction => - new NativeFunction(protos.Function, options) +export const native = (builtins: Builtins, options: NativeOptions): Native => + new Native(builtins.Function, options) -export const fn = (protos: Prototypes, name: string, length: number, impl: Impl): NativeFunction => - native(protos, { name, length, call: lift(impl) }) +export const fn = (builtins: Builtins, name: string, length: number, impl: Impl): Native => + native(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): 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): void => { + for (const [name, length, impl] of table) define(target, name, fn(builtins, name, length, impl), hidden) } -export const constants = (target: ProgramObject, table: Record): void => { +export const constants = (target: Obj, table: Record): 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 = ( - protos: Prototypes, - proto: ProgramObject, - options: NativeOptions, -): NativeFunction => { - const ctor = native(protos, options) +export const constructor = (builtins: Builtins, proto: Obj, options: NativeOptions): Native => { + const ctor = native(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 { +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 = ( +export const receiver = ( cls: abstract new (...args: never) => T, thisValue: unknown, method: string, diff --git a/packages/codemode/src/interpreter/objects.ts b/packages/codemode/src/interpreter/objects.ts index 13cd9f4e84a..58c20d21632 100644 --- a/packages/codemode/src/interpreter/objects.ts +++ b/packages/codemode/src/interpreter/objects.ts @@ -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() - 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 = [], ) { 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, readonly body: BlockStatement | Expression, @@ -90,12 +90,12 @@ export type NativeOptions = { readonly callback?: boolean } -export class NativeFunction extends Callable { +export class Native extends Callable { readonly call: NativeCall readonly construct: NativeConstruct | undefined readonly callback: boolean - constructor(proto: ProgramObject, options: NativeOptions) { + constructor(proto: Obj, options: NativeOptions) { super(proto, options.name, options.length ?? 0) this.call = options.call this.construct = options.construct @@ -103,18 +103,18 @@ export class NativeFunction extends Callable { } } -export class ProgramPromise extends ProgramObject { +export class PromiseObj extends Obj { constructor( - proto: ProgramObject, + proto: Obj, readonly fiber: Fiber.Fiber, ) { 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, ) { @@ -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() } -export class ProgramSet extends ProgramObject { +export class SetObj extends Obj { readonly set = new Set() } -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 | 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 => { +export const ownKeys = (target: Obj): Array => { 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 => +export const enumerableKeys = (target: Obj): Array => 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 => +export const keys = (target: Obj): Array => 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): ProgramObject => { - const target = new ProgramObject(proto) +export const record = (proto: Obj, fields: Record): 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): void => { +export const assign = (target: Obj, source: Obj, skip?: ReadonlySet): void => { for (const key of enumerableKeys(source)) { if (skip?.has(key)) continue set(target, key, getOwn(source, key)) diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts index 1d79d84c0c6..4850af97fa9 100644 --- a/packages/codemode/src/interpreter/promises.ts +++ b/packages/codemode/src/interpreter/promises.ts @@ -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 = (runner: Runner, name: string, settle: (value: unknown) => void) => - fn(runner.prototypes, name, 1, (_, args) => { +const capability = (ctx: Interpreter, 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 { - private readonly active = new Set() - private readonly ids = new WeakMap() - private readonly observed = new WeakSet() +export class Pending { + private readonly active = new Set() + private readonly ids = new WeakMap() + private readonly observed = new WeakSet() private readonly failures = new Map() 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, - ): Effect.Effect { - const self: { promise?: ProgramPromise } = {} + body: (self: { promise?: PromiseObj }) => Effect.Effect, + ): Effect.Effect { + const self: { promise?: PromiseObj } = {} return Effect.map(this.create(body(self)), (promise) => { self.promise = promise return promise }) } - create(effect: Effect.Effect): Effect.Effect { + create(effect: Effect.Effect): Effect.Effect { return Effect.flatMap(CallSite, (site) => { if (this.active.size >= MAX_PENDING_PROMISES) { throw rangeError( @@ -60,7 +51,7 @@ export class PromiseRuntime { 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 { } // 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> { + await(promise: PromiseObj): Effect.Effect> { return Fiber.await(promise.fiber) } @@ -113,15 +104,15 @@ export class PromiseRuntime { } export const resolvePromiseValue = ( - runner: Runner, + ctx: Interpreter, value: unknown, - own?: { promise?: ProgramPromise }, + own?: { promise?: PromiseObj }, ): Effect.Effect => { 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 = ( // Promise resolution invokes a thenable's method in a later job. yield* Effect.yieldNow const deferred = Deferred.makeUnsafe() - 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 = ( - runner: Runner, - promises: PromiseRuntime, - value: unknown, -): Effect.Effect => { - if (value instanceof ProgramPromise) return Effect.succeed(value) - return promises.createWithSelf((self) => resolvePromiseValue(runner, value, self)) +export const resolvePromise = (ctx: Interpreter, value: unknown): Effect.Effect => { + 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 = ( - runner: Runner, - promises: PromiseRuntime, + ctx: Interpreter, name: (typeof promiseStatics)[number], args: Array, ): Effect.Effect => { 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 = [] + const items: Array = [] 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 = ( if (name === "allSettled") { const outcomes: Array = [] 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 = ( } const instanceMethod = ( - runner: Runner, - promises: PromiseRuntime, + ctx: Interpreter, name: "then" | "catch" | "finally", thisValue: unknown, args: Array, -): Effect.Effect => { +): Effect.Effect => { 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 = ( - runner: Runner, - promises: PromiseRuntime, - executor: unknown, -): Effect.Effect => { - if (!(executor instanceof ProgramFunction)) { +const constructPromise = (ctx: Interpreter, executor: unknown): Effect.Effect => { + 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() - 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 = ( - promises: PromiseRuntime, - source: ProgramPromise, + ctx: Interpreter, + source: PromiseObj, ): Effect.Effect, 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 = ( - runner: Runner, - promises: PromiseRuntime, - source: ProgramPromise, + ctx: Interpreter, + source: PromiseObj, onFulfilled: Callable | undefined, onRejected: Callable | undefined, method: string, -): Effect.Effect => { - return promises.createWithSelf((self) => +): Effect.Effect => { + 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 = ( - runner: Runner, - promises: PromiseRuntime, - source: ProgramPromise, + ctx: Interpreter, + source: PromiseObj, cleanup: Callable | undefined, method: string, -): Effect.Effect => - promises.create( +): Effect.Effect => + 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 = (runner: Runner, promises: PromiseRuntime) => { - const protos = runner.prototypes - const proto = protos.Promise - const promise = constructor(protos, proto, { +export const promiseGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Promise + const promise = constructor(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(protos, { + native(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 } diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index 8ed973f5c9d..6466a227147 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -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}` diff --git a/packages/codemode/src/openapi/types.ts b/packages/codemode/src/openapi/types.ts index d1d4e5b49a5..79fdb7fbff0 100644 --- a/packages/codemode/src/openapi/types.ts +++ b/packages/codemode/src/openapi/types.ts @@ -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 /** The operation identity handed to auth resolution and errors. */ diff --git a/packages/codemode/src/stdlib/array.ts b/packages/codemode/src/stdlib/array.ts index 468e05d566b..8e495311441 100644 --- a/packages/codemode/src/stdlib/array.ts +++ b/packages/codemode/src/stdlib/array.ts @@ -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 = (runner: Runner, args: Array): Effect.Effect => { +const arrayFrom = (ctx: Interpreter, args: Array): Effect.Effect => { 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 = (runner: Runner, args: Array): Effect.Effect = [] 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 = (runner: Runner, args: Array): Effect.Effect( - runner: Runner, + ctx: Interpreter, target: Array, comparator: unknown, name: string, @@ -59,7 +60,7 @@ export const sortArray = ( 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): Effect.Effect, unknown, R> => { if (items.length <= 1) return Effect.succeed(items) const midpoint = Math.floor(items.length / 2) @@ -84,31 +85,31 @@ export const sortArray = ( } // Array constructs identically with or without new, like JS. -export const arrayGlobal = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.Array - const wrap = (items: Array) => new ProgramArray(proto, items) - const construct = (args: Array, into: ProgramObject): ProgramArray => { - if (args.length !== 1) return new ProgramArray(into, [...args]) +export const arrayGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Array + const wrap = (items: Array) => new Arr(proto, items) + const construct = (args: Array, 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(protos, proto, { + const array = constructor(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 = (runner: Runner) => { length: number, body: ( target: Array, - receiver: ProgramArray, + receiver: Arr, apply: (args: Array) => Effect.Effect, args: Array, ) => Effect.Effect, @@ -131,11 +132,11 @@ export const arrayGlobal = (runner: Runner) => { 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 = (runner: Runner) => { 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 = (runner: Runner) => { 0, (thisValue, args) => { const flatten = (items: Array, depth: number): Array => - 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 = (runner: Runner) => { 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 = (runner: Runner) => { "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 = (runner: Runner) => { 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) diff --git a/packages/codemode/src/stdlib/bytes.ts b/packages/codemode/src/stdlib/bytes.ts new file mode 100644 index 00000000000..3a69e08dfe2 --- /dev/null +++ b/packages/codemode/src/stdlib/bytes.ts @@ -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 = (ctx: Interpreter, source: unknown, name: string): Effect.Effect => { + 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 = [] + while (true) { + const step = yield* cursor.next + if (step.done) return Uint8Array.from(values) + values.push(coerceToNumber(step.value)) + checkArrayLength(values.length) + } + }) +} + +const constructBytes = (ctx: Interpreter, args: Array, 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 = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Uint8Array + const wrap = (bytes: Uint8Array) => new Bytes(proto, bytes) + const uint8Array = constructor(builtins, proto, { + name: "Uint8Array", + length: 3, + call: requiresNew("Uint8Array"), + construct: (args, newTarget) => constructBytes(ctx, args, prototypeFrom(newTarget, proto)), + }) + const decode = (name: string, args: Array, from: (text: string) => Uint8Array) => { + if (typeof args[0] !== "string") throw typeError(`Uint8Array.${name} expects a string.`) + 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) => 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 = (ctx: Interpreter) => { + 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(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 = (ctx: Interpreter) => { + 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(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") }), + ) + }), + }) +} diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index 5b72e7d45c5..656c732723f 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -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 = (runner: Runner, value: unknown): Effect.Effect => { - if (value instanceof ProgramPromise) return Effect.succeed("[object Promise]") +const coerceGroupByPropertyKey = (ctx: Interpreter, value: unknown): Effect.Effect => { + 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 = (runner: Runner, namespace: "Map" | "Object") => - fn(runner.prototypes, "groupBy", 2, (_, args) => { - const protos = runner.prototypes +export const groupBy = (ctx: Interpreter, namespace: "Map" | "Object") => + fn(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 = (runner: Runner, 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 = (runner: Runner, 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 = (runner: Runner, init: unknown, proto: ProgramObject) => { - const target = new ProgramMap(proto) +const constructMap = (ctx: Interpreter, 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 = (runner: Runner, 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 = (runner: Runner, init: unknown, proto: ProgramObject) }) } -const constructSet = (runner: Runner, init: unknown, proto: ProgramObject) => { - const target = new ProgramSet(proto) +const constructSet = (ctx: Interpreter, 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 = (runner: Runner, init: unknown, proto: ProgramObject) }) } -export const mapGlobal = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.Map - const map = constructor(protos, proto, { +export const mapGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Map + const map = constructor(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) => 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) => 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 = (runner: Runner) => { 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 = { } const loadSetRecord = ( - runner: Runner, + ctx: Interpreter, source: unknown, name: string, ): Effect.Effect, 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 = ( } 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 = ( } const setOperation = ( - runner: Runner, - target: ProgramSet, + ctx: Interpreter, + target: SetObj, name: string, source: unknown, ): Effect.Effect => 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 = ( 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 = ( return true }) -export const setGlobal = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.Set - const set = constructor(protos, proto, { +export const setGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Set + const set = constructor(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) => new ProgramArray(protos.Array, items) + const self = (thisValue: unknown, name: string) => receiver(SetObj, thisValue, `Set.prototype.${name}`) + const wrap = (items: Array) => 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 = (runner: Runner) => { 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 diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts index cbfc04cba69..c6da2fcfe4a 100644 --- a/packages/codemode/src/stdlib/console.ts +++ b/packages/codemode/src/stdlib/console.ts @@ -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 = (runner: Runner, logs: Array) => { - const protos = runner.prototypes - const console = new ProgramObject(protos.Object) +export const consoleGlobal = (ctx: Interpreter) => { + 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 = (runner: Runner, logs: Array) => { const MAX_CONSOLE_DEPTH = 32 -const formatConsoleMessage = (protos: Prototypes, name: string, args: Array): string => { +const formatConsoleMessage = (builtins: Builtins, name: string, args: Array): 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, 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, 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, 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, depth: number): s const formatItems = (items: Array, seen: Set, 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 | undefined, ): Array<{ readonly index: string; readonly values: Record }> => { - 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 | undefined): Record => { - 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)) } diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index fa65c31b74b..699924c8067 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -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 = (runner: Runner, args: Array, proto: ProgramObject) => { - if (args.length === 0) return Effect.succeed(new ProgramDate(proto, Date.now())) +const constructDate = (ctx: Interpreter, args: Array, 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 = [ ["setUTCFullYear", 3], ] -export const dateGlobal = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.Date - const date = constructor(protos, proto, { +export const dateGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.Date + const date = constructor(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))], ]) - 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 = (runner: Runner) => { // 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) => { diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index 7983bbb349d..17091e18e38 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -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 = (runner: Runner) => { - 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 = (ctx: Interpreter) => { + 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 = (runner: Runner, args: Array): Effect.Effect => { +const parse = (ctx: Interpreter, args: Array): Effect.Effect => { 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 => + const apply = applyCollectionCallback(ctx, args[1], "JSON.parse") + const visit = (holder: Obj, key: string): Effect.Effect => 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 = (runner: Runner, args: Array): Effect.Effect(runner: Runner, args: Array): Effect.Effect => { +const stringify = (ctx: Interpreter, args: Array): Effect.Effect => { 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 = (runner: Runner, args: Array): Effect.Effect() - const visit = (holder: ProgramObject, key: string): Effect.Effect => + const visit = (holder: Obj, key: string): Effect.Effect => 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 = [] for (let index = 0; index < value.items.length; index += 1) { result.push((yield* visit(value, String(index))) ?? null) @@ -94,14 +95,14 @@ const stringify = (runner: Runner, args: Array): Effect.Effect + 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 = (runner: Runner, value: unknown, key: string) => { - if (!(value instanceof ProgramObject)) return Effect.succeed(value) +const toJSONValue = (ctx: Interpreter, 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) } diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 251c9763413..aad3190c37d 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -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): Metho ), ] -export const mathGlobal = (runner: Runner) => { - const protos = runner.prototypes - const math = new ProgramObject(protos.Object) +export const mathGlobal = (ctx: Interpreter) => { + 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 = (runner: Runner) => { 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 = (runner: Runner) => { 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.") } diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts index a3fb880bbd8..eb737e07d8f 100644 --- a/packages/codemode/src/stdlib/number.ts +++ b/packages/codemode/src/stdlib/number.ts @@ -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 = (runner: Runner) => { - const protos = runner.prototypes - const number = constructor(protos, protos.Number, { +export const numberGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const number = constructor(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 = (runner: Runner) => { 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 = (runner: Runner) => { 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 = (runner: Runner) => { return number } -export const booleanGlobal = (runner: Runner) => { - const protos = runner.prototypes - const boolean = constructor(protos, protos.Boolean, { +export const booleanGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const boolean = constructor(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")], ]) diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index fa8bdaf57df..0a6777673fa 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -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 = ( - runner: Runner, - label: string, - value: unknown, - node?: AstNode, -): ProgramObject => { +export const enumerableSource = (ctx: Interpreter, 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 = ( 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 = (runner: Runner, args: Array): unknown => { +export const objectAssign = (ctx: Interpreter, args: Array): 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() 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 = (runner: Runner, args: Array): unknow return target } -const objectFromEntries = (runner: Runner, source: unknown): Effect.Effect => { - const out = new ProgramObject(runner.prototypes.Object) +const objectFromEntries = (ctx: Interpreter, source: unknown): Effect.Effect => { + 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 = (runner: Runner, 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 = (runner: Runner, 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 = ( - runner: Runner, - toolKeys: (path: ReadonlyArray) => ReadonlyArray, -) => { - const protos = runner.prototypes +export const objectGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins const construct = (args: Array): 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(protos, protos.Object, { + const object = constructor(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 = ( "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 = ( 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)}]`], diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index fbd10798b32..75f76fc4dcf 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -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, - proto: ProgramObject = protos.RegExp, -): ProgramRegExp => { +export const constructRegExp = (builtins: Builtins, args: Array, 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 = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.RegExp - const regexp = constructor(protos, proto, { +export const regexpGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.RegExp + const regexp = constructor(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 = (runner: Runner) => { ], ]) - 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 = (runner: Runner) => { 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 = (runner: Runner) => { 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 } diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index 1cfeaef033e..cb2e3d22b8f 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -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 = ( - runner: Runner, + ctx: Interpreter, value: string, name: "replace" | "replaceAll", args: Array, ): Effect.Effect => { - 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 }> = [] const collect = (...callbackArgs: Array): string => { const match = callbackArgs[0] @@ -42,13 +43,13 @@ const replaceWithCallback = ( 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) + if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(builtins.Object, groups as Record) 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 = ( 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 = ( }) } -export const stringGlobal = (runner: Runner) => { - const protos = runner.prototypes - const string = constructor(protos, protos.String, { +export const stringGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const string = constructor(builtins, builtins.String, { name: "String", length: 1, - call: coercion(runner, "String").call, + call: coercion(ctx, "String").call, }) const codeUnits = (name: string, op: (...codes: Array) => string): Method => [ name, @@ -96,7 +97,7 @@ export const stringGlobal = (runner: Runner) => { }), ), ] - methods(protos, string, [ + methods(builtins, string, [ codeUnits("fromCharCode", String.fromCharCode), codeUnits("fromCodePoint", String.fromCodePoint), ]) @@ -118,7 +119,7 @@ export const stringGlobal = (runner: Runner) => { const optStr = (name: string, args: Array, index: number): string | undefined => args[index] === undefined ? undefined : str(name, args, index) const rejectRegex = (name: string, args: Array): 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 = (runner: Runner) => { ] 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 = (runner: Runner) => { 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 = (runner: Runner) => { } }), simple("split", 2, (value, args) => { - const wrap = (parts: Array) => new ProgramArray(protos.Array, parts) + const wrap = (parts: Array) => 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 = (runner: Runner) => { 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 = (runner: Runner) => { 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 = (runner: Runner) => { const matches: Array = [] 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) => { diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts index f5eba11a608..37dcc18da46 100644 --- a/packages/codemode/src/stdlib/url.ts +++ b/packages/codemode/src/stdlib/url.ts @@ -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 string> = { decodeURIComponent, } -export const uriGlobal = (runner: Runner, name: UriFunction) => - fn(runner.prototypes, name, 1, (_, args) => { - const value = uriArgument(runner.prototypes, args[0], `${name} input`) +export const uriGlobal = (ctx: Interpreter, name: UriFunction) => + fn(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 = (runner: Runner, 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 = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.URL - const construct = (args: Array, into: ProgramObject): ProgramURL => { +export const urlGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.URL + const construct = (args: Array, 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(protos, proto, { + const url = constructor(builtins, proto, { name: "URL", length: 1, call: requiresNew("URL"), @@ -81,19 +74,19 @@ export const urlGlobal = (runner: Runner) => { 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 = (runner: Runner) => { : (thisValue, value) => { const target = self(thisValue, name) try { - ;(target.url as unknown as Record)[name] = uriArgument(protos, value, `URL.${name} value`) + ;(target.url as unknown as Record)[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 = (runner: Runner) => { ) } 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 = (runner: Runner, value: unknown): Effect.Effect, unknown, R> => +const readPair = (ctx: Interpreter, value: unknown): Effect.Effect, 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 = (runner: Runner, value: unknown): Effect.Effect uriArgument(runner.prototypes, step.value, "URLSearchParams pair value")), + Effect.sync(() => uriArgument(ctx.builtins, step.value, "URLSearchParams pair value")), ), ) } }) const constructURLSearchParams = ( - runner: Runner, + ctx: Interpreter, init: unknown, - proto: ProgramObject, -): Effect.Effect => { - const wrap = (params: URLSearchParams) => new ProgramURLSearchParams(proto, params) + proto: Obj, +): Effect.Effect => { + 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> = [] while (true) { @@ -163,14 +160,14 @@ const constructURLSearchParams = ( } 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 = ( }) } -export const urlSearchParamsGlobal = (runner: Runner) => { - const protos = runner.prototypes - const proto = protos.URLSearchParams - const searchParams = constructor(protos, proto, { +export const urlSearchParamsGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.URLSearchParams + const searchParams = constructor(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) => new ProgramArray(protos.Array, items) + receiver(URLSearchParamsObj, thisValue, `URLSearchParams.prototype.${name}`) + const wrap = (items: Array) => new Arr(builtins.Array, items) const arg = (name: string, args: Array, 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, 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 = (runner: Runner) => { (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 diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index 89c321f7e24..2edfc1d3f34 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -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 = (runner: Runner, name: Coercion, args: Array): unknown => { +const coerce = (ctx: Interpreter, name: Coercion, args: Array): 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 = (runner: Runner, name: Coercion, args: Array): 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 = (runner: Runner, name: Coercion, args: Array): unk } /** A global coercion function such as `Number` or `parseInt`. */ -export const coercion = (runner: Runner, name: Coercion, length = 1): NativeFunction => - fn(runner.prototypes, name, length, (_, args) => - toProgram(runner.prototypes, coerce(runner, name, args), `${name} result`), - ) +export const coercion = (ctx: Interpreter, name: Coercion, length = 1): Native => + fn(ctx.builtins, name, length, (_, args) => toProgram(ctx.builtins, coerce(ctx, name, args), `${name} result`)) diff --git a/packages/codemode/src/stdlib/web.ts b/packages/codemode/src/stdlib/web.ts index 9eaca82002a..d549c77ba67 100644 --- a/packages/codemode/src/stdlib/web.ts +++ b/packages/codemode/src/stdlib/web.ts @@ -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 = (runner: Runner, name: "atob" | "btoa") => - fn(runner.prototypes, name, 1, (_, args) => { +export const base64Global = (ctx: Interpreter, name: "atob" | "btoa") => + fn(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 = (runner: Runner, name: "atob" | "btoa") => } }) -export const cryptoGlobal = (runner: Runner) => { - const object = new ProgramObject(runner.prototypes.Object) - methods(runner.prototypes, object, [["randomUUID", 0, () => crypto.randomUUID()]]) +export const cryptoGlobal = (ctx: Interpreter) => { + 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 } diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 63d576251a5..7fa42fa6776 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -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 = { /** Per-execution call state over tools prepared once for the runtime. */ export const make = ( prepared: Prepared, - prototypes: Prototypes, + builtins: Builtins, maxToolCalls: number | undefined, hooks?: ToolCallHooks, ): ToolRuntime => { @@ -375,7 +375,7 @@ export const make = ( }), ) 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}`), }) }), diff --git a/packages/codemode/test/extensions.test.ts b/packages/codemode/test/extensions.test.ts index 0f7d1911fd4..1f3298f4572 100644 --- a/packages/codemode/test/extensions.test.ts +++ b/packages/codemode/test/extensions.test.ts @@ -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 })`) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index a872819c1a7..8a002d2dc3e 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -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( diff --git a/packages/codemode/test/test262/run.ts b/packages/codemode/test/test262/run.ts index aa71849cbab..4ec05a50cb9 100644 --- a/packages/codemode/test/test262/run.ts +++ b/packages/codemode/test/test262/run.ts @@ -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 => { // 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 => { return { status: "pass" } } -const harness = (host: Host, onDone: (error: unknown) => void): ReadonlyArray => { - const protos = host.runner.prototypes - const test262Prototype = new ProgramObject(protos.Object) +const harness = ( + ctx: Interpreter, + onDone: (error: unknown) => void, +): ReadonlyArray => { + const builtins = ctx.builtins + const test262Prototype = new Obj(builtins.Object) define(test262Prototype, "name", "Test262Error", hidden) const test262 = (args: Array) => 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(protos, test262Prototype, { + const test262Error = constructor(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(protos, "compareArray", 2, (_, args) => compare(args[0], args[1])) - methods(protos, compareArray, [["format", 1, (_, args) => show(args[0])]]) - const assert = fn(protos, "assert", 2, (_, args) => + methods(builtins, test262Error, [["thrower", 1, (_, args) => fail(String(args[0]))]]) + const compareArray = fn(builtins, "compareArray", 2, (_, args) => compare(args[0], args[1])) + methods(builtins, compareArray, [["format", 1, (_, args) => show(args[0])]]) + const assert = fn(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 = (host: Host, 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 = (host: Host, onDone: (error: unknown) => void): ReadonlyAr ["assert", assert], ["compareArray", compareArray], ["Test262Error", test262Error], - ["$DONE", fn(protos, "$DONE", 1, (_, args) => onDone(args[0]))], + ["$DONE", fn(builtins, "$DONE", 1, (_, args) => onDone(args[0]))], [ "$DONOTEVALUATE", - fn(protos, "$DONOTEVALUATE", 0, () => - Effect.fail(new ProgramThrow("Test262: This statement should not be evaluated.")), + fn(builtins, "$DONOTEVALUATE", 0, () => + Effect.fail(new Throw("Test262: This statement should not be evaluated.")), ), ], ] @@ -164,10 +159,10 @@ const harness = (host: Host, 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" }