feat(codemode): add Uint8Array, drop the Program prefix, and give built-ins one context (#49076)

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