From 1248b22b5a254bd870ddf011608afe4ea2d4afbf Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 12 Sep 2026 00:02:57 -0500 Subject: [PATCH] refactor(codemode): move built-ins onto real prototypes with native function objects --- packages/codemode/interpreter-support.md | 39 +- packages/codemode/src/data.ts | 113 ++- packages/codemode/src/index.ts | 1 - packages/codemode/src/interpreter/errors.ts | 69 +- packages/codemode/src/interpreter/execute.ts | 7 +- .../codemode/src/interpreter/generators.ts | 33 + packages/codemode/src/interpreter/globals.ts | 123 ++- packages/codemode/src/interpreter/host.ts | 79 -- .../codemode/src/interpreter/intrinsics.ts | 87 +- packages/codemode/src/interpreter/methods.ts | 895 ------------------ packages/codemode/src/interpreter/model.ts | 52 +- packages/codemode/src/interpreter/native.ts | 69 ++ packages/codemode/src/interpreter/objects.ts | 314 ++++-- packages/codemode/src/interpreter/promises.ts | 174 ++-- .../codemode/src/interpreter/references.ts | 73 +- packages/codemode/src/interpreter/runner.ts | 52 +- packages/codemode/src/interpreter/runtime.ts | 548 ++++------- packages/codemode/src/stdlib/array.ts | 478 +++++++++- packages/codemode/src/stdlib/collections.ts | 424 ++++++--- packages/codemode/src/stdlib/console.ts | 84 +- packages/codemode/src/stdlib/date.ts | 276 ++---- packages/codemode/src/stdlib/json.ts | 50 +- packages/codemode/src/stdlib/math.ts | 161 ++-- packages/codemode/src/stdlib/number.ts | 154 +-- packages/codemode/src/stdlib/object.ts | 203 ++-- packages/codemode/src/stdlib/regexp.ts | 158 ++-- packages/codemode/src/stdlib/string.ts | 326 +++++-- packages/codemode/src/stdlib/url.ts | 305 ++++-- packages/codemode/src/stdlib/value.ts | 51 +- packages/codemode/src/stdlib/web.ts | 19 +- packages/codemode/src/tool-runtime.ts | 10 +- packages/codemode/src/values.ts | 59 -- packages/codemode/test/callbacks.test.ts | 6 +- packages/codemode/test/enumeration.test.ts | 2 +- .../codemode/test/generator-test262.test.ts | 2 +- packages/codemode/test/parity.test.ts | 49 +- packages/codemode/test/promise.test.ts | 5 +- packages/codemode/test/stdlib.test.ts | 7 +- packages/codemode/test/test262/manifest.json | 1 - packages/codemode/test/test262/run.ts | 152 +-- packages/codemode/test/test262/skipped.txt | 216 ++++- packages/codemode/test/tool-paths.test.ts | 24 +- 42 files changed, 3207 insertions(+), 2743 deletions(-) create mode 100644 packages/codemode/src/interpreter/generators.ts delete mode 100644 packages/codemode/src/interpreter/host.ts delete mode 100644 packages/codemode/src/interpreter/methods.ts create mode 100644 packages/codemode/src/interpreter/native.ts delete mode 100644 packages/codemode/src/values.ts diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 5844a8f06c2e..04943b35b2d2 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -71,8 +71,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 or binding/default failure. - [ ] Object destructuring from primitives follows ToObject (`const { length } = "abc"`, `const {} = 1`); non-object sources are rejected. -- [ ] Destructuring a key that member access resolves through the owning built-in, such as - `const { constructor } = error`, reads `undefined`. +- [x] Destructuring reads through the prototype chain like member access: `const { constructor } = error` and + `const { slice } = values` find the inherited built-in. - [ ] Member expressions as `for...in` targets (`for (x.y in obj)`). - [ ] `IteratorClose` during destructuring should throw a `TypeError` when `return()` yields a non-object. @@ -107,8 +107,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 - [x] Built-in method references as callbacks, such as `values.map(Math.abs)`, `records.map(JSON.stringify)`, `items.forEach(console.log)`, and `Promise.resolve(-1).then(Math.abs)`. Extra callback arguments a built-in does not consume are ignored, like JS; consumed arguments stay strictly validated (`Math.floor` still rejects a - string). Intrinsic references keep their receiver (`"abc".includes` works as a predicate), unlike detached JS - methods, which lose `this`. + string). A detached method loses its receiver, as in JS: `values.filter("abc".includes)` is a `TypeError` + because `includes` is called without a string `this`. - [x] Constructors work as callbacks with JS call semantics: `Error` types construct (`messages.map(Error)`), and new-requiring constructors (`Map`, `Set`, `URL`, `URLSearchParams`, `Promise`) throw a `TypeError`, like JS. @@ -125,7 +125,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 - [x] Functions are objects: they hold own properties (`fn.count = 1`), enumerate them, and expose read-only `name` and `length`. Names follow JavaScript's NamedEvaluation: declarations, named expressions, bindings, assignments, object literal keys, and destructuring or parameter defaults. -- [ ] `name` and `length` of built-in functions such as `Math.max` or `"a".includes`. +- [x] Built-in functions are objects too, with `name` and `length` (`Math.max.length === 2`, + `Array.prototype.push.name === "push"`). - [ ] A named function expression's name is not bound inside its own body. - [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins. - [ ] A line terminator between `async function` and the function name. @@ -169,11 +170,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 - [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`. - [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`. - [x] Logical operators: `&&`, `||`, `??`, and `!`, with short-circuiting. -- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof`, and own-property-only `in`. +- [x] Unary `+`, unary `-`, `void`, `typeof`, `instanceof` (through the constructor's `prototype`, so + `[] instanceof Object` holds), and `in` across the prototype chain. - [x] Prefix and postfix `++` and `--`. - [x] Plain, arithmetic, bitwise, and logical assignment operators. - [x] Property deletion on plain data objects and arrays, including computed and optional forms; deleting an array index - creates a hole without changing its length. + creates a hole without changing its length. Deleting a non-configurable property (`length`, `lastIndex`) or + assigning a read-only one (`Math.PI`, `fn.name`) throws a `TypeError`, as in strict mode. - [ ] Operators, `switch` discriminants, template interpolation, and coercion helpers such as `String` and `isNaN` applied to functions and namespaces; JavaScript coerces them, the interpreter rejects non-data operands. - [ ] ToPrimitive on object operands: operators, `Error(message)`, `Date` arguments, and `parseInt` radix should call @@ -244,17 +247,18 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 targets for index keys only; a primitive target is a `TypeError` rather than a boxed object. - [x] `Object.keys` over arrays and tool references. - [x] Object identity is preserved by in-CodeMode Object helpers. -- [x] `__proto__`, `constructor`, and `prototype` are ordinary own data keys. Errors inherit `constructor` from a real - `Error.prototype` → `TypeError.prototype` chain, so `new TypeError().constructor === TypeError` holds through - member access and destructuring alike; `x.constructor` on other values without an own key resolves to the owning - built-in (`[].constructor === Array`). Prototype objects are not observable, so `[].__proto__` and - `Object.prototype` read as `undefined` and `o.__proto__ = x` sets an own field. +- [x] Every value has a real prototype chain built fresh for each run: `Object.prototype`, `Array.prototype`, + `String.prototype`, `Error.prototype` → `TypeError.prototype`, and so on hold the built-in methods as + non-enumerable properties, and each constructor's `prototype` points at it (`[].constructor === Array`, + `Object.getPrototypeOf` is not exposed). Programs may read and even overwrite these prototypes; the change is + confined to that run. `__proto__` is an ordinary own data key, so `o.__proto__ = x` never changes the chain, and + `Object.groupBy` results have no prototype at all, as in JS. - [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS. - [x] `Object.is` for supported data values. - [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion and plain-object results. -- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, and - `propertyIsEnumerable`. +- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString`, `valueOf`, + `hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`. ## Arrays @@ -419,9 +423,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262 or without `new`. - [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators. -- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. Errors have no - `stack`; the diagnostic carries the source location instead. -- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types. +- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. `message` is an own + non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` while the host still + receives `{ name, message }`. Errors have no `stack`; the diagnostic carries the source location instead. +- [x] `instanceof` against any constructor with a `prototype`, including every built-in and `Function`. - [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program `catch`. diff --git a/packages/codemode/src/data.ts b/packages/codemode/src/data.ts index 324995690f1f..4aa69154e7f4 100644 --- a/packages/codemode/src/data.ts +++ b/packages/codemode/src/data.ts @@ -1,17 +1,26 @@ export * as Data from "./data.js" import type { DiagnosticKind } from "./codemode.js" +import type { Prototypes } from "./interpreter/intrinsics.js" import { + Callable, + define, + entries, get, - ownEntries, + isWrapper, parseArrayIndex, ProgramArray, + ProgramDate, ProgramError, - ProgramFunction, + ProgramGenerator, + ProgramMap, ProgramObject, - set, + ProgramPromise, + ProgramRegExp, + ProgramSet, + ProgramURL, + ProgramURLSearchParams, } from "./interpreter/objects.js" -import { Values } from "./values.js" const MAX_VALUE_DEPTH = 32 @@ -30,17 +39,19 @@ export class ToolRuntimeError extends Error { } /** - * Brings a host-produced value into the program: program and runtime values pass through, their - * host counterparts (Date, RegExp, Map, Set, URL, URLSearchParams) are wrapped, and host objects - * and arrays are copied. + * Brings a host-produced value into the program: program values pass through, host Date, RegExp, + * Map, Set, URL, and URLSearchParams become their built-in wrappers, and host objects and arrays + * are copied. */ -export const toProgram = (value: unknown, label: string): unknown => copy(value, label, "program", 0, new Set()) +export const toProgram = (protos: Prototypes, value: unknown, label: string): unknown => + copy(value, label, "program", 0, new Set(), protos) /** * 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 = (value: unknown, label: string): unknown => copy(value, label, "data", 0, new Set()) +export const fromData = (protos: Prototypes, value: unknown, label: string): unknown => + copy(value, label, "data", 0, new Set(), protos) /** * Takes a program value out as plain JSON: runtime values serialize like `JSON.stringify` would, @@ -54,7 +65,15 @@ export const toData = (value: unknown, label: string, undefinedAs: "json" | "res // "program" and "data" build program objects; "json" and "result" build ordinary objects for the host. type Mode = "program" | "data" | "json" | "result" -const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Set): unknown => { +const copy = ( + value: unknown, + label: string, + mode: Mode, + depth: number, + seen: Set, + protos?: Prototypes, +): unknown => { + const next = (item: unknown) => copy(item, label, mode, depth + 1, seen, protos) if (depth > MAX_VALUE_DEPTH) { throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) } @@ -64,50 +83,48 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se if (typeof value !== "object") { throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`) } - if (value instanceof Values.Promise) { + if (value instanceof ProgramPromise) { 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 ProgramFunction && mode !== "program") { + if ((value instanceof Callable || value instanceof ProgramGenerator) && mode !== "program") { throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`) } - const plain = mode === "program" || mode === "data" - if (mode === "program") { - if (value instanceof ProgramObject || Values.isValue(value)) return value - if (value instanceof Date) return new Values.Date(value.getTime()) - if (value instanceof RegExp) return new Values.RegExp(value.source, value.flags) + 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 (value instanceof Map) { - const wrapped = new Values.Map() - for (const [key, item] of value.entries()) { - wrapped.map.set(copy(key, label, mode, depth + 1, seen), copy(item, label, mode, depth + 1, seen)) - } + const wrapped = new ProgramMap(protos.Map) + for (const [key, item] of value.entries()) wrapped.map.set(next(key), next(item)) return wrapped } if (value instanceof Set) { - const wrapped = new Values.Set() - for (const item of value.values()) wrapped.set.add(copy(item, label, mode, depth + 1, seen)) + const wrapped = new ProgramSet(protos.Set) + for (const item of value.values()) wrapped.set.add(next(item)) return wrapped } - if (value instanceof URL) return new Values.URL(new URL(value.href)) - if (value instanceof URLSearchParams) return new Values.URLSearchParams(new URLSearchParams(value)) + if (value instanceof URL) return new ProgramURL(protos.URL, protos.URLSearchParams, new URL(value.href)) + if (value instanceof URLSearchParams) + return new ProgramURLSearchParams(protos.URLSearchParams, new URLSearchParams(value)) } - if (value instanceof Values.Date) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null + if (value instanceof ProgramDate) 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 Values.URL) return value.url.href + if (value instanceof ProgramURL) return value.url.href if (value instanceof URL) return value.href - // Remaining runtime values and their host counterparts serialize as empty objects, like JSON.stringify. + // Remaining wrappers and their host counterparts serialize as empty objects, like JSON.stringify. if ( - Values.isValue(value) || + isWrapper(value) || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof URLSearchParams ) { - return plain ? new ProgramObject() : {} + return protos !== undefined ? new ProgramObject(protos.Object) : {} } if (seen.has(value)) { @@ -116,7 +133,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se seen.add(value) if (value instanceof ProgramArray) { - const copied = Array.from(value.items, (item) => copy(item, label, mode, depth + 1, seen) ?? null) + const copied = Array.from(value.items, (item) => next(item) ?? null) seen.delete(value) return copied } @@ -124,28 +141,28 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se const copied: Record = {} // Errors serialize as { name, message, ...own }: both may be inherited, and neither is enumerable in JS. if (value instanceof ProgramError) { - define(copied, "name", copy(get(value, "name"), label, mode, depth + 1, seen)) - define(copied, "message", copy(get(value, "message"), label, mode, depth + 1, seen)) + defineHost(copied, "name", next(get(value, "name"))) + defineHost(copied, "message", next(get(value, "message"))) } - for (const [key, item] of ownEntries(value)) { - const next = copy(item, label, mode, depth + 1, seen) - if (next === undefined && mode === "json") continue - define(copied, key, next) + for (const [key, item] of entries(value)) { + const copiedItem = next(item) + if (copiedItem === undefined && mode === "json") continue + defineHost(copied, key, copiedItem) } seen.delete(value) return copied } if (Array.isArray(value)) { - if (plain) { - const copied = new ProgramArray(value.map((item) => copy(item, label, mode, depth + 1, seen))) + if (protos !== undefined) { + const copied = new ProgramArray(protos.Array, value.map(next)) for (const [key, item] of Object.entries(value)) { - if (parseArrayIndex(key) === undefined) set(copied, key, copy(item, label, mode, depth + 1, seen)) + if (parseArrayIndex(key) === undefined) define(copied, key, next(item)) } seen.delete(value) return copied } - const copied = Array.from(value, (item) => copy(item, label, mode, depth + 1, seen) ?? null) + const copied = Array.from(value, (item) => next(item) ?? null) seen.delete(value) return copied } @@ -155,17 +172,17 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`) } - if (plain) { - const copied = new ProgramObject() - for (const [key, item] of Object.entries(value)) set(copied, key, copy(item, label, mode, depth + 1, seen)) + if (protos !== undefined) { + const copied = new ProgramObject(protos.Object) + for (const [key, item] of Object.entries(value)) define(copied, key, next(item)) seen.delete(value) return copied } const copied: Record = {} for (const [key, item] of Object.entries(value)) { - const next = copy(item, label, mode, depth + 1, seen) - if (next === undefined && mode === "json") continue - define(copied, key, next) + const copiedItem = next(item) + if (copiedItem === undefined && mode === "json") continue + defineHost(copied, key, copiedItem) } seen.delete(value) return copied @@ -173,6 +190,6 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se // Own data property regardless of the target's prototype, so a "__proto__" key on a host object // never reaches the Object.prototype setter. -const define = (target: object, key: string, value: unknown): void => { +const defineHost = (target: object, key: string, value: unknown): void => { Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true }) } diff --git a/packages/codemode/src/index.ts b/packages/codemode/src/index.ts index b8365cb425e0..2528c7acf6f2 100644 --- a/packages/codemode/src/index.ts +++ b/packages/codemode/src/index.ts @@ -2,6 +2,5 @@ export * as CodeMode from "./codemode.js" export * as Namespace from "./namespace.js" export * as Tool from "./tool.js" export * as OpenAPI from "./openapi/index.js" -export { Values } from "./values.js" export { searchSignature, toolExpression } from "./codemode.js" export { ToolError, toolError } from "./tool-error.js" diff --git a/packages/codemode/src/interpreter/errors.ts b/packages/codemode/src/interpreter/errors.ts index 16e1722a4e1a..d6f08a3dc525 100644 --- a/packages/codemode/src/interpreter/errors.ts +++ b/packages/codemode/src/interpreter/errors.ts @@ -4,9 +4,18 @@ import { ToolError } from "../tool-error.js" import { toData, ToolRuntimeError } from "../data.js" import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js" import { containsRuntimeReference } from "./references.js" -import { type HostCall, HostFunction } from "./host.js" import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js" -import { get, hasPrototype, ProgramArray, ProgramError, ProgramObject, set } from "./objects.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 { coerceToString } from "../stdlib/value.js" @@ -74,21 +83,27 @@ export const normalizeError = (error: unknown): Diagnostic => { export const caughtErrorValue = (runner: Runner, thrown: unknown): unknown => { if (thrown instanceof ProgramThrow) return thrown.value - const prototypes = runner.intrinsics.errors + const prototypes = runner.prototypes if (thrown instanceof InterpreterRuntimeError) return createErrorValue(prototypes[thrown.type], thrown.message) const type = thrown instanceof Error && isErrorType(thrown.name) ? thrown.name : "Error" return createErrorValue(prototypes[type], normalizeError(thrown).message) } -export const createAggregateErrorValue = (runner: Runner, errors: Array, message: string) => { - const value = createErrorValue(runner.intrinsics.errors.AggregateError, message) - set(value, "errors", new ProgramArray(errors)) +export const createAggregateErrorValue = ( + runner: Runner, + errors: Array, + message: string, + proto: ProgramObject = runner.prototypes.AggregateError, +) => { + const value = createErrorValue(proto, message) + define(value, "errors", new ProgramArray(runner.prototypes.Array, errors), { ...hidden }) return value } const constructAggregateErrorValue = ( runner: Runner, args: Array, + proto: ProgramObject, node: AstNode, ): Effect.Effect => Effect.gen(function* () { @@ -100,7 +115,7 @@ const constructAggregateErrorValue = ( while (true) { const step = yield* cursor.next if (step.done) { - return createAggregateErrorValue(runner, errors, args[1] === undefined ? "" : coerceToString(args[1])) + return createAggregateErrorValue(runner, errors, args[1] === undefined ? "" : coerceToString(args[1]), proto) } errors.push(step.value) } @@ -108,17 +123,37 @@ const constructAggregateErrorValue = ( /** An error constructor such as `Error` or `TypeError`; callable with or without `new`, like JS. */ export const errorGlobal = (type: ErrorType, runner: Runner) => { - const prototype = runner.intrinsics.errors[type] - const construct: HostCall = (args, node) => - type === "AggregateError" - ? constructAggregateErrorValue(runner, args, node) - : Effect.sync(() => createErrorValue(prototype, args[0] === undefined ? undefined : coerceToString(args[0]))) - const fn = new HostFunction({ + const protos = runner.prototypes + const prototype = protos[type] + const construct = (args: Array, newTarget: Callable, node: AstNode) => { + const proto = prototypeFrom(newTarget, prototype) + return type === "AggregateError" + ? constructAggregateErrorValue(runner, args, proto, node) + : Effect.sync(() => createErrorValue(proto, args[0] === undefined ? undefined : coerceToString(args[0]))) + } + const ctor: NativeFunction = constructor(protos, prototype, { name: type, - call: construct, + length: type === "AggregateError" ? 2 : 1, + call: (_, args, node) => construct(args, ctor, node), construct, - instanceOf: (value) => hasPrototype(value, prototype), }) - set(prototype, "constructor", fn) - return fn + if (type === "Error") { + methods(protos, prototype, [ + [ + "toString", + 0, + (thisValue, _, node) => { + const self = receiver(ProgramObject, thisValue, "Error.prototype.toString", node) + const name = get(self, "name") + const message = get(self, "message") + const shownName = name === undefined ? "Error" : coerceToString(name) + const shownMessage = message === undefined ? "" : coerceToString(message) + if (shownMessage === "") return shownName + if (shownName === "") return shownMessage + return `${shownName}: ${shownMessage}` + }, + ], + ]) + } + return ctor } diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts index 0a1bc30d23dd..050380a35a8d 100644 --- a/packages/codemode/src/interpreter/execute.ts +++ b/packages/codemode/src/interpreter/execute.ts @@ -7,6 +7,7 @@ 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 { InterpreterRuntimeError } from "./model.js" import { PromiseRuntime } from "./promises.js" @@ -29,7 +30,8 @@ export const executeProgram = ( // Allocate execution state inside suspension so reused Effects never share it. return Effect.suspend(() => { - const tools = ToolRuntime.make(prepared, limits.maxToolCalls, hooks) + const prototypes = createPrototypes() + const tools = ToolRuntime.make(prepared, prototypes, limits.maxToolCalls, hooks) const logs: Array = [] const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) // Set only after copy-out so timeouts cannot report invalid values as completed. @@ -40,12 +42,13 @@ export const executeProgram = ( (scope) => Effect.gen(function* () { const program = parseProgram(code) - const promises = new PromiseRuntime(scope) + const promises = new PromiseRuntime(scope, prototypes.Promise) const value = yield* new Runtime( tools.execute, tools.search, tools.keys, promises, + prototypes, logs, extraGlobals, ).run(program) diff --git a/packages/codemode/src/interpreter/generators.ts b/packages/codemode/src/interpreter/generators.ts new file mode 100644 index 000000000000..f45dca23215e --- /dev/null +++ b/packages/codemode/src/interpreter/generators.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect" +import { fn, type Method, methods, receiver } from "./native.js" +import { type AstNode, 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" + +/** `next`/`return`/`throw` on the generator prototypes; async generators answer with promises. */ +export const generatorGlobals = (runner: Runner, promises: PromiseRuntime): void => { + const protos = runner.prototypes + const install = (asynchronous: boolean) => { + const proto = asynchronous ? protos.AsyncGenerator : protos.Generator + const label = asynchronous ? "AsyncGenerator" : "Generator" + const request = (kind: GeneratorRequestKind): Method => [ + kind, + 1, + (thisValue: unknown, args: Array, node: AstNode) => { + const generator = receiver(ProgramGenerator, thisValue, `${label}.prototype.${kind}`, node) + const requested = generator.request(kind, args[0], node) as Effect.Effect + return generator.asynchronous ? promises.create(requested) : requested + }, + ] + methods(protos, proto, [request("next"), request("return"), request("throw")]) + define( + asynchronous ? protos.AsyncIterator : protos.Iterator, + asynchronous ? AsyncIteratorSymbol : IteratorSymbol, + fn(protos, asynchronous ? "[Symbol.asyncIterator]" : "[Symbol.iterator]", 0, (thisValue) => thisValue), + hidden, + ) + } + install(false) + install(true) +} diff --git a/packages/codemode/src/interpreter/globals.ts b/packages/codemode/src/interpreter/globals.ts index ca24e42c4aba..4e93e9a35b8a 100644 --- a/packages/codemode/src/interpreter/globals.ts +++ b/packages/codemode/src/interpreter/globals.ts @@ -5,18 +5,19 @@ import { consoleGlobal } from "../stdlib/console.js" import { dateGlobal } from "../stdlib/date.js" import { jsonGlobal } from "../stdlib/json.js" import { mathGlobal } from "../stdlib/math.js" -import { numberGlobal } from "../stdlib/number.js" +import { booleanGlobal, numberGlobal } from "../stdlib/number.js" import { objectGlobal } from "../stdlib/object.js" import { regexpGlobal } from "../stdlib/regexp.js" import { stringGlobal } from "../stdlib/string.js" import { uriGlobal, urlGlobal, urlSearchParamsGlobal } from "../stdlib/url.js" import { coercion } from "../stdlib/value.js" -import { atobGlobal, btoaGlobal, cryptoGlobal } from "../stdlib/web.js" +import { base64Global, cryptoGlobal } from "../stdlib/web.js" import { ToolReference } from "../tool-runtime.js" import { errorGlobal } from "./errors.js" import { errorTypes } from "./intrinsics.js" -import { HostFunction } from "./host.js" -import { AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "./model.js" +import { constants, constructor, native } from "./native.js" +import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "./model.js" +import { generatorGlobals } from "./generators.js" import { promiseGlobal, type PromiseRuntime } from "./promises.js" import type { Runner } from "./runner.js" @@ -29,52 +30,74 @@ export type Host = { readonly logs: Array } -const symbolGlobal = new HostFunction({ - name: "Symbol", - call: (_, node) => +// Function.prototype.constructor exists so `fn.constructor === Function` holds; dynamic code is unsupported. +const functionGlobal = (runner: Runner) => { + const reject = (_: unknown, __: Array, node: AstNode) => Effect.sync(() => { - throw new InterpreterRuntimeError( - "Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.", - node, - ) - }), - callback: false, - members: { asyncIterator: AsyncIteratorSymbol, iterator: IteratorSymbol }, -}) + throw new InterpreterRuntimeError("The Function constructor is not supported; write the function inline.", node) + }) + return constructor(runner.prototypes, runner.prototypes.Function, { + name: "Function", + length: 1, + call: reject, + construct: (args, _, node) => reject(undefined, args, node), + }) +} + +const symbolGlobal = (runner: Runner) => { + const symbol = native(runner.prototypes, { + name: "Symbol", + call: (_, __, node) => + Effect.sync(() => { + throw new InterpreterRuntimeError( + "Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.", + node, + ) + }), + callback: false, + }) + constants(symbol, { asyncIterator: AsyncIteratorSymbol, iterator: IteratorSymbol }) + return symbol +} /** The immutable global bindings of every program, in declaration order. */ -export const globals = (host: Host): ReadonlyArray => [ - ["tools", new ToolReference([])], - ["search", new HostFunction({ name: "search", call: (args) => host.search(args), callback: false })], - ["undefined", undefined], - ["NaN", NaN], - ["Infinity", Infinity], - ["Object", objectGlobal(host.runner, host.toolKeys)], - ["Array", arrayGlobal(host.runner)], - ["Math", mathGlobal(host.runner)], - ["JSON", jsonGlobal(host.runner)], - ["console", consoleGlobal(host.logs)], - ["Promise", promiseGlobal(host.runner, host.promises)], - ["Symbol", symbolGlobal], - ["Number", numberGlobal], - ["String", stringGlobal], - ["Boolean", coercion("Boolean", { instanceOf: () => false })], - ["parseInt", coercion("parseInt")], - ["parseFloat", coercion("parseFloat")], - ["isFinite", coercion("isFinite")], - ["isNaN", coercion("isNaN")], - ["Date", dateGlobal(host.runner)], - ["RegExp", regexpGlobal], - ["Map", mapGlobal(host.runner)], - ["Set", setGlobal(host.runner)], - ["URL", urlGlobal], - ["URLSearchParams", urlSearchParamsGlobal(host.runner)], - ["encodeURI", uriGlobal("encodeURI")], - ["encodeURIComponent", uriGlobal("encodeURIComponent")], - ["decodeURI", uriGlobal("decodeURI")], - ["decodeURIComponent", uriGlobal("decodeURIComponent")], - ["atob", atobGlobal], - ["btoa", btoaGlobal], - ["crypto", cryptoGlobal], - ...errorTypes.map((type) => [type, errorGlobal(type, host.runner)] as const), -] +export const globals = (host: Host): ReadonlyArray => { + const runner = host.runner + generatorGlobals(runner, host.promises) + return [ + ["tools", new ToolReference([])], + ["search", native(runner.prototypes, { name: "search", call: (_, args) => host.search(args), callback: false })], + ["undefined", undefined], + ["NaN", NaN], + ["Infinity", Infinity], + ["Object", objectGlobal(runner, host.toolKeys)], + ["Function", functionGlobal(runner)], + ["Array", arrayGlobal(runner)], + ["Math", mathGlobal(runner)], + ["JSON", jsonGlobal(runner)], + ["console", consoleGlobal(runner, host.logs)], + ["Promise", promiseGlobal(runner, host.promises)], + ["Symbol", symbolGlobal(runner)], + ["Number", numberGlobal(runner)], + ["String", stringGlobal(runner)], + ["Boolean", booleanGlobal(runner)], + ["parseInt", coercion(runner, "parseInt", 2)], + ["parseFloat", coercion(runner, "parseFloat")], + ["isFinite", coercion(runner, "isFinite")], + ["isNaN", coercion(runner, "isNaN")], + ["Date", dateGlobal(runner)], + ["RegExp", regexpGlobal(runner)], + ["Map", mapGlobal(runner)], + ["Set", setGlobal(runner)], + ["URL", urlGlobal(runner)], + ["URLSearchParams", urlSearchParamsGlobal(runner)], + ["encodeURI", uriGlobal(runner, "encodeURI")], + ["encodeURIComponent", uriGlobal(runner, "encodeURIComponent")], + ["decodeURI", uriGlobal(runner, "decodeURI")], + ["decodeURIComponent", uriGlobal(runner, "decodeURIComponent")], + ["atob", base64Global(runner, "atob")], + ["btoa", base64Global(runner, "btoa")], + ["crypto", cryptoGlobal(runner)], + ...errorTypes.map((type) => [type, errorGlobal(type, runner)] as const), + ] +} diff --git a/packages/codemode/src/interpreter/host.ts b/packages/codemode/src/interpreter/host.ts deleted file mode 100644 index dd8e28980c6d..000000000000 --- a/packages/codemode/src/interpreter/host.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Effect } from "effect" -import { type AstNode, InterpreterRuntimeError } from "./model.js" - -export type HostCall = (args: Array, node: AstNode) => Effect.Effect - -type HostMember = (key: PropertyKey, node: AstNode) => unknown - -type HostFunctionOptions = { - readonly name: string - readonly call: HostCall - /** `new name(...)`; without it `new` is unsupported syntax. */ - readonly construct?: HostCall - /** Static members read through `name.key`; unknown keys read as `undefined` unless the function decides otherwise. */ - readonly members?: Record | HostMember - /** `value instanceof name`; without it the operator rejects this right-hand side. */ - readonly instanceOf?: (value: unknown) => boolean - /** Whether callback sites (array methods, replacers, promise reactions) admit this function. Defaults to true. */ - readonly callback?: boolean -} - -/** A host-implemented function value. `typeof` is "function". */ -export class HostFunction { - readonly name: string - readonly call: HostCall - readonly construct: HostCall | undefined - readonly member: HostMember - readonly instanceOf: ((value: unknown) => boolean) | undefined - readonly callback: boolean - - constructor(options: HostFunctionOptions) { - this.name = options.name - this.call = options.call - this.construct = options.construct - this.member = memberLookup(options.members) - this.instanceOf = options.instanceOf - this.callback = options.callback ?? true - } -} - -/** A host-implemented object of static members. `typeof` is "object" and it is not callable. */ -export class HostNamespace { - readonly member: HostMember - - constructor( - readonly name: string, - members: Record | HostMember, - ) { - this.member = memberLookup(members) - } -} - -const memberLookup = (members: Record | HostMember | undefined): HostMember => { - if (members === undefined) return () => undefined - if (typeof members === "function") return members - const table = new Map(Object.entries(members)) - return (key) => (typeof key === "string" ? table.get(key) : undefined) -} - -export type SyncOptions = Omit, "name" | "call"> - -type SyncImpl = (args: Array, node: AstNode) => unknown - -/** Lifts a synchronous implementation, which may throw `InterpreterRuntimeError`, into a host call. */ -export const syncCall = - (impl: SyncImpl): HostCall => - (args, node) => - Effect.sync(() => impl(args, node)) - -/** A synchronous host function. */ -export const sync = (name: string, impl: SyncImpl, options: SyncOptions = {}): HostFunction => - new HostFunction({ name, call: syncCall(impl), ...options }) - -/** The `call` of a constructor that JS requires to be invoked with `new`. */ -export const requiresNew = - (name: string): HostCall => - (_, node) => - Effect.sync(() => { - throw new InterpreterRuntimeError(`Constructor ${name} requires 'new'.`, node) - }) diff --git a/packages/codemode/src/interpreter/intrinsics.ts b/packages/codemode/src/interpreter/intrinsics.ts index 703589989f0d..95de313ff812 100644 --- a/packages/codemode/src/interpreter/intrinsics.ts +++ b/packages/codemode/src/interpreter/intrinsics.ts @@ -1,4 +1,5 @@ -import { ProgramError, ProgramObject, set } from "./objects.js" +import { Effect } from "effect" +import { define, hidden, NativeFunction, ProgramArray, ProgramError, ProgramObject } from "./objects.js" export const errorTypes = [ "Error", @@ -15,37 +16,79 @@ export type ErrorType = (typeof errorTypes)[number] export const isErrorType = (name: string): name is ErrorType => (errorTypes as ReadonlyArray).includes(name) -/** The built-in prototype objects of one runtime. Constructors attach themselves as `constructor` when created. */ -export type Intrinsics = { - readonly errors: Readonly> -} +const builtins = [ + "Object", + "Function", + "Array", + "String", + "Number", + "Boolean", + "Date", + "RegExp", + "Map", + "Set", + "URL", + "URLSearchParams", + "Promise", + "Iterator", + "AsyncIterator", + "Generator", + "AsyncGenerator", +] as const + +/** + * The built-in prototype objects of one runtime, allocated empty in dependency order. The globals populate them + * and attach their constructors when the runtime is built. + */ +export type Prototypes = Readonly> export const createErrorValue = (prototype: ProgramObject, message: string | undefined): ProgramError => { const value = new ProgramError(prototype) - if (message !== undefined) set(value, "message", message) + if (message !== undefined) define(value, "message", message, hidden) return value } -export const createIntrinsics = (): Intrinsics => { - const error = new ProgramObject() - set(error, "name", "Error") - set(error, "message", "") +export const createPrototypes = (): Prototypes => { + const object = new ProgramObject(null) + // Function.prototype is itself callable and returns undefined. + const fn = new NativeFunction(object, { name: "", call: () => Effect.undefined }) + const plain = () => new ProgramObject(object) + const error = plain() + define(error, "name", "Error", hidden) + define(error, "message", "", hidden) const derived = (type: ErrorType) => { const proto = new ProgramObject(error) - set(proto, "name", type) - set(proto, "message", "") + define(proto, "name", type, hidden) + define(proto, "message", "", hidden) return proto } + const iterator = plain() + const asyncIterator = plain() return { - errors: { - Error: error, - TypeError: derived("TypeError"), - RangeError: derived("RangeError"), - SyntaxError: derived("SyntaxError"), - ReferenceError: derived("ReferenceError"), - EvalError: derived("EvalError"), - URIError: derived("URIError"), - AggregateError: derived("AggregateError"), - }, + Object: object, + Function: fn, + Array: new ProgramArray(object), + String: plain(), + Number: plain(), + Boolean: plain(), + Date: plain(), + RegExp: plain(), + Map: plain(), + Set: plain(), + URL: plain(), + URLSearchParams: plain(), + Promise: plain(), + Iterator: iterator, + AsyncIterator: asyncIterator, + Generator: new ProgramObject(iterator), + AsyncGenerator: new ProgramObject(asyncIterator), + Error: error, + TypeError: derived("TypeError"), + RangeError: derived("RangeError"), + SyntaxError: derived("SyntaxError"), + ReferenceError: derived("ReferenceError"), + EvalError: derived("EvalError"), + URIError: derived("URIError"), + AggregateError: derived("AggregateError"), } } diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts deleted file mode 100644 index 4d9476092d08..000000000000 --- a/packages/codemode/src/interpreter/methods.ts +++ /dev/null @@ -1,895 +0,0 @@ -import { Effect } from "effect" -import { toProgram } from "../data.js" -import { dateSetterArgumentCount, invokeDateMethod } from "../stdlib/date.js" -import { invokeNumberMethod } from "../stdlib/number.js" -import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js" -import { invokeURLMethod, uriArgument } from "../stdlib/url.js" -import { coerceToNumber, coerceToString } from "../stdlib/value.js" -import { compareText } from "../tool-runtime.js" -import { Values } from "../values.js" -import { type AstNode, InterpreterRuntimeError, IntrinsicReference, rangeError } from "./model.js" -import { get, ProgramArray, ProgramObject, record } from "./objects.js" -import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "./references.js" -import { applyCollectionCallback, isSupportedCallback, type Runner, toPrimitive } from "./runner.js" - -export const invokeIntrinsic = ( - runner: Runner, - ref: IntrinsicReference, - args: Array, - node: AstNode, -): Effect.Effect => - Effect.map(invoke(runner, ref, args, node), (result) => (Array.isArray(result) ? new ProgramArray(result) : result)) - -const invoke = ( - runner: Runner, - ref: IntrinsicReference, - args: Array, - node: AstNode, -): Effect.Effect => { - if (typeof ref.receiver === "string") { - if (ref.name === "replace" || ref.name === "replaceAll") { - if (isSupportedCallback(args[1])) return invokeStringReplacer(runner, ref.receiver, ref.name, args, node) - if (typeofValue(args[1]) === "function") { - throw new InterpreterRuntimeError( - `String.${ref.name} cannot use this callable as a replacer; wrap it in an arrow function, e.g. (match) => tools.ns.tool(match).`, - node, - ) - } - } - return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node)) - } - if (typeof ref.receiver === "number") { - return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node)) - } - if (ref.receiver instanceof ProgramArray) { - return invokeArrayMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof Values.Date) { - const target = ref.receiver - const argumentCount = dateSetterArgumentCount(ref.name) - if (argumentCount === undefined) return Effect.succeed(invokeDateMethod(target, ref.name, [], node)) - // Native setters read the current time before argument coercion, whose callbacks may mutate the Date. - const initialTime = target.time - return Effect.map( - Effect.forEach(args.slice(0, argumentCount), (arg) => coerceNumericArgument(runner, arg, node), { - concurrency: 1, - }), - (values) => invokeDateMethod(target, ref.name, values, node, initialTime), - ) - } - if (ref.receiver instanceof Values.RegExp) { - return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) - } - if (ref.receiver instanceof Values.Map) { - return invokeMapMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof Values.Set) { - return invokeSetMethod(runner, ref.receiver, ref.name, args, node) - } - if (ref.receiver instanceof Values.URL) { - return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) - } - if (ref.receiver instanceof Values.URLSearchParams) { - return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node) - } - throw new InterpreterRuntimeError(`Method '${ref.name}' is not available.`, node) -} - -/** - * ToPrimitive: tries an object's own `valueOf`/`toString` in hint order and returns the first - * primitive result. Runtime values behave like their JS counterparts (Date yields its time under a - * number hint; the rest yield their string form). An inherited `toString` yields the default - * string form, so plain objects become "[object Object]" and arrays join. - */ - -const coerceNumericArgument = ( - runner: Runner, - value: unknown, - node: AstNode, -): Effect.Effect => Effect.map(toPrimitive(runner, value, "number", node), coerceToNumber) - -// console is intercepted by the interpreter before reaching here. -const requireDataArgument = (name: string, index: number, arg: unknown, node: AstNode): unknown => { - if (containsOpaqueReference(arg)) { - throw new InterpreterRuntimeError( - `String.${name} expects argument ${index + 1} to be a data value.`, - node, - "InvalidDataValue", - ) - } - return arg -} - -const invokeStringMethod = (value: string, name: string, args: Array, node: AstNode): unknown => { - // Coerce arguments like native JS; opaque runtime references still reject. - const str = (index: number): string => coerceToString(requireDataArgument(name, index, args[index], node)) - const num = (index: number): number => coerceToNumber(requireDataArgument(name, index, args[index], node)) - const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index)) - const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index)) - const rejectRegex = (): void => { - if (args[0] instanceof Values.RegExp) { - throw new InterpreterRuntimeError( - `String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`, - node, - ) - } - } - - let result: unknown - switch (name) { - case "toLowerCase": - result = value.toLowerCase() - break - case "toUpperCase": - result = value.toUpperCase() - break - case "trim": - result = value.trim() - break - case "trimStart": - case "trimLeft": - result = value.trimStart() - break - case "trimEnd": - case "trimRight": - result = value.trimEnd() - break - // Locale/options are deliberately unsupported; comparison uses the host default locale. - case "localeCompare": - result = value.localeCompare(str(0)) - break - case "normalize": { - const form = optStr(0) - try { - result = value.normalize(form) - } catch { - throw rangeError( - `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, - node, - ) - } - break - } - case "split": { - // Native: an undefined separator returns the whole string, not a split on "undefined", - // unless the limit truncates to zero. - if (args[0] === undefined) { - const requestedLimit = optNum(1) - result = requestedLimit !== undefined && requestedLimit >>> 0 === 0 ? [] : [value] - break - } - if (args[0] instanceof Values.RegExp) { - result = value.split(args[0].regex, optNum(1)) - break - } - const requestedLimit = optNum(1) - result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0) - break - } - case "slice": - result = value.slice(optNum(0), optNum(1)) - break - case "includes": - rejectRegex() - result = value.includes(str(0), optNum(1)) - break - case "startsWith": - rejectRegex() - result = value.startsWith(str(0), optNum(1)) - break - case "endsWith": - rejectRegex() - result = value.endsWith(str(0), optNum(1)) - break - case "indexOf": - result = value.indexOf(str(0), optNum(1)) - break - case "lastIndexOf": - result = value.lastIndexOf(str(0), optNum(1)) - break - case "replace": - case "replaceAll": { - if (args[0] instanceof Values.RegExp) { - const pattern = args[0].regex - const replacement = str(1) - if (name === "replaceAll" && !pattern.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) - break - } - if (name === "replace") { - result = value.replace(str(0), str(1)) - break - } - result = value.replaceAll(str(0), str(1)) - break - } - case "match": { - const pattern = toHostRegex(args[0], name, node) - 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(matched, "String.match result") - return matchToValue(matched) - } - case "matchAll": { - const pattern = toHostRegex(args[0], name, node, "g") - if (!pattern.global) { - throw new InterpreterRuntimeError( - `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, - node, - ) - } - return new ProgramArray(Array.from(value.matchAll(pattern), matchToValue)) - } - case "search": { - result = value.search(toHostRegex(args[0], name, node)) - break - } - case "repeat": { - const count = num(0) - if (!Number.isFinite(count) || count < 0) - throw rangeError("String.repeat expects a finite non-negative count.", node) - result = value.repeat(count) - break - } - case "padStart": - result = value.padStart(num(0), optStr(1)) - break - case "padEnd": - result = value.padEnd(num(0), optStr(1)) - break - case "charAt": - result = value.charAt(optNum(0) ?? 0) - break - case "at": - result = value.at(optNum(0) ?? 0) - break - case "substring": - result = value.substring(optNum(0) ?? 0, optNum(1)) - break - case "substr": - result = value.substr(optNum(0) ?? 0, optNum(1)) - break - case "isWellFormed": - result = value.isWellFormed() - break - case "toWellFormed": - result = value.toWellFormed() - break - case "charCodeAt": - result = value.charCodeAt(optNum(0) ?? 0) - break - case "codePointAt": - result = value.codePointAt(optNum(0) ?? 0) - break - case "toString": - result = value - break - case "concat": { - result = value.concat(...args.map((_, index) => str(index))) - break - } - default: - throw new InterpreterRuntimeError(`String method '${name}' is not available.`, node) - } - return toProgram(result, `String.${name} result`) -} - -const invokeStringReplacer = ( - runner: Runner, - value: string, - name: "replace" | "replaceAll", - args: Array, - node: AstNode, -): Effect.Effect => { - const apply = applyCollectionCallback(runner, args[1], `String.${name}`, node) - const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] - const collect = (...callbackArgs: Array): string => { - const match = callbackArgs[0] - const groups = callbackArgs[callbackArgs.length - 1] - const hasGroups = groups !== null && typeof groups === "object" - const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] - if (typeof match !== "string" || typeof offset !== "number") { - throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) - } - if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(groups as Record) - matches.push({ match, offset, args: callbackArgs }) - return match - } - - const pattern = args[0] - if (pattern instanceof Values.RegExp) { - if (name === "replaceAll" && !pattern.regex.global) { - throw new InterpreterRuntimeError( - `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, - node, - ) - } - if (name === "replace") value.replace(pattern.regex, collect) - else value.replaceAll(pattern.regex, collect) - } else { - const search = coerceToString(requireDataArgument(name, 0, pattern, node)) - if (name === "replace") value.replace(search, collect) - else value.replaceAll(search, collect) - } - - return Effect.gen(function* () { - const output: Array = [] - let end = 0 - for (const match of matches) { - const replacement = yield* apply(match.args) - output.push( - value.slice(end, match.offset), - replacement instanceof Values.Promise - ? "[object Promise]" - : coerceToString(toProgram(replacement, `String.${name} replacer result`)), - ) - end = match.offset + match.match.length - } - output.push(value.slice(end)) - return toProgram(output.join(""), `String.${name} result`) - }) -} - -const invokeMapMethod = ( - runner: Runner, - target: Values.Map, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - switch (name) { - case "get": - return Effect.succeed(target.map.get(args[0])) - case "has": - return Effect.succeed(target.map.has(args[0])) - case "set": - return Effect.sync(() => { - target.map.set(args[0], args[1]) - return target - }) - case "delete": - return Effect.sync(() => target.map.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.map.clear() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.map.keys())) - case "values": - return Effect.sync(() => Array.from(target.map.values())) - case "entries": - return Effect.sync(() => Array.from(target.map.entries(), ([key, item]) => new ProgramArray([key, item]))) - case "forEach": { - const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node) - return Effect.gen(function* () { - for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`Map method '${name}' is not available.`, node) - } -} - -const invokeSetMethod = ( - runner: Runner, - target: Values.Set, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - switch (name) { - case "has": - return Effect.succeed(target.set.has(args[0])) - case "add": - return Effect.sync(() => { - target.set.add(args[0]) - return target - }) - case "delete": - return Effect.sync(() => target.set.delete(args[0])) - case "clear": - return Effect.sync(() => { - target.set.clear() - return undefined - }) - case "keys": - case "values": - return Effect.sync(() => Array.from(target.set.values())) - case "entries": - return Effect.sync(() => Array.from(target.set.values(), (item) => new ProgramArray([item, item]))) - case "forEach": { - const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node) - return Effect.gen(function* () { - for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) - return undefined - }) - } - case "union": - case "intersection": - case "difference": - case "symmetricDifference": - case "isSubsetOf": - case "isSupersetOf": - case "isDisjointFrom": - return invokeSetOperation(runner, target, name, args[0], node) - default: - throw new InterpreterRuntimeError(`Set method '${name}' is not available.`, node) - } -} - -const invokeSetOperation = ( - runner: Runner, - target: Values.Set, - name: string, - source: unknown, - node: AstNode, -): Effect.Effect => - Effect.gen(function* () { - const other = yield* loadSetRecord(runner, source, name, node) - if (name === "union") { - const result = copySet(target) - for (const item of yield* other.keys()) result.set.add(item) - return result - } - if (name === "intersection") { - const result = new Values.Set() - if (target.set.size <= other.size) { - for (const item of target.set.values()) { - if (yield* other.has(item)) result.set.add(item) - } - return result - } - for (const item of yield* other.keys()) { - if (target.set.has(item)) result.set.add(item) - } - return result - } - if (name === "difference") { - const result = copySet(target) - if (target.set.size <= other.size) { - for (const item of result.set.values()) { - if (yield* other.has(item)) result.set.delete(item) - } - return result - } - for (const item of yield* other.keys()) result.set.delete(item) - return result - } - if (name === "symmetricDifference") { - const result = copySet(target) - for (const item of yield* other.keys()) { - if (target.set.has(item)) result.set.delete(item) - else result.set.add(item) - } - return result - } - if (name === "isSubsetOf") { - if (target.set.size > other.size) return false - for (const item of target.set.values()) { - if (!(yield* other.has(item))) return false - } - return true - } - if (name === "isSupersetOf") { - if (target.set.size < other.size) return false - for (const item of yield* other.keys()) { - if (!target.set.has(item)) return false - } - return true - } - if (target.set.size <= other.size) { - for (const item of target.set.values()) { - if (yield* other.has(item)) return false - } - return true - } - for (const item of yield* other.keys()) { - if (target.set.has(item)) return false - } - return true - }) - -const copySet = (source: Values.Set): Values.Set => { - const result = new Values.Set() - for (const item of source.set.values()) result.set.add(item) - return result -} - -const loadSetRecord = (runner: Runner, source: unknown, name: string, node: AstNode) => { - if (source instanceof Values.Set) { - 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 Values.Map) { - 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)) { - throw new InterpreterRuntimeError(`Set.${name} expects a Set-like object.`, node) - } - return Effect.gen(function* () { - const size = yield* coerceNumericArgument(runner, get(source, "size"), node) - if (Number.isNaN(size)) { - throw new InterpreterRuntimeError(`Set.${name} received a Set-like object with an invalid size.`, node) - } - const has = get(source, "has") - const keys = get(source, "keys") - if (!isSupportedCallback(has) || !isSupportedCallback(keys)) { - throw new InterpreterRuntimeError(`Set.${name} expects callable 'has' and 'keys' methods.`, node) - } - return { - size: Math.max(Math.trunc(size), 0), - has: (item: unknown) => Effect.map(runner.invokeCallable(has, [item], node), Boolean), - keys: () => - Effect.flatMap(runner.invokeCallable(keys, [], node), (result) => { - if (result instanceof ProgramArray) return Effect.succeed(result.items) - throw new InterpreterRuntimeError(`Set.${name} expected 'keys' to return an iterator.`, node) - }), - } - }) -} - -const invokeURLSearchParamsMethod = ( - runner: Runner, - target: Values.URLSearchParams, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`) - const requireArgs = (count: number): void => { - if (args.length < count) { - throw new InterpreterRuntimeError( - `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, - node, - ) - } - } - switch (name) { - case "append": { - requireArgs(2) - return Effect.sync(() => { - target.params.append(arg(0), arg(1)) - return undefined - }) - } - case "delete": { - requireArgs(1) - return Effect.sync(() => { - if (args[1] !== undefined) target.params.delete(arg(0), arg(1)) - else target.params.delete(arg(0)) - return undefined - }) - } - case "get": - requireArgs(1) - return Effect.sync(() => target.params.get(arg(0))) - case "getAll": - requireArgs(1) - return Effect.sync(() => target.params.getAll(arg(0))) - case "has": - requireArgs(1) - return Effect.sync(() => (args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0)))) - case "set": { - requireArgs(2) - return Effect.sync(() => { - target.params.set(arg(0), arg(1)) - return undefined - }) - } - case "sort": - return Effect.sync(() => { - target.params.sort() - return undefined - }) - case "keys": - return Effect.sync(() => Array.from(target.params.keys())) - case "values": - return Effect.sync(() => Array.from(target.params.values())) - case "entries": - return Effect.sync(() => Array.from(target.params.entries(), ([key, value]) => new ProgramArray([key, value]))) - case "toString": - return Effect.sync(() => target.params.toString()) - case "forEach": { - requireArgs(1) - const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach", node) - return Effect.gen(function* () { - for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) - return undefined - }) - } - default: - throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available.`, node) - } -} - -const invokeArrayMethod = ( - runner: Runner, - receiver: ProgramArray, - name: string, - args: Array, - node: AstNode, -): Effect.Effect => { - const target = receiver.items - const optNumber = (value: unknown, label: string): number | undefined => { - if (value === undefined) return undefined - if (typeof value !== "number") - throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) - return value - } - switch (name) { - case "join": { - if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { - throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) - } - return Effect.succeed( - target.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)), - ) - } - case "includes": - if (args.length === 0 || args.length > 2) - throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) - return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index"))) - case "indexOf": - return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index"))) - case "lastIndexOf": - return Effect.succeed( - args[1] === undefined - ? target.lastIndexOf(args[0]) - : target.lastIndexOf(args[0], optNumber(args[1], "start index")), - ) - case "at": - return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0)) - case "slice": - return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end"))) - case "concat": - return Effect.succeed(target.concat(...args.map((item) => (item instanceof ProgramArray ? item.items : item)))) - case "flat": { - const flatten = (items: Array, depth: number): Array => - items.flatMap((item) => (item instanceof ProgramArray && depth > 0 ? flatten(item.items, depth - 1) : [item])) - return Effect.succeed(flatten(target, optNumber(args[0], "depth") ?? 1)) - } - case "reverse": - target.reverse() - return Effect.succeed(receiver) - case "sort": { - const length = target.length - const holeCount = Array.from({ length }, (_, index) => Object.hasOwn(target, index)).filter((own) => !own).length - const itemCount = length - holeCount - return Effect.map(sortArray(runner, target, args[0], "Array.sort", node), (sorted) => { - sorted.slice(0, itemCount).forEach((item, index) => { - target[index] = item - }) - Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => { - Reflect.deleteProperty(target, index) - }) - return receiver - }) - } - case "toSorted": - return sortArray(runner, target, args[0], "Array.toSorted", node) - case "toReversed": - return Effect.succeed([...target].reverse()) - case "with": { - const index = optNumber(args[0], "index") ?? 0 - const resolved = index < 0 ? target.length + index : index - if (resolved < 0 || resolved >= target.length) { - throw rangeError("Array.with index is out of range.", node) - } - const copied = [...target] - copied[resolved] = args[1] - return Effect.succeed(copied) - } - case "push": { - // Validate all insertions before mutating to avoid partial cyclic updates. - for (const item of args) rejectCircularInsertion(receiver, item, "Array.push result", node) - target.push(...args) - return Effect.succeed(target.length) - } - case "unshift": { - for (const item of args) rejectCircularInsertion(receiver, item, "Array.unshift result", node) - target.unshift(...args) - return Effect.succeed(target.length) - } - case "pop": - return Effect.succeed(target.pop()) - case "shift": - return Effect.succeed(target.shift()) - case "splice": { - if (args.length === 0) return Effect.succeed(target.splice(0, 0)) - const start = optNumber(args[0], "start") ?? 0 - if (args.length === 1) return Effect.succeed(target.splice(start)) - const deleteCount = optNumber(args[1], "delete count") ?? 0 - const inserted = args.slice(2) - for (const item of inserted) rejectCircularInsertion(receiver, item, "Array.splice result", node) - return Effect.succeed(target.splice(start, deleteCount, ...inserted)) - } - case "toSpliced": { - if (args.length === 0) return Effect.succeed([...target]) - const start = optNumber(args[0], "start") ?? 0 - if (args.length === 1) { - const copied = [...target] - copied.splice(start) - return Effect.succeed(copied) - } - const deleteCount = optNumber(args[1], "delete count") ?? 0 - const copied = [...target] - copied.splice(start, deleteCount, ...args.slice(2)) - return Effect.succeed(copied) - } - case "fill": { - rejectCircularInsertion(receiver, args[0], "Array.fill result", node) - target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")) - return Effect.succeed(receiver) - } - case "copyWithin": - target.copyWithin( - optNumber(args[0], "target index") ?? 0, - optNumber(args[1], "start") ?? 0, - optNumber(args[2], "end"), - ) - return Effect.succeed(receiver) - case "keys": - return Effect.succeed(Array.from(target.keys())) - case "values": - return Effect.succeed([...target]) - case "entries": - return Effect.succeed(Array.from(target.entries(), ([index, item]) => new ProgramArray([index, item]))) - } - - const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node) - return Effect.gen(function* () { - // Fix iteration length while reading existing elements live. - const length = target.length - switch (name) { - case "map": { - const values: Array = [] - values.length = length - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - values[index] = yield* apply([target[index], index, receiver]) - } - return values - } - case "flatMap": { - const values: Array = [] - 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) - else values.push(mapped) - } - return values - } - case "filter": { - const values: Array = [] - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - const item = target[index] - if (yield* apply([item, index, receiver])) values.push(item) - } - return values - } - case "find": - for (let index = 0; index < length; index += 1) { - const item = target[index] - if (yield* apply([item, index, receiver])) return item - } - return undefined - case "findIndex": - for (let index = 0; index < length; index += 1) { - if (yield* apply([target[index], index, receiver])) return index - } - return -1 - case "some": - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - if (yield* apply([target[index], index, receiver])) return true - } - return false - case "every": - for (let index = 0; index < length; index += 1) { - if (!(index in target)) continue - if (!(yield* apply([target[index], index, receiver]))) return false - } - return true - case "forEach": - for (let index = 0; index < length; index += 1) { - if (index in target) yield* apply([target[index], index, receiver]) - } - return undefined - case "reduce": { - let start = 0 - let accumulator = args[1] - if (args.length < 2) { - while (start < length && !(start in target)) start += 1 - if (start === length) - throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) - accumulator = target[start] - start += 1 - } - for (let index = start; index < length; index += 1) { - if (!(index in target)) continue - accumulator = yield* apply([accumulator, target[index], index, receiver]) - } - return accumulator - } - case "reduceRight": { - let start = length - 1 - let accumulator = args[1] - if (args.length < 2) { - while (start >= 0 && !(start in target)) start -= 1 - if (start < 0) - throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) - accumulator = target[start] - start -= 1 - } - for (let index = start; index >= 0; index -= 1) { - if (!(index in target)) continue - accumulator = yield* apply([accumulator, target[index], index, receiver]) - } - return accumulator - } - case "findLast": - for (let index = length - 1; index >= 0; index -= 1) { - const item = target[index] - if (yield* apply([item, index, receiver])) return item - } - return undefined - case "findLastIndex": - for (let index = length - 1; index >= 0; index -= 1) { - if (yield* apply([target[index], index, receiver])) return index - } - return -1 - } - throw new InterpreterRuntimeError(`Array method '${name}' is not available.`, node) - }) -} - -const sortArray = ( - runner: Runner, - target: Array, - comparator: unknown, - name: string, - node: AstNode, -): Effect.Effect, unknown, R> => { - if (comparator === undefined) { - return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b)))) - } - const apply = applyCollectionCallback(runner, comparator, name, node) - const mergeSort = (items: Array): Effect.Effect, unknown, R> => { - if (items.length <= 1) return Effect.succeed(items) - const midpoint = Math.floor(items.length / 2) - return Effect.gen(function* () { - const left = yield* mergeSort(items.slice(0, midpoint)) - const right = yield* mergeSort(items.slice(midpoint)) - const merged: Array = [] - let leftIndex = 0 - let rightIndex = 0 - while (leftIndex < left.length && rightIndex < right.length) { - // Treat a NaN comparator result as equal to preserve stable ordering. - const order = coerceToNumber(yield* apply([left[leftIndex], right[rightIndex]])) - if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) - else merged.push(right[rightIndex++]) - } - return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] - }) - } - const defined = target.filter((item) => item !== undefined) - const undefinedCount = target.length - defined.length - return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) -} diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index 650433646664..a716ed26e922 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -1,9 +1,6 @@ import type { Node } from "acorn" import type { ErrorType } from "./intrinsics.js" -import type { Effect } from "effect" import type { DiagnosticKind } from "../codemode.js" -import type { ProgramObject } from "./objects.js" -import type { Values } from "../values.js" /** Any parsed node; the interpreter narrows on `type` and reads `loc` for diagnostics. */ export type AstNode = Node @@ -20,55 +17,12 @@ export type StatementResult = | { kind: "break"; label?: string } | { kind: "continue"; label?: string } -export type MemberReference = { - target: ProgramObject | Values.RegExp | Values.URL - key: PropertyKey -} - export type GeneratorRequestKind = "next" | "return" | "throw" -export class CodeModeGenerator { - constructor( - readonly asynchronous: boolean, - readonly request: ( - kind: GeneratorRequestKind, - value: unknown, - node: AstNode, - ) => Effect.Effect, - ) {} -} - -export class GeneratorMethodReference { - constructor( - readonly generator: CodeModeGenerator, - readonly kind: GeneratorRequestKind | "iterator", - ) {} -} - -export class IntrinsicReference { - constructor( - readonly receiver: unknown, - readonly name: string, - ) {} -} - -export class ComputedValue { - constructor(readonly value: unknown) {} -} - export const AsyncIteratorSymbol: unique symbol = Symbol("codemode.async-iterator") export const IteratorSymbol: unique symbol = Symbol("codemode.iterator") export const IteratorSymbols = [AsyncIteratorSymbol, IteratorSymbol] as const -export type PromiseInstanceMethodName = "then" | "catch" | "finally" - -export class PromiseInstanceMethodReference { - constructor( - readonly promise: Values.Promise, - readonly name: PromiseInstanceMethodName, - ) {} -} - export class ProgramThrow { constructor(readonly value: unknown) {} } @@ -96,6 +50,12 @@ export class InterpreterRuntimeError extends Error { } } +/** Attaches a source location to a failure raised where none was known, such as inside a property accessor. */ +export const locate = (error: unknown, node: AstNode): unknown => + error instanceof InterpreterRuntimeError && error.node === undefined + ? new InterpreterRuntimeError(error.message, node, error.kind, error.suggestions, error.type) + : error + const failure = (type: ErrorType) => (message: string, node?: AstNode) => new InterpreterRuntimeError(message, node, "ExecutionFailure", undefined, type) diff --git a/packages/codemode/src/interpreter/native.ts b/packages/codemode/src/interpreter/native.ts new file mode 100644 index 000000000000..14a94af89540 --- /dev/null +++ b/packages/codemode/src/interpreter/native.ts @@ -0,0 +1,69 @@ +import { Effect } from "effect" +import type { Prototypes } from "./intrinsics.js" +import { type AstNode, InterpreterRuntimeError } from "./model.js" +import { type Callable, define, frozen, hidden, NativeFunction, type NativeOptions, ProgramObject } from "./objects.js" +import { describeValue } from "./references.js" + +/** A native function body: a plain value, a thrown `InterpreterRuntimeError`, or an Effect. */ +export type Impl = (thisValue: unknown, args: Array, node: AstNode) => unknown + +const lift = + (impl: Impl) => + (thisValue: unknown, args: Array, node: AstNode): Effect.Effect => + Effect.suspend(() => { + const result = impl(thisValue, args, node) + return Effect.isEffect(result) ? (result as Effect.Effect) : Effect.succeed(result) + }) + +export const native = (protos: Prototypes, options: NativeOptions): NativeFunction => + new NativeFunction(protos.Function, options) + +export const fn = (protos: Prototypes, name: string, length: number, impl: Impl): NativeFunction => + native(protos, { name, length, call: lift(impl) }) + +export type Method = readonly [name: string, length: number, impl: Impl] + +export const methods = (protos: Prototypes, target: ProgramObject, table: ReadonlyArray): void => { + for (const [name, length, impl] of table) define(target, name, fn(protos, name, length, impl), hidden) +} + +export const constants = (target: ProgramObject, table: Record): void => { + for (const [name, value] of Object.entries(table)) define(target, name, value, frozen) +} + +/** A constructor wired to its prototype: `C.prototype === proto` and `proto.constructor === C`. */ +export const constructor = ( + protos: Prototypes, + proto: ProgramObject, + options: NativeOptions, +): NativeFunction => { + const ctor = native(protos, options) + define(ctor, "prototype", proto, frozen) + define(proto, "constructor", ctor, hidden) + return ctor +} + +/** The `call` of a constructor that JS requires to be invoked with `new`. */ +export const requiresNew = + (name: string) => + (_: unknown, __: Array, node: AstNode): Effect.Effect => + Effect.sync(() => { + throw new InterpreterRuntimeError(`Constructor ${name} requires 'new'.`, node) + }) + +/** The instance prototype for `new` via `newTarget.prototype`, falling back to the built-in's own. */ +export const prototypeFrom = (newTarget: Callable, fallback: ProgramObject): ProgramObject => { + const proto = newTarget.props.get("prototype") + return proto !== undefined && "value" in proto && proto.value instanceof ProgramObject ? proto.value : fallback +} + +/** Narrows a method receiver to the built-in it belongs to, or throws the TypeError JS would. */ +export const receiver = ( + cls: abstract new (...args: never) => T, + thisValue: unknown, + method: string, + node?: AstNode, +): T => { + if (thisValue instanceof cls) return thisValue + throw new InterpreterRuntimeError(`${method} called on incompatible receiver ${describeValue(thisValue)}.`, node) +} diff --git a/packages/codemode/src/interpreter/objects.ts b/packages/codemode/src/interpreter/objects.ts index 582ce5db185d..87c97d66c5ef 100644 --- a/packages/codemode/src/interpreter/objects.ts +++ b/packages/codemode/src/interpreter/objects.ts @@ -1,37 +1,187 @@ import type { BlockStatement, Expression, Pattern } from "acorn" -import { AsyncIteratorSymbol, type Binding, IteratorSymbol } from "./model.js" +import type { Effect, Fiber } from "effect" +import { type AstNode, AsyncIteratorSymbol, type Binding, type GeneratorRequestKind, IteratorSymbol } from "./model.js" + +/** Property attributes, as in a JS property descriptor. */ +export type Attributes = { + readonly writable: boolean + readonly enumerable: boolean + readonly configurable: boolean +} + +export type Getter = (receiver: unknown) => unknown +export type Setter = (receiver: unknown, value: unknown) => void + +/** One own property: a data slot or a native accessor pair. */ +export type Slot = + | { value: unknown; writable: boolean; enumerable: boolean; configurable: boolean } + | { get: Getter | undefined; set: Setter | undefined; enumerable: boolean; configurable: boolean } + +/** Ordinary assignment: writable, enumerable, configurable. */ +export const data: Attributes = { writable: true, enumerable: true, configurable: true } +/** Built-in methods and `constructor`: writable and configurable but hidden from enumeration. */ +export const hidden: Attributes = { writable: true, enumerable: false, configurable: true } +/** Function `name` and `length`: read-only but deletable. */ +export const readonly: Attributes = { writable: false, enumerable: false, configurable: true } +/** Constants such as `Math.PI` and a constructor's `prototype`. */ +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 { - readonly props = new Map() - constructor(public proto: ProgramObject | null = null) {} + readonly props = new Map() + constructor(public proto: ProgramObject | null) {} } export class ProgramArray extends ProgramObject { - constructor(readonly items: Array = []) { - super() + constructor( + proto: ProgramObject, + readonly items: Array = [], + ) { + super(proto) } } /** 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 ProgramFunction extends ProgramObject { - readonly length: number +export abstract class Callable extends ProgramObject { + constructor(proto: ProgramObject, name: string, length: number) { + super(proto) + define(this, "length", length, readonly) + define(this, "name", name, readonly) + } +} + +export class ProgramFunction extends Callable { constructor( - readonly name: string, + proto: ProgramObject, + name: string, readonly parameters: ReadonlyArray, readonly body: BlockStatement | Expression, readonly capturedScopes: ReadonlyArray>, readonly async: boolean, readonly generator: boolean, ) { - super() const optional = parameters.findIndex((p) => p.type === "AssignmentPattern" || p.type === "RestElement") - this.length = optional === -1 ? parameters.length : optional + super(proto, name, optional === -1 ? parameters.length : optional) + } +} + +export type NativeCall = ( + thisValue: unknown, + args: Array, + node: AstNode, +) => Effect.Effect +export type NativeConstruct = ( + args: Array, + newTarget: Callable, + node: AstNode, +) => Effect.Effect + +export type NativeOptions = { + readonly name: string + readonly length?: number + readonly call: NativeCall + /** `new name(...)`; without it the function is not a constructor. */ + readonly construct?: NativeConstruct + /** Whether callback sites (array methods, replacers, promise reactions) admit this function. Defaults to true. */ + readonly callback?: boolean +} + +export class NativeFunction extends Callable { + readonly call: NativeCall + readonly construct: NativeConstruct | undefined + readonly callback: boolean + + constructor(proto: ProgramObject, options: NativeOptions) { + super(proto, options.name, options.length ?? 0) + this.call = options.call + this.construct = options.construct + this.callback = options.callback ?? true + } +} + +export class ProgramPromise extends ProgramObject { + constructor( + proto: ProgramObject, + readonly fiber: Fiber.Fiber, + ) { + super(proto) + } +} + +export class ProgramGenerator extends ProgramObject { + constructor( + proto: ProgramObject, + readonly asynchronous: boolean, + readonly request: ( + kind: GeneratorRequestKind, + value: unknown, + node: AstNode, + ) => Effect.Effect, + ) { + super(proto) + } +} + +export class ProgramDate extends ProgramObject { + constructor( + proto: ProgramObject, + public time: number, + ) { + super(proto) + } +} + +export class ProgramRegExp extends ProgramObject { + readonly regex: RegExp + constructor(proto: ProgramObject, 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 { + readonly map = new Map() +} + +export class ProgramSet extends ProgramObject { + readonly set = new Set() +} + +export class ProgramURLSearchParams extends ProgramObject { + constructor( + proto: ProgramObject, + readonly params: URLSearchParams, + ) { + super(proto) + } +} + +export class ProgramURL extends ProgramObject { + readonly searchParams: ProgramURLSearchParams + constructor( + proto: ProgramObject, + searchParamsProto: ProgramObject, + readonly url: URL, + ) { + super(proto) + this.searchParams = new ProgramURLSearchParams(searchParamsProto, url.searchParams) } } +/** 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 + const MAX_ARRAY_LENGTH = 4_294_967_295 export const parseArrayIndex = (key: string | number): number | undefined => { @@ -46,35 +196,45 @@ const canonical = (key: PropertyKey): string | symbol => (typeof key === "symbol const index = (target: ProgramObject, key: string | symbol): number | undefined => target instanceof ProgramArray && typeof key === "string" ? parseArrayIndex(key) : undefined -// Non-enumerable built-in properties: array length, function name and length. -const builtin = (target: ProgramObject, name: string | symbol): boolean => - (name === "length" && target instanceof ProgramArray) || - ((name === "name" || name === "length") && target instanceof ProgramFunction) - -export const hasOwn = (target: ProgramObject, key: PropertyKey): boolean => { +/** The own property under `key`, including an array's live indexes and `length`. */ +export const own = (target: ProgramObject, key: PropertyKey): Slot | undefined => { const name = canonical(key) - const at = index(target, name) - if (at !== undefined) return at in (target as ProgramArray).items - return builtin(target, name) || target.props.has(name) + if (target instanceof ProgramArray) { + const at = index(target, name) + if (at !== undefined) { + return at in target.items ? { value: target.items[at], ...data } : undefined + } + if (name === "length") return { value: target.items.length, writable: true, enumerable: false, configurable: false } + } + return target.props.get(name) } +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 getOwn = (target: ProgramObject, key: PropertyKey): unknown => { - const name = canonical(key) - const at = index(target, name) - if (at !== undefined) return (target as ProgramArray).items[at] - if (target instanceof ProgramArray && name === "length") return target.items.length - if (target instanceof ProgramFunction && name === "name") return target.name - if (target instanceof ProgramFunction && name === "length") return target.length - return target.props.get(name) + const slot = own(target, key) + return slot === undefined ? undefined : read(slot, target) } -export const get = (target: ProgramObject, key: PropertyKey): unknown => { +/** [[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) { - if (hasOwn(current, key)) return getOwn(current, key) + 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) { + 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) { if (current === proto) return true @@ -82,37 +242,68 @@ export const hasPrototype = (value: unknown, proto: ProgramObject): boolean => { return false } -export const has = (target: ProgramObject, key: PropertyKey): boolean => { - for (let current: ProgramObject | null = target; current !== null; current = current.proto) { - if (hasOwn(current, key)) return true +const writeArray = (target: ProgramArray, name: string | symbol, value: unknown): boolean | undefined => { + const at = index(target, name) + if (at !== undefined) { + target.items[at] = value + return true } - return false + if (name !== "length") return undefined + const length = typeof value === "number" ? value : Number(value) + if (!Number.isInteger(length) || length < 0 || length > MAX_ARRAY_LENGTH) return false + target.items.length = length + return true } +/** [[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 => { const name = canonical(key) - const at = index(target, name) - if (at !== undefined) { - ;(target as ProgramArray).items[at] = value + for (let current: ProgramObject | null = target; current !== null; current = current.proto) { + const slot = own(current, name) + if (slot === undefined) continue + if (!("value" in slot)) { + if (slot.set === undefined) return false + slot.set(target, value) + return true + } + if (!slot.writable) return false + if (current !== target) break + if (target instanceof ProgramArray) { + const written = writeArray(target, name, value) + if (written !== undefined) return written + } + slot.value = value return true } - if (name === "length" && target instanceof ProgramArray) { - const length = typeof value === "number" ? value : Number(value) - if (!Number.isInteger(length) || length < 0 || length > 4_294_967_295) return false - target.items.length = length - return true + if (target instanceof ProgramArray) { + const written = writeArray(target, name, value) + if (written !== undefined) return written } - if (builtin(target, name)) return false - target.props.set(name, value) + target.props.set(name, { value, ...data }) return true } +/** [[DefineOwnProperty]] for a data property, ignoring the chain. */ +export const define = (target: ProgramObject, key: PropertyKey, value: unknown, attrs: Attributes = data): void => { + const name = canonical(key) + if (target instanceof ProgramArray && writeArray(target, name, value) !== undefined) return + target.props.set(name, { value, ...attrs }) +} + +export const defineAccessor = (target: ProgramObject, key: PropertyKey, get: Getter, set?: Setter): void => { + target.props.set(canonical(key), { get, set, enumerable: false, configurable: true }) +} + export const remove = (target: ProgramObject, key: PropertyKey): boolean => { const name = canonical(key) - const at = index(target, name) - if (at !== undefined) return delete (target as ProgramArray).items[at] - if (name === "length" && target instanceof ProgramArray) return false - if (builtin(target, name)) return true + if (target instanceof ProgramArray) { + const at = index(target, name) + if (at !== undefined) return delete target.items[at] + if (name === "length") return false + } + const slot = target.props.get(name) + if (slot === undefined) return true + if (!slot.configurable) return false target.props.delete(name) return true } @@ -122,28 +313,39 @@ export const ownKeys = (target: ProgramObject): Array => { const strings = [...target.props.keys()].filter((key): key is string => typeof key === "string") const symbols = [...target.props.keys()].filter((key): key is symbol => typeof key === "symbol") return [ - ...(target instanceof ProgramArray ? Object.keys(target.items) : []), + ...(target instanceof ProgramArray ? [...Object.keys(target.items), "length"] : []), ...strings.filter((key) => parseArrayIndex(key) !== undefined).sort((a, b) => Number(a) - Number(b)), ...strings.filter((key) => parseArrayIndex(key) === undefined), ...symbols, ] } -export const ownEntries = (target: ProgramObject): Array<[string, unknown]> => - ownKeys(target) - .filter((key): key is string => typeof key === "string") - .map((key) => [key, getOwn(target, key)]) +const enumerable = (target: ProgramObject, 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 => + 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 => + 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 record = (entries: Record): ProgramObject => { - const target = new ProgramObject() - for (const [key, value] of Object.entries(entries)) set(target, key, value) +export const record = (proto: ProgramObject, fields: Record): ProgramObject => { + const target = new ProgramObject(proto) + for (const [key, value] of Object.entries(fields)) define(target, key, value) return target } export const assign = (target: ProgramObject, source: ProgramObject, skip?: ReadonlySet): void => { - for (const key of ownKeys(source)) { + for (const key of enumerableKeys(source)) { if (skip?.has(key)) continue - if (typeof key === "symbol" && key !== IteratorSymbol && key !== AsyncIteratorSymbol) continue set(target, key, getOwn(source, key)) } } diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts index 43e987699ac6..9c591a0dd1fe 100644 --- a/packages/codemode/src/interpreter/promises.ts +++ b/packages/codemode/src/interpreter/promises.ts @@ -1,47 +1,59 @@ import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect" import type { Diagnostic } from "../codemode.js" -import { type AstNode, InterpreterRuntimeError, ProgramThrow, PromiseInstanceMethodReference } from "./model.js" -import { get, ProgramArray, ProgramFunction, ProgramObject, record } from "./objects.js" -import { HostFunction, requiresNew, sync } from "./host.js" +import { type AstNode, InterpreterRuntimeError, ProgramThrow } from "./model.js" +import { + Callable, + define, + get, + hidden, + ProgramArray, + ProgramFunction, + ProgramObject, + ProgramPromise, + record, +} from "./objects.js" +import { constructor, fn, methods, native, receiver, requiresNew } from "./native.js" import { caughtErrorValue, createAggregateErrorValue, normalizeError } from "./errors.js" import { typeofValue } from "./references.js" -import { Values } from "../values.js" -import { applyCollectionCallback, isSupportedCallback, type Runner, type SupportedCallback } from "./runner.js" +import { applyCollectionCallback, isSupportedCallback, type Runner } from "./runner.js" // A `resolve`/`reject` handed to an executor or thenable: calling it settles the capability. -const capability = (name: string, settle: (value: unknown) => void) => - sync(name, (args) => { +const capability = (runner: Runner, name: string, settle: (value: unknown) => void) => + fn(runner.prototypes, name, 1, (_, args) => { settle(args[0]) return undefined }) // Observation only controls rejection reporting; program completion interrupts all promise work. export class PromiseRuntime { - private readonly active = new Set() - private readonly ids = new WeakMap() - private readonly observed = new WeakSet() + private readonly active = new Set() + private readonly ids = new WeakMap() + private readonly observed = new WeakSet() private readonly failures = new Map() private nextID = 0 - constructor(private readonly scope: Scope.Scope) {} + constructor( + private readonly scope: Scope.Scope, + private readonly proto: ProgramObject, + ) {} // Resolution bodies need the promise's own identity to reject `resolve(promise)` self-resolution. createWithSelf( - body: (self: { promise?: Values.Promise }) => Effect.Effect, - ): Effect.Effect { - const self: { promise?: Values.Promise } = {} + body: (self: { promise?: ProgramPromise }) => Effect.Effect, + ): Effect.Effect { + const self: { promise?: ProgramPromise } = {} return Effect.map(this.create(body(self)), (promise) => { self.promise = promise return promise }) } - create(effect: Effect.Effect): Effect.Effect { + create(effect: Effect.Effect): Effect.Effect { return Effect.suspend(() => { // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order. const id = this.nextID++ return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => { - const promise = new Values.Promise(fiber) + const promise = new ProgramPromise(this.proto, fiber) this.active.add(promise) this.ids.set(promise, id) fiber.addObserver((exit) => { @@ -62,14 +74,14 @@ export class PromiseRuntime { } // Observation must be recorded when responsibility transfers, before the consumer fiber runs. - markObserved(promise: Values.Promise): void { + markObserved(promise: ProgramPromise): void { this.observed.add(promise) const id = this.ids.get(promise) this.ids.delete(promise) if (id !== undefined) this.failures.delete(id) } - await(promise: Values.Promise): Effect.Effect> { + await(promise: ProgramPromise): Effect.Effect> { return Fiber.await(promise.fiber) } @@ -100,10 +112,10 @@ export const resolvePromiseValue = ( runner: Runner, value: unknown, node: AstNode, - own?: { promise?: Values.Promise }, + own?: { promise?: ProgramPromise }, ): Effect.Effect => { if (own?.promise !== undefined && value === own.promise) return Effect.fail(selfResolutionError(node)) - if (value instanceof Values.Promise) return runner.settlePromise(value) + if (value instanceof ProgramPromise) return runner.settlePromise(value) if (!(value instanceof ProgramObject)) return Effect.succeed(value) const then = get(value, "then") if (typeofValue(then) !== "function") return Effect.succeed(value) @@ -112,9 +124,11 @@ export const resolvePromiseValue = ( // Promise resolution invokes a thenable's method in a later job. yield* Effect.yieldNow const deferred = Deferred.makeUnsafe() - const resolve = capability("resolve", (result) => Deferred.doneUnsafe(deferred, Exit.succeed(result))) - const reject = capability("reject", (reason) => Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(reason)))) - const executed = yield* Effect.exit(runner.invokeCallable(then, [resolve, reject], node)) + 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], node)) if (!Exit.isSuccess(executed)) { if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause) Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause))) @@ -128,8 +142,8 @@ export const resolvePromise = ( promises: PromiseRuntime, value: unknown, node: AstNode, -): Effect.Effect => { - if (value instanceof Values.Promise) return Effect.succeed(value) +): Effect.Effect => { + if (value instanceof ProgramPromise) return Effect.succeed(value) return promises.createWithSelf((self) => resolvePromiseValue(runner, value, node, self)) } @@ -155,7 +169,7 @@ const invokePromiseMethod = ( if (cursor === undefined) { throw new InterpreterRuntimeError(`Promise.${name} expects an array or other synchronous iterable.`, node) } - const items: Array = [] + const items: Array = [] while (true) { const step = yield* cursor.next if (step.done) break @@ -166,6 +180,7 @@ const invokePromiseMethod = ( if (name === "all") { return new ProgramArray( + runner.prototypes.Array, yield* settleAfterTurn( Effect.all( items.map((item) => Effect.flatten(promises.await(item))), @@ -179,14 +194,19 @@ const invokePromiseMethod = ( for (const item of items) { const exit = yield* promises.await(item) if (Exit.isSuccess(exit)) { - outcomes.push(record({ status: "fulfilled", value: exit.value })) + outcomes.push(record(runner.prototypes.Object, { status: "fulfilled", value: exit.value })) continue } if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause) - outcomes.push(record({ status: "rejected", reason: caughtErrorValue(runner, Cause.squash(exit.cause)) })) + outcomes.push( + record(runner.prototypes.Object, { + status: "rejected", + reason: caughtErrorValue(runner, Cause.squash(exit.cause)), + }), + ) } yield* Effect.yieldNow - return new ProgramArray(outcomes) + return new ProgramArray(runner.prototypes.Array, outcomes) } if (name === "race") { if (items.length === 0) { @@ -218,21 +238,23 @@ const invokePromiseMethod = ( ) } -export const invokePromiseInstanceMethod = ( +const instanceMethod = ( runner: Runner, promises: PromiseRuntime, - ref: PromiseInstanceMethodReference, + name: "then" | "catch" | "finally", + thisValue: unknown, args: Array, node: AstNode, -): Effect.Effect => { - const method = `Promise.prototype.${ref.name}` - promises.markObserved(ref.promise) - if (ref.name === "finally") { - return chainFinally(runner, promises, ref.promise, reactionHandler(args[0], method, node), method, node) +): Effect.Effect => { + const method = `Promise.prototype.${name}` + const promise = receiver(ProgramPromise, thisValue, method, node) + promises.markObserved(promise) + if (name === "finally") { + return chainFinally(runner, promises, promise, reactionHandler(args[0], method, node), method, node) } - const onFulfilled = ref.name === "then" ? reactionHandler(args[0], method, node) : undefined - const onRejected = reactionHandler(ref.name === "then" ? args[1] : args[0], method, node) - return chainReaction(runner, promises, ref.promise, onFulfilled, onRejected, method, node) + const onFulfilled = name === "then" ? reactionHandler(args[0], method, node) : undefined + const onRejected = reactionHandler(name === "then" ? args[1] : args[0], method, node) + return chainReaction(runner, promises, promise, onFulfilled, onRejected, method, node) } const constructPromise = ( @@ -240,7 +262,7 @@ const constructPromise = ( promises: PromiseRuntime, executor: unknown, node: AstNode, -): Effect.Effect => { +): Effect.Effect => { if (!(executor instanceof ProgramFunction)) { throw new InterpreterRuntimeError( "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).", @@ -252,9 +274,11 @@ const constructPromise = ( const promise = yield* promises.createWithSelf((self) => Effect.flatMap(Deferred.await(deferred), (value) => resolvePromiseValue(runner, value, node, self)), ) - const resolve = capability("resolve", (value) => Deferred.doneUnsafe(deferred, Exit.succeed(value))) - const reject = capability("reject", (value) => Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value)))) - const executed = yield* Effect.exit(runner.invokeFunction(executor, [resolve, reject])) + 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], node)) if (!Exit.isSuccess(executed)) { if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause) Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause))) @@ -271,7 +295,7 @@ class PromiseAnyFulfilled { constructor(readonly value: unknown) {} } -const reactionHandler = (value: unknown, method: string, node: AstNode): SupportedCallback | undefined => { +const reactionHandler = (value: unknown, method: string, node: AstNode): Callable | undefined => { if (isSupportedCallback(value)) return value if (typeofValue(value) === "function") { throw new InterpreterRuntimeError( @@ -285,7 +309,7 @@ const reactionHandler = (value: unknown, method: string, node: AstNode): Support // Teardown bypasses handlers; settled reactions yield once so handlers never run inline. const reactionExit = ( promises: PromiseRuntime, - source: Values.Promise, + source: ProgramPromise, ): Effect.Effect, unknown, R> => Effect.gen(function* () { const exit = yield* promises.await(source) @@ -297,12 +321,12 @@ const reactionExit = ( const chainReaction = ( runner: Runner, promises: PromiseRuntime, - source: Values.Promise, - onFulfilled: SupportedCallback | undefined, - onRejected: SupportedCallback | undefined, + source: ProgramPromise, + onFulfilled: Callable | undefined, + onRejected: Callable | undefined, method: string, node: AstNode, -): Effect.Effect => { +): Effect.Effect => { return promises.createWithSelf((self) => Effect.gen(function* () { const exit = yield* reactionExit(promises, source) @@ -318,11 +342,11 @@ const chainReaction = ( const chainFinally = ( runner: Runner, promises: PromiseRuntime, - source: Values.Promise, - cleanup: SupportedCallback | undefined, + source: ProgramPromise, + cleanup: Callable | undefined, method: string, node: AstNode, -): Effect.Effect => +): Effect.Effect => promises.create( Effect.gen(function* () { const exit = yield* reactionExit(promises, source) @@ -341,30 +365,32 @@ const chainFinally = ( ) export const promiseGlobal = (runner: Runner, promises: PromiseRuntime) => { + const protos = runner.prototypes + const proto = protos.Promise + const promise = constructor(protos, proto, { + name: "Promise", + length: 1, + call: requiresNew("Promise"), + construct: (args, _, node) => constructPromise(runner, promises, args[0], node), + }) // Combinators are not callbacks: `[p].map(Promise.resolve)` must ask for an arrow function. - const statics = new Map>( - promiseStatics.map((name) => [ + for (const name of promiseStatics) { + define( + promise, name, - new HostFunction({ - name: `Promise.${name}`, - call: (args, node) => invokePromiseMethod(runner, promises, name, args, node), + native(protos, { + name, + length: 1, + call: (_, args, node) => invokePromiseMethod(runner, promises, name, args, node), callback: false, }), - ]), - ) - return new HostFunction({ - name: "Promise", - call: requiresNew("Promise"), - construct: (args, node) => constructPromise(runner, promises, args[0], node), - instanceOf: (value) => value instanceof Values.Promise, - // Unknown statics fail loudly so a missing await cannot hide behind `undefined`. - members: (key, node) => { - const method = typeof key === "string" ? statics.get(key) : undefined - if (method !== undefined) return method - throw new InterpreterRuntimeError( - `Promise.${String(key)} is not available. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`, - node, - ) - }, - }) + hidden, + ) + } + methods(protos, proto, [ + ["then", 2, (thisValue, args, node) => instanceMethod(runner, promises, "then", thisValue, args, node)], + ["catch", 1, (thisValue, args, node) => instanceMethod(runner, promises, "catch", thisValue, args, node)], + ["finally", 1, (thisValue, args, node) => instanceMethod(runner, promises, "finally", thisValue, args, node)], + ]) + return promise } diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index aaa603c8a003..0a49a35f7b6e 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -1,27 +1,29 @@ import { ToolReference } from "../tool-runtime.js" -import { Values } from "../values.js" -import { HostFunction, HostNamespace } from "./host.js" +import { type AstNode, InterpreterRuntimeError } from "./model.js" import { - type AstNode, - CodeModeGenerator, - GeneratorMethodReference, - InterpreterRuntimeError, - IntrinsicReference, - PromiseInstanceMethodReference, -} from "./model.js" -import { getOwn, ownKeys, ProgramArray, ProgramFunction, ProgramObject } from "./objects.js" + Callable, + getOwn, + isWrapper, + ownKeys, + ProgramArray, + ProgramDate, + ProgramGenerator, + ProgramMap, + ProgramObject, + ProgramPromise, + ProgramRegExp, + ProgramSet, + ProgramURL, + ProgramURLSearchParams, +} from "./objects.js" +/** Values that cannot cross the data boundary. */ export const isRuntimeReference = (value: unknown): boolean => - value instanceof HostFunction || - value instanceof HostNamespace || - value instanceof ProgramFunction || - value instanceof CodeModeGenerator || - value instanceof GeneratorMethodReference || + value instanceof Callable || + value instanceof ProgramGenerator || value instanceof ToolReference || - value instanceof IntrinsicReference || - value instanceof PromiseInstanceMethodReference || - value instanceof Values.Promise || - Values.isValue(value) + value instanceof ProgramPromise || + isWrapper(value) function* childValues(value: object): Generator { if (!(value instanceof ProgramObject)) return @@ -55,9 +57,9 @@ const never = () => false export const containsRuntimeReference = (value: unknown): boolean => find(value, isRuntimeReference, never, new Set()) -// CodeMode values are data here, not opaque interpreter references. +// Wrapper values are data here, not opaque interpreter references. export const containsOpaqueReference = (value: unknown): boolean => - find(value, (current) => !Values.isValue(current) && isRuntimeReference(current), Values.isValue, new Set()) + find(value, (current) => !isWrapper(current) && isRuntimeReference(current), isWrapper, new Set()) // Reject cycles before mutation so later boundary walks remain safe. export const rejectCircularInsertion = ( @@ -73,33 +75,24 @@ export const rejectCircularInsertion = ( } export const describeValue = (value: unknown): string => { - if (value === null) return "null" + if (value === null || value === undefined) return String(value) if (value instanceof ProgramArray) return "an array" - if (value instanceof Values.Promise) return "an un-awaited Promise" + if (value instanceof ProgramPromise) return "an un-awaited Promise" if (value instanceof ToolReference) return "a tool reference" - if (value instanceof Values.Date) return "a Date" - if (value instanceof Values.RegExp) return "a RegExp" - if (value instanceof Values.Map) return "a Map" - if (value instanceof Values.Set) return "a Set" - if (value instanceof Values.URL) return "a URL" - if (value instanceof Values.URLSearchParams) return "a URLSearchParams" - if (value instanceof CodeModeGenerator) return "a generator" + 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 (isRuntimeReference(value)) return "a function" if (typeof value === "object") return "a data object" return `a ${typeof value}` } export const typeofValue = (value: unknown): string => { - if ( - value instanceof HostFunction || - value instanceof ProgramFunction || - value instanceof GeneratorMethodReference || - value instanceof IntrinsicReference || - value instanceof PromiseInstanceMethodReference - ) { - return "function" - } - if (value instanceof HostNamespace) return "object" + if (value instanceof Callable) return "function" if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object" return typeof value } diff --git a/packages/codemode/src/interpreter/runner.ts b/packages/codemode/src/interpreter/runner.ts index 53aff3fd2e5a..2cc704c3c92c 100644 --- a/packages/codemode/src/interpreter/runner.ts +++ b/packages/codemode/src/interpreter/runner.ts @@ -1,10 +1,8 @@ import { Effect, Exit } from "effect" -import { Values } from "../values.js" -import { coerceToString } from "../stdlib/value.js" -import { HostFunction } from "./host.js" -import { type AstNode, InterpreterRuntimeError, IntrinsicReference } from "./model.js" -import type { Intrinsics } from "./intrinsics.js" -import { get, has, ProgramFunction, ProgramObject } from "./objects.js" +import { coerceToNumber, coerceToString } from "../stdlib/value.js" +import type { Prototypes } from "./intrinsics.js" +import { type AstNode, InterpreterRuntimeError } from "./model.js" +import { Callable, get, NativeFunction, ProgramDate, ProgramObject, ProgramPromise } from "./objects.js" import { typeofValue } from "./references.js" export type IteratorCursor = { @@ -12,17 +10,17 @@ export type IteratorCursor = { readonly close: Effect.Effect } -/** Everything a host function needs from the realm: calling back into the program and its intrinsic objects. */ +/** Everything a native function needs from the realm: calling back into the program and its intrinsic objects. */ export type Runner = { - readonly invokeFunction: (fn: ProgramFunction, args: Array) => Effect.Effect readonly invokeCallable: ( callable: unknown, + thisValue: unknown, args: Array, node: AstNode, ) => Effect.Effect - readonly settlePromise: (promise: Values.Promise) => Effect.Effect + readonly settlePromise: (promise: ProgramPromise) => Effect.Effect readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect | undefined, unknown, R> - readonly intrinsics: Intrinsics + readonly prototypes: Prototypes } export const preserveConsumerError = ( @@ -35,40 +33,42 @@ export const preserveConsumerError = ( : Effect.andThen(Effect.exit(cursor.close), Effect.failCause(exit.cause)), ) +/** + * ToPrimitive: calls `valueOf`/`toString` in hint order and returns the first primitive result. Dates treat the + * default hint as "string", like their `Symbol.toPrimitive`. + */ export const toPrimitive = ( runner: Runner, value: unknown, - hint: "number" | "string", + hint: "number" | "string" | "default", node: AstNode, ): Effect.Effect => { - if (value === null || typeof value !== "object") return Effect.succeed(value) - if (Values.isValue(value)) { - return Effect.succeed(value instanceof Values.Date && hint === "number" ? value.time : coerceToString(value)) - } if (!(value instanceof ProgramObject)) return Effect.succeed(value) - const order = hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"] + const asString = hint === "string" || (hint === "default" && value instanceof ProgramDate) + const order = asString ? ["toString", "valueOf"] : ["valueOf", "toString"] return Effect.gen(function* () { for (const method of order) { - if (method === "toString" && !has(value, "toString")) return coerceToString(value) const callable = get(value, method) - if (typeofValue(callable) !== "function") continue - const result = yield* runner.invokeCallable(callable, [], node) + if (!(callable instanceof Callable)) continue + const result = yield* runner.invokeCallable(callable, value, [], node) if (result === null || (typeof result !== "object" && typeof result !== "function")) return result } throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node) }) } +export const toPrimitiveString = (runner: Runner, value: unknown, node: AstNode) => + Effect.map(toPrimitive(runner, value, "string", node), coerceToString) + +export const toPrimitiveNumber = (runner: Runner, value: unknown, node: AstNode) => + Effect.map(toPrimitive(runner, value, "number", node), 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 type SupportedCallback = ProgramFunction | HostFunction | IntrinsicReference - -export const isSupportedCallback = (value: unknown): value is SupportedCallback => - value instanceof ProgramFunction || - (value instanceof HostFunction && value.callback) || - value instanceof IntrinsicReference +export const isSupportedCallback = (value: unknown): value is Callable => + value instanceof Callable && !(value instanceof NativeFunction && !value.callback) export const applyCollectionCallback = ( runner: Runner, @@ -85,5 +85,5 @@ export const applyCollectionCallback = ( } throw new InterpreterRuntimeError(`${name} expects a function callback.`, node) } - return (callbackArgs) => runner.invokeCallable(callback, callbackArgs, node) + return (callbackArgs) => runner.invokeCallable(callback, undefined, callbackArgs, node) } diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index d5cff8295d35..63d3a9734917 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -42,65 +42,56 @@ import type { YieldExpression, } from "acorn" import { Cause, Deferred, Effect, Exit } from "effect" -import { ToolRuntimeError, toProgram } from "../data.js" +import { toProgram } from "../data.js" import { ToolReference } from "../tool-runtime.js" import { type AstNode, AsyncIteratorSymbol, type Binding, - CodeModeGenerator, - ComputedValue, - GeneratorMethodReference, type GeneratorRequestKind, GeneratorReturn, InterpreterRuntimeError, - IntrinsicReference, IteratorSymbol, - type MemberReference, + locate, OptionalShortCircuit, ProgramThrow, - PromiseInstanceMethodReference, rangeError, type StatementResult, unsupportedSyntax, } from "./model.js" import { caughtErrorValue } from "./errors.js" -import { createIntrinsics } from "./intrinsics.js" +import type { Prototypes } from "./intrinsics.js" import { globals, type Host } from "./globals.js" -import { HostFunction, HostNamespace } from "./host.js" -import { invokeIntrinsic } from "./methods.js" import { assign, + Callable, + define, get, has, - ownKeys, + hasPrototype, + keys, + NativeFunction, parseArrayIndex, ProgramArray, + ProgramDate, ProgramFunction, + ProgramGenerator, + ProgramMap, ProgramObject, + ProgramPromise, + ProgramSet, + ProgramURLSearchParams, record, remove, set, } from "./objects.js" import { preserveConsumerError, type Runner } from "./runner.js" -import { invokePromiseInstanceMethod, PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js" -import { - containsOpaqueReference, - describeValue, - isRuntimeReference, - rejectCircularInsertion, - typeofValue, -} from "./references.js" +import { PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js" +import { containsOpaqueReference, describeValue, rejectCircularInsertion, typeofValue } from "./references.js" import { ScopeStack } from "./scope.js" -import { arrayMethods, mapMethods, setMethods } from "../stdlib/collections.js" -import { dateMethods } from "../stdlib/date.js" -import { numberMethods } from "../stdlib/number.js" -import { constructRegExp, regexpMethods, regexpProperties } from "../stdlib/regexp.js" -import { stringMethods } from "../stdlib/string.js" -import { uriArgument, urlMethods, urlProperties, urlSearchParamsMethods, urlWritableProperties } from "../stdlib/url.js" +import { constructRegExp } from "../stdlib/regexp.js" import { enumerableSource } from "../stdlib/object.js" import { coerceToNumber, coerceToString, compoundOperators } from "../stdlib/value.js" -import { Values } from "../values.js" // What a loop does with its body's result: exit with a StatementResult, or undefined to keep iterating. // Unlabelled break ends this loop; a label the loop does not carry propagates outward. @@ -130,28 +121,16 @@ const calleeDescription = (callee: Expression | Super | undefined): string => { return "The called value" } -const constructorName = (value: unknown): string | undefined => { - if (typeof value === "string") return "String" - if (typeof value === "number") return "Number" - if (typeof value === "boolean") return "Boolean" - if (value instanceof ProgramArray) return "Array" - if (value instanceof Values.Date) return "Date" - if (value instanceof Values.RegExp) return "RegExp" - if (value instanceof Values.Map) return "Map" - if (value instanceof Values.Set) return "Set" - if (value instanceof Values.URL) return "URL" - if (value instanceof Values.URLSearchParams) return "URLSearchParams" - if (value instanceof Values.Promise) return "Promise" - if (!(value instanceof ProgramObject) || value instanceof ProgramFunction) return undefined - return "Object" -} - +// OrdinaryHasInstance: walk the left operand's chain looking for the constructor's `prototype`. const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { - if (rhs instanceof HostFunction && rhs.instanceOf !== undefined) return rhs.instanceOf(lhs) - throw new InterpreterRuntimeError( - "The right-hand side of 'instanceof' must be a supported constructor: Error (or a specific error type like TypeError), Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, or Promise.", - node, - ) + if (!(rhs instanceof Callable)) { + throw new InterpreterRuntimeError("The right-hand side of 'instanceof' is not callable.", node) + } + const prototype = get(rhs, "prototype") + if (!(prototype instanceof ProgramObject)) { + throw new InterpreterRuntimeError("The right-hand side of 'instanceof' has no 'prototype' object.", node) + } + return hasPrototype(lhs, prototype) } const collectPatternNames = (pattern: Pattern, out: Array = []): Array => { @@ -239,22 +218,17 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for... } type CustomIterator = { - iterator: ProgramObject | CodeModeGenerator + iterator: ProgramObject next: unknown asynchronous: boolean } -type OpaqueMemberReference = - | ToolReference - | PromiseInstanceMethodReference - | IntrinsicReference - | GeneratorMethodReference - -const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference => - value instanceof ToolReference || - value instanceof PromiseInstanceMethodReference || - value instanceof IntrinsicReference || - value instanceof GeneratorMethodReference +/** A resolved member: the object to read through and the receiver that inherited accessors see. */ +type MemberReference = { + target: ProgramObject + key: PropertyKey + receiver: unknown +} type GeneratorRequest = { kind: GeneratorRequestKind @@ -277,8 +251,6 @@ const promiseResolutionNode: AstNode = { type: "PromiseResolution", start: 0, en /** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */ export class Runtime { readonly runner: Runner - /** Built-in globals by name, unaffected by program shadowing. */ - readonly builtins: ReadonlyMap private readonly root: Frame constructor( @@ -286,6 +258,7 @@ export class Runtime { readonly search: (args: Array) => Effect.Effect, readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray, readonly promises: PromiseRuntime, + readonly prototypes: Prototypes, readonly logs: Array = [], extraGlobals: (host: Host) => ReadonlyArray = () => [], ) { @@ -293,14 +266,14 @@ export class Runtime { // 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 = { - invokeFunction: (fn, args) => this.root.invokeFunction(fn, args), - invokeCallable: (callable, args, node) => this.root.invokeCallable(callable, args, node), + invokeCallable: (callable, thisValue, args, node) => this.root.invokeCallable(callable, thisValue, args, node), settlePromise: (promise) => this.root.settlePromise(promise), syncIterator: (value, node) => this.root.syncIterator(value, node), - intrinsics: createIntrinsics(), + prototypes, + } + for (const [name, value] of [...globals(this), ...extraGlobals(this)]) { + globalScope.set(name, { mutable: false, value }) } - this.builtins = new Map([...globals(this), ...extraGlobals(this)]) - for (const [name, value] of this.builtins) globalScope.set(name, { mutable: false, value }) } run(program: Program): Effect.Effect { @@ -354,16 +327,12 @@ class Frame { private createToolCallPromise( path: ReadonlyArray, args: Array, - ): Effect.Effect { - return this.createPromise(Effect.suspend(() => this.runtime.executeTool(path, args))) - } - - private createPromise(effect: Effect.Effect): Effect.Effect { - return this.runtime.promises.create(effect) + ): Effect.Effect { + return this.runtime.promises.create(Effect.suspend(() => this.runtime.executeTool(path, args))) } // Fiber exits make settlement idempotent; yielding prevents inline continuation. - settlePromise(promise: Values.Promise): Effect.Effect { + settlePromise(promise: ProgramPromise): Effect.Effect { const promises = this.runtime.promises return Effect.suspend(() => { promises.markObserved(promise) @@ -442,7 +411,15 @@ class Frame { node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression, name = node.type === "ArrowFunctionExpression" ? "" : (node.id?.name ?? ""), ): ProgramFunction { - return new ProgramFunction(name, node.params, node.body, this.scopes.capture(), node.async, node.generator) + return new ProgramFunction( + this.runtime.prototypes.Function, + name, + node.params, + node.body, + this.scopes.capture(), + node.async, + node.generator, + ) } // NamedEvaluation: an anonymous function definition takes the name of what it is assigned to. @@ -742,20 +719,21 @@ class Frame { ? value.items[Symbol.iterator]() : typeof value === "string" ? value[Symbol.iterator]() - : value instanceof Values.Map + : value instanceof ProgramMap ? value.map.entries() - : value instanceof Values.Set + : value instanceof ProgramSet ? value.set.values() - : value instanceof Values.URLSearchParams + : value instanceof ProgramURLSearchParams ? value.params.entries() : undefined if (iterator !== undefined) { + const proto = this.runtime.prototypes.Array return Effect.succeed({ next: Effect.sync(() => { const step = iterator.next() return { done: Boolean(step.done), - value: Array.isArray(step.value) ? new ProgramArray(step.value) : step.value, + value: Array.isArray(step.value) ? new ProgramArray(proto, step.value) : step.value, } }), close: Effect.void, @@ -773,29 +751,18 @@ class Frame { } private customIterator(value: unknown, node: AstNode, allowAsync = true) { - if (value instanceof CodeModeGenerator) { - if (value.asynchronous && !allowAsync) return Effect.undefined - return Effect.succeed({ - iterator: value, - next: new GeneratorMethodReference(value, "next"), - asynchronous: value.asynchronous, - }) - } if (!(value instanceof ProgramObject)) 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), [], node), + this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), value, [], node), (iterator) => { - const object = self.requireIterator(iterator, node) + const object = self.requireIteratorObject(iterator, "Iterator method result", node) return { iterator: object, - next: - object instanceof CodeModeGenerator - ? new GeneratorMethodReference(object, "next") - : self.requireIteratorMethod(get(object, "next"), "Iterator next", node), + next: self.requireIteratorMethod(get(object, "next"), "Iterator next", node), asynchronous: asyncMethod !== undefined && asyncMethod !== null, } }, @@ -807,14 +774,14 @@ class Frame { return Effect.gen(function* () { if (iterator.asynchronous) { const object = self.requireIteratorObject( - yield* self.awaitValue(yield* self.invokeCallable(iterator.next, [], node)), + yield* self.awaitValue(yield* self.invokeCallable(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, [], node)) + const called = yield* Effect.exit(self.invokeCallable(iterator.next, iterator.iterator, [], node)) if (!Exit.isSuccess(called)) { if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(called.cause) @@ -839,24 +806,21 @@ class Frame { } private closeIterator(iterator: CustomIterator, node: AstNode, awaiting = true): Effect.Effect { - const close = - iterator.iterator instanceof CodeModeGenerator - ? new GeneratorMethodReference(iterator.iterator, "return") - : get(iterator.iterator, "return") + const close = get(iterator.iterator, "return") if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow const self = this return Effect.gen(function* () { const method = self.requireIteratorMethod(close, "Iterator return", node) if (iterator.asynchronous) { self.requireIteratorObject( - yield* self.awaitValue(yield* self.invokeCallable(method, [], node)), + yield* self.awaitValue(yield* self.invokeCallable(method, iterator.iterator, [], node)), "Iterator return() result", node, ) return } - const called = yield* Effect.exit(self.invokeCallable(method, [], node)) + const called = yield* Effect.exit(self.invokeCallable(method, iterator.iterator, [], node)) if (!Exit.isSuccess(called)) { if (awaiting) yield* Effect.yieldNow return yield* Effect.failCause(called.cause) @@ -877,12 +841,6 @@ class Frame { throw new InterpreterRuntimeError(`${context} must be an object.`, node) } - private requireIterator(value: unknown, node: AstNode): ProgramObject | CodeModeGenerator { - return value instanceof CodeModeGenerator - ? value - : this.requireIteratorObject(value, "Iterator method result", node) - } - private requireIteratorMethod(value: unknown, context: string, node: AstNode): unknown { if (typeofValue(value) === "function") return value throw new InterpreterRuntimeError(`${context} must be a function.`, node) @@ -892,7 +850,7 @@ class Frame { private enumerableKeys(value: unknown, node: AstNode): Array { if (value instanceof ToolReference) return [...this.runtime.toolKeys(value.path)] if (value === null || value === undefined) return [] - return ownKeys(enumerableSource("for...in", value, node)).filter((key): key is string => typeof key === "string") + return keys(enumerableSource(this.runtime.runner, "for...in", value, node)) } private evaluateForInStatement( @@ -1085,7 +1043,7 @@ class Frame { const consumed = new Set() for (const property of pattern.properties) { if (property.type === "RestElement") { - const rest = new ProgramObject() + const rest = new ProgramObject(self.runtime.prototypes.Object) assign(rest, value, consumed) yield* self.declarePattern(property.argument, rest, mutable, property, initialize) continue @@ -1093,13 +1051,7 @@ class Frame { const key = yield* self.destructuringPropertyKey(property) consumed.add(typeof key === "symbol" ? key : String(key)) - yield* self.declarePattern( - property.value, - self.destructuringPropertyValue(value, key), - mutable, - property, - initialize, - ) + yield* self.declarePattern(property.value, get(value, key), mutable, property, initialize) } return } @@ -1145,14 +1097,14 @@ class Frame { const consumed = new Set() for (const property of pattern.properties) { if (property.type === "RestElement") { - const rest = new ProgramObject() + const rest = new ProgramObject(self.runtime.prototypes.Object) assign(rest, value, consumed) yield* self.assignPattern(property.argument, rest, property) continue } const key = yield* self.destructuringPropertyKey(property) consumed.add(typeof key === "symbol" ? key : String(key)) - yield* self.assignPattern(property.value, self.destructuringPropertyValue(value, key), property) + yield* self.assignPattern(property.value, get(value, key), property) } return } @@ -1190,7 +1142,7 @@ class Frame { if (element === null) continue yield* consume( element.type === "RestElement" ? element.argument : element, - element.type === "RestElement" ? new ProgramArray() : undefined, + element.type === "RestElement" ? new ProgramArray(self.runtime.prototypes.Array) : undefined, element, ) if (element.type === "RestElement") return @@ -1207,7 +1159,7 @@ class Frame { done = next.done if (!done) rest.push(next.value) } - yield* consume(element.argument, new ProgramArray(rest), element) + yield* consume(element.argument, new ProgramArray(self.runtime.prototypes.Array, rest), element) return } const consumed = consume(element, step.done ? undefined : step.value, pattern) @@ -1230,19 +1182,13 @@ class Frame { throw unsupportedSyntax(keyNode.type, keyNode) } - private destructuringPropertyValue(source: ProgramObject, key: PropertyKey): unknown { - if (!(source instanceof ProgramArray)) return get(source, key) - if (has(source, key)) return get(source, key) - if (typeof key === "string" && arrayMethods.has(key)) return new IntrinsicReference(source, key) - return undefined - } - private evaluateExpression(node: Expression): Effect.Effect { switch (node.type) { case "Literal": { const regex = node.regex - if (regex) return Effect.sync(() => constructRegExp([regex.pattern, regex.flags], node)) - return Effect.sync(() => toProgram(node.value, "Literal")) + if (regex) + return Effect.sync(() => constructRegExp(this.runtime.prototypes, [regex.pattern, regex.flags], node)) + return Effect.sync(() => toProgram(this.runtime.prototypes, node.value, "Literal")) } case "Identifier": return Effect.sync(() => this.scopes.get(node.name, node)) @@ -1303,7 +1249,7 @@ class Frame { return Effect.gen(function* () { const callee = yield* self.evaluateExpression(node.callee) // Globals are built with this interpreter's R; `instanceof` cannot recover the type argument. - const construct = callee instanceof HostFunction ? (callee as HostFunction).construct : undefined + const construct = callee instanceof NativeFunction ? (callee as NativeFunction).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 @@ -1312,13 +1258,13 @@ class Frame { const message = callee instanceof ProgramFunction ? `${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 HostFunction + : callee instanceof NativeFunction ? `new ${name}(...) is not supported; call ${name}(...) without new instead.` : `${name} is not a constructor.` throw new InterpreterRuntimeError(message, node) } const args = yield* self.evaluateCallArguments(node.arguments) - return yield* construct(args, node) + return yield* construct(args, callee as NativeFunction, node) }) } @@ -1331,7 +1277,11 @@ class Frame { const lhs = yield* self.evaluateExpression(left) const rhs = yield* self.evaluateExpression(node.right) if (operator === "instanceof") return instanceofValue(lhs, rhs, node) - return toProgram(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result") + return toProgram( + self.runtime.prototypes, + self.applyBinaryOperator(operator, lhs, rhs, node), + "Binary expression result", + ) }) } @@ -1347,7 +1297,7 @@ class Frame { // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. // Dates use their default string hint for addition and loose equality, and epoch time elsewhere. const coerceOperand = (operand: unknown): unknown => { - if (operand instanceof Values.Date) { + if (operand instanceof ProgramDate) { return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time } return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand @@ -1429,7 +1379,7 @@ class Frame { throw new InterpreterRuntimeError("Unary operators require data values.", node, "InvalidDataValue") } const operand = - value instanceof Values.Date + value instanceof ProgramDate ? value.time : value !== null && typeof value === "object" ? coerceToString(value) @@ -1448,7 +1398,7 @@ class Frame { default: throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node) } - return toProgram(result, "Unary expression result") + return toProgram(this.runtime.prototypes, result, "Unary expression result") }) } @@ -1470,7 +1420,11 @@ class Frame { if (operator !== "=") { const current = self.scopes.get(name, left) const rightValue = yield* self.evaluateExpression(node.right) - const next = toProgram(self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result") + const next = toProgram( + self.runtime.prototypes, + self.applyCompoundAssignment(operator, current, rightValue, node), + "Assignment result", + ) return self.scopes.set(name, next, left) } const rightValue = yield* self.evaluateNamed(node.right, name) @@ -1481,6 +1435,7 @@ class Frame { Effect.map(self.evaluateExpression(node.right), (rightValue) => { if (operator === "=") return { write: true, next: rightValue, result: rightValue } const next = toProgram( + self.runtime.prototypes, self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result", ) @@ -1564,24 +1519,38 @@ class Frame { throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument) } + // EvaluateCall: a member callee supplies its base object as `this`; anything else calls with undefined. private evaluateCallExpression(node: CallExpression): Effect.Effect { const callee = node.callee const self = this return Effect.gen(function* () { if (callee.type === "Super") throw unsupportedSyntax(callee.type, callee) - const callable = yield* self.evaluateExpression(callee) + const { callable, thisValue } = + callee.type === "MemberExpression" + ? yield* self.readMethod(callee) + : { callable: yield* self.evaluateExpression(callee), thisValue: undefined } if (callable === OptionalShortCircuit) return OptionalShortCircuit if ((callable === null || callable === undefined) && node.optional) return OptionalShortCircuit const args = yield* self.evaluateCallArguments(node.arguments) - return yield* self.invokeCallable(callable, args, node, callee) + return yield* self.invokeCallable(callable, thisValue, args, node, callee) + }) + } + + private readMethod(node: MemberExpression): Effect.Effect<{ callable: unknown; thisValue: unknown }, unknown, R> { + return Effect.map(this.getMemberReference(node), (reference) => { + if (reference === OptionalShortCircuit) return { callable: OptionalShortCircuit, thisValue: undefined } + if (reference instanceof ToolReference) return { callable: reference, thisValue: undefined } + if ("value" in reference) return { callable: reference.value, thisValue: undefined } + return { callable: this.readReference(reference, node), thisValue: reference.receiver } }) } // The single dispatch for every invocation: call expressions and callbacks share it. invokeCallable( callable: unknown, + thisValue: unknown, args: Array, node: AstNode, callee?: Expression, @@ -1594,25 +1563,9 @@ class Frame { } return yield* self.createToolCallPromise(callable.path, args) } - if (callable instanceof ProgramFunction) { - return yield* self.invokeFunction(callable, args) - } - if (callable instanceof GeneratorMethodReference) { - if (callable.kind === "iterator") return callable.generator - const requested = callable.generator.request(callable.kind, args[0], node) as Effect.Effect - return callable.generator.asynchronous ? yield* self.createPromise(requested) : yield* requested - } - if (callable instanceof IntrinsicReference) { - return yield* invokeIntrinsic(self.runtime.runner, callable, args, node) - } - if (callable instanceof PromiseInstanceMethodReference) { - return yield* invokePromiseInstanceMethod(self.runtime.runner, self.runtime.promises, callable, args, node) - } - if (callable instanceof HostFunction) return yield* (callable as HostFunction).call(args, node) - if (callable === undefined || callable === null) { - throw new InterpreterRuntimeError(`${calleeDescription(callee)} is not a function.`, callee ?? node) - } - throw new InterpreterRuntimeError("Only tools are callable here.", callee ?? node) + if (callable instanceof ProgramFunction) return yield* self.invokeFunction(callable, args) + if (callable instanceof NativeFunction) return yield* (callable as NativeFunction).call(thisValue, args, node) + throw new InterpreterRuntimeError(`${calleeDescription(callee)} is not a function.`, callee ?? node) }) } @@ -1642,6 +1595,7 @@ class Frame { } invokeFunction(fn: ProgramFunction, args: Array): Effect.Effect { + const self = this const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()])) const run = Effect.gen(function* () { // Seed all parameters first so defaults cannot fall through to same-named outer bindings. @@ -1655,7 +1609,7 @@ class Frame { if (parameter.type === "RestElement") { yield* invocation.declarePattern( parameter.argument, - new ProgramArray(args.slice(index)), + new ProgramArray(self.runtime.prototypes.Array, args.slice(index)), true, parameter, true, @@ -1685,11 +1639,13 @@ class Frame { invocation: Frame, run: Effect.Effect, asynchronous: boolean, - ): CodeModeGenerator { + ): ProgramGenerator { const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 } invocation.generatorState = state invocation.generatorAsync = asynchronous - const generator = new CodeModeGenerator(asynchronous, (kind, value, node) => { + const protos = this.runtime.prototypes + const result = (value: unknown, done: boolean) => record(protos.Object, { value, done }) + const request = (kind: GeneratorRequestKind, value: unknown, node: AstNode) => { const request = { kind, value, response: Deferred.makeUnsafe() } if (!asynchronous && state.active) { return Effect.fail(new InterpreterRuntimeError("Generator is already running.", node)) @@ -1711,12 +1667,12 @@ class Frame { } if (state.completed) { if (kind === "throw") return Effect.fail(new ProgramThrow(value)) - return Effect.succeed(record({ value: kind === "return" ? value : undefined, done: true })) + 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)) - return Effect.succeed(record({ value, done: true })) + return Effect.succeed(result(value, true)) } state.pending.push(request) @@ -1744,10 +1700,7 @@ class Frame { const active = state.active state.active = undefined if (active) { - Deferred.doneUnsafe( - active.response, - Exit.isSuccess(exit) ? Exit.succeed(record({ value: exit.value, done: true })) : exit, - ) + Deferred.doneUnsafe(active.response, Exit.isSuccess(exit) ? Exit.succeed(result(exit.value, true)) : exit) } yield* invocation.completeGeneratorRequests(state, asynchronous) state.completed = true @@ -1755,12 +1708,18 @@ class Frame { return Effect.andThen(this.runtime.promises.fork(body), Deferred.await(request.response)) } return Deferred.await(request.response) - }) + } + const generator = new ProgramGenerator( + asynchronous ? protos.AsyncGenerator : protos.Generator, + asynchronous, + request, + ) return generator } private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect { const self = this + const result = (value: unknown, done: boolean) => record(self.runtime.prototypes.Object, { value, done }) return Effect.gen(function* () { while (true) { const pending = self.dequeueGeneratorRequest(state) @@ -1773,13 +1732,13 @@ class Frame { const resolved = yield* Effect.exit(self.awaitValue(pending.value)) Deferred.doneUnsafe( pending.response, - Exit.isSuccess(resolved) ? Exit.succeed(record({ value: resolved.value, done: true })) : resolved, + Exit.isSuccess(resolved) ? Exit.succeed(result(resolved.value, true)) : resolved, ) continue } Deferred.doneUnsafe( pending.response, - Exit.succeed(record({ value: pending.kind === "return" ? pending.value : undefined, done: true })), + Exit.succeed(result(pending.kind === "return" ? pending.value : undefined, true)), ) } }) @@ -1824,7 +1783,10 @@ class Frame { private suspendGenerator(value: unknown, node: AstNode): Effect.Effect { const state = this.generatorState if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node) - Deferred.doneUnsafe(state.active.response, Exit.succeed(record({ value, done: false }))) + Deferred.doneUnsafe( + state.active.response, + Exit.succeed(record(this.runtime.prototypes.Object, { value, done: false })), + ) state.active = undefined return Effect.flatMap(this.takeGeneratorRequest(state), (request) => { state.active = request @@ -1842,9 +1804,9 @@ class Frame { if ( value instanceof ProgramArray || typeof value === "string" || - value instanceof Values.Map || - value instanceof Values.Set || - value instanceof Values.URLSearchParams + value instanceof ProgramMap || + value instanceof ProgramSet || + value instanceof ProgramURLSearchParams ) { const cursor = yield* self.syncIterator(value, node) if (!cursor) throw new InterpreterRuntimeError("Built-in iterator is unavailable.", node) @@ -1873,12 +1835,7 @@ class Frame { let kind: GeneratorRequestKind = "next" let input: unknown = undefined while (true) { - const method = - kind === "next" - ? iterator.next - : iterator.iterator instanceof CodeModeGenerator - ? new GeneratorMethodReference(iterator.iterator, kind) - : get(iterator.iterator, kind) + const method = kind === "next" ? iterator.next : get(iterator.iterator, kind) if (method === undefined || method === null) { if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input)) yield* self.closeIterator(iterator, node, self.generatorAsync) @@ -1886,6 +1843,7 @@ class Frame { } const called = yield* self.invokeCallable( self.requireIteratorMethod(method, `Iterator ${kind}`, node), + iterator.iterator, [input], node, ) @@ -1921,14 +1879,14 @@ class Frame { } private evaluateObjectExpression(node: ObjectExpression): Effect.Effect { - const objectValue = new ProgramObject() + const objectValue = new ProgramObject(this.runtime.prototypes.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("Object spread", spread, property)) + assign(objectValue, enumerableSource(self.runtime.runner, "Object spread", spread, property)) continue } @@ -1956,7 +1914,7 @@ class Frame { : key === AsyncIteratorSymbol ? "[Symbol.asyncIterator]" : String(key) - set(objectValue, key, yield* self.evaluateNamed(property.value, name)) + define(objectValue, key, yield* self.evaluateNamed(property.value, name)) } return objectValue @@ -1988,7 +1946,7 @@ class Frame { values.push(yield* self.evaluateExpression(element)) } } - return new ProgramArray(values) + return new ProgramArray(self.runtime.prototypes.Array, values) }) } @@ -2010,7 +1968,7 @@ class Frame { if (index < expressions.length) { const raw = yield* self.evaluateExpression(expressions[index]) - output += coerceToString(toProgram(raw, "Template interpolation")) + output += coerceToString(toProgram(self.runtime.prototypes, raw, "Template interpolation")) } } @@ -2033,19 +1991,7 @@ class Frame { private getMemberReference( node: MemberExpression, - operation: "read" | "write" | "delete" = "read", - ): Effect.Effect< - | MemberReference - | ToolReference - | PromiseInstanceMethodReference - | IntrinsicReference - | GeneratorMethodReference - | ComputedValue - | typeof OptionalShortCircuit - | undefined, - unknown, - R - > { + ): Effect.Effect { const objectNode = node.object const propertyNode = node.property if (objectNode.type === "Super") throw unsupportedSyntax(objectNode.type, objectNode) @@ -2069,127 +2015,51 @@ class Frame { return new ToolReference([...objectValue.path, key]) } - if (objectValue instanceof HostFunction || objectValue instanceof HostNamespace) { - // Unknown static members read as undefined so feature detection works like native JS. - return new ComputedValue(objectValue.member(key, propertyNode)) - } - - // Values have no prototype chain, so `.constructor` resolves to the owning built-in directly. - if ( - operation === "read" && - key === "constructor" && - !(objectValue instanceof ProgramObject && has(objectValue, key)) - ) { - const name = constructorName(objectValue) - if (name !== undefined) return new ComputedValue(self.runtime.builtins.get(name)) - } + if (objectValue instanceof ProgramObject) 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 if (typeof objectValue === "string") { - if (key === "length") return new ComputedValue(objectValue.length) + if (key === "length") return { value: objectValue.length } const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) - if (index !== undefined) return new ComputedValue(objectValue[index]) - if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) + if (index !== undefined) return { value: objectValue[index] } + return { target: protos.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") { - if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) - } - - if (objectValue instanceof Values.Date) { - if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) - } - if (objectValue instanceof Values.RegExp) { - if (key === "lastIndex") return { target: objectValue, key } - if (typeof key === "string" && regexpProperties.has(key)) { - return new ComputedValue((objectValue.regex as unknown as Record)[key]) - } - if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) - } - if (objectValue instanceof Values.Map) { - if (key === "size") return new ComputedValue(objectValue.map.size) - if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) - } - if (objectValue instanceof Values.Set) { - if (key === "size") return new ComputedValue(objectValue.set.size) - if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) - return new ComputedValue(undefined) - } - if (objectValue instanceof Values.URL) { - if (key === "searchParams") { - return new ComputedValue(objectValue.searchParams) - } - if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key) - if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } - return new ComputedValue(undefined) - } - if (objectValue instanceof Values.URLSearchParams) { - if (key === "size") return new ComputedValue(objectValue.params.size) - if (typeof key === "string" && urlSearchParamsMethods.has(key)) { - return new IntrinsicReference(objectValue, key) - } - return new ComputedValue(undefined) - } - - // Reject unknown promise properties so a missing await cannot hide. - if (objectValue instanceof Values.Promise) { - if (key === "then" || key === "catch" || key === "finally") { - return new PromiseInstanceMethodReference(objectValue, key) - } + if (objectValue === null || objectValue === undefined) { throw new InterpreterRuntimeError( - "This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.", + `Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode, - "InvalidDataValue", - ) - } - - if (objectValue instanceof CodeModeGenerator) { - if (key === "next" || key === "return" || key === "throw") { - return new GeneratorMethodReference(objectValue, key) - } - if ( - (key === IteratorSymbol && !objectValue.asynchronous) || - (key === AsyncIteratorSymbol && objectValue.asynchronous) - ) { - return new GeneratorMethodReference(objectValue, "iterator") - } - return new ComputedValue(undefined) - } - - if (objectValue instanceof ProgramObject) return { target: objectValue, key } - - if (isRuntimeReference(objectValue)) { - throw new InterpreterRuntimeError( - `Cannot read properties of ${describeValue(objectValue)}; only data values expose properties.`, - objectNode, - "InvalidDataValue", ) } throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode) }) } + 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)) { + throw new InterpreterRuntimeError( + "This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.", + node.object, + "InvalidDataValue", + ) + } + try { + return get(reference.target, reference.key, reference.receiver) + } catch (error) { + throw locate(error, node) + } + } + private readMember(node: MemberExpression): Effect.Effect { - const self = this return Effect.map(this.getMemberReference(node), (reference) => { if (reference === OptionalShortCircuit) return OptionalShortCircuit - if (reference instanceof ComputedValue) return reference.value - if (reference === undefined || isOpaqueMemberReference(reference)) return reference - const value = self.readReferenceValue(reference, reference.key) - if ( - value === undefined && - reference.target instanceof ProgramArray && - typeof reference.key === "string" && - arrayMethods.has(reference.key) && - !has(reference.target, reference.key) - ) { - return new IntrinsicReference(reference.target, reference.key) - } - return value + if (reference instanceof ToolReference) return reference + if ("value" in reference) return reference.value + return this.readReference(reference, node) }) } @@ -2202,20 +2072,13 @@ class Frame { if (target.type !== "MemberExpression") { throw new InterpreterRuntimeError("Only data fields may be deleted.", argument) } - return Effect.map(this.getMemberReference(target, "delete"), (reference) => { + return Effect.map(this.getMemberReference(target), (reference) => { if (reference === OptionalShortCircuit) return true - if ( - reference instanceof ComputedValue || - reference === undefined || - isOpaqueMemberReference(reference) || - reference.target instanceof Values.URL - ) { + if (reference instanceof ToolReference || "value" in reference || reference.receiver !== reference.target) { throw new InterpreterRuntimeError("Only data fields may be deleted.", target, "InvalidDataValue") } - if (reference.target instanceof Values.RegExp) { - return Reflect.deleteProperty(reference.target.regex, reference.key) - } - return remove(reference.target, reference.key) + if (remove(reference.target, reference.key)) return true + throw new InterpreterRuntimeError(`Cannot delete property '${String(reference.key)}'.`, target) }) } @@ -2226,58 +2089,39 @@ class Frame { ): Effect.Effect { const self = this return Effect.gen(function* () { - const reference = yield* self.getMemberReference(node, "write") - if ( - reference === OptionalShortCircuit || - reference instanceof ComputedValue || - reference === undefined || - isOpaqueMemberReference(reference) - ) { + const reference = yield* self.getMemberReference(node) + if (reference === OptionalShortCircuit || reference instanceof ToolReference || "value" in reference) { throw new InterpreterRuntimeError("Only data fields may be assigned.", node) } + if (reference.receiver !== reference.target) { + throw new InterpreterRuntimeError( + `Cannot create property '${String(reference.key)}' on ${typeof reference.receiver} '${String(reference.receiver)}'.`, + node, + ) + } const key = reference.key - const { write, next, result } = yield* compute(self.readReferenceValue(reference, key)) - if (write) self.assignToReference(reference, key, next, node) + const { write, next, result } = yield* compute(self.readReference(reference, node)) + if (write) self.assignToReference(reference.target, key, next, node) return result }) } - private readReferenceValue(reference: MemberReference, key: PropertyKey): unknown { - if (reference.target instanceof Values.URL) { - return Reflect.get(reference.target.url, key) - } - if (reference.target instanceof Values.RegExp) return reference.target.lastIndex - return get(reference.target, key) - } - - private assignToReference(reference: MemberReference, key: PropertyKey, next: unknown, node: AstNode): void { - if (reference.target instanceof Values.URL) { - const property = key as string - if (!urlWritableProperties.has(property)) { - throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node) - } - try { - const url = reference.target.url as unknown as Record - url[property] = uriArgument(next, `URL.${property} value`) - return - } catch (error) { - if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error - throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node) - } - } - if (reference.target instanceof Values.RegExp) { - reference.target.lastIndex = next - return - } - const target = reference.target + private assignToReference(target: ProgramObject, key: PropertyKey, next: unknown, node: AstNode): void { rejectCircularInsertion( target, next, target instanceof ProgramArray ? "Array assignment result" : "Object assignment result", node, ) - if (set(target, key, next)) return - if (target instanceof ProgramArray) throw rangeError("Invalid array length", node) + const written = (() => { + try { + return set(target, key, next) + } catch (error) { + throw locate(error, node) + } + })() + if (written) return + if (target instanceof ProgramArray && key === "length") throw rangeError("Invalid array length", node) throw new InterpreterRuntimeError(`Cannot assign to read only property '${String(key)}'.`, node) } diff --git a/packages/codemode/src/stdlib/array.ts b/packages/codemode/src/stdlib/array.ts index a731d2eb4a7c..dd41ee7eed34 100644 --- a/packages/codemode/src/stdlib/array.ts +++ b/packages/codemode/src/stdlib/array.ts @@ -1,20 +1,13 @@ import { Effect } from "effect" -import { HostFunction, sync, syncCall } from "../interpreter/host.js" -import { type AstNode, CodeModeGenerator, InterpreterRuntimeError, rangeError } from "../interpreter/model.js" -import { get, ProgramArray, ProgramObject } from "../interpreter/objects.js" -import { describeValue } from "../interpreter/references.js" +import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js" +import { type AstNode, InterpreterRuntimeError, rangeError } from "../interpreter/model.js" +import { get, ProgramArray, ProgramGenerator, ProgramObject } from "../interpreter/objects.js" +import { describeValue, rejectCircularInsertion } from "../interpreter/references.js" import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js" +import { compareText } from "../tool-runtime.js" +import { coerceToNumber, coerceToString } from "./value.js" -const constructArray = (args: Array, node: AstNode): ProgramArray => { - if (args.length !== 1) return new ProgramArray([...args]) - const first = args[0] - if (typeof first !== "number") return new ProgramArray([first]) - if (!Number.isInteger(first) || first < 0 || first > 4294967295) { - throw rangeError("Invalid array length.", node) - } - // Sparse like JS: Array(3) has holes, and combinator loops already skip them. - return new ProgramArray(new Array(first)) -} +const MAX_LENGTH = 4_294_967_295 const arrayLikeSource = ( source: unknown, @@ -23,7 +16,7 @@ const arrayLikeSource = ( if (source instanceof ProgramObject && typeof get(source, "length") === "number") { const length = get(source, "length") as number const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length) - if (normalized > 4_294_967_295) throw new RangeError("Invalid array length") + if (normalized > MAX_LENGTH) throw new RangeError("Invalid array length") return { length: normalized, source } } throw new InterpreterRuntimeError( @@ -35,12 +28,13 @@ const arrayLikeSource = ( const arrayFrom = (runner: Runner, args: Array, node: AstNode): Effect.Effect => { const source = args[0] + const proto = runner.prototypes.Array const apply = args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from", node) return Effect.gen(function* () { const cursor = yield* runner.syncIterator(source, node) if (cursor === undefined) { - if (source instanceof CodeModeGenerator) { + if (source instanceof ProgramGenerator) { throw new InterpreterRuntimeError("Array.from expects a synchronous iterable or array-like value.", node) } const arrayLike = arrayLikeSource(source, node) @@ -49,29 +43,457 @@ const arrayFrom = (runner: Runner, args: Array, node: AstNode): E const item = get(arrayLike.source, index) values.push(apply === undefined ? item : yield* apply([item, index])) } - return new ProgramArray(values) + return new ProgramArray(proto, values) } const values: Array = [] let index = 0 while (true) { const step = yield* cursor.next - if (step.done) return new ProgramArray(values) + if (step.done) return new ProgramArray(proto, values) values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index]))) index += 1 } }) } +export const sortArray = ( + runner: Runner, + target: Array, + comparator: unknown, + name: string, + node: AstNode, +): Effect.Effect, unknown, R> => { + if (comparator === undefined) { + return Effect.sync(() => [...target].sort((a, b) => compareText(coerceToString(a), coerceToString(b)))) + } + const apply = applyCollectionCallback(runner, comparator, name, node) + const mergeSort = (items: Array): Effect.Effect, unknown, R> => { + if (items.length <= 1) return Effect.succeed(items) + const midpoint = Math.floor(items.length / 2) + return Effect.gen(function* () { + const left = yield* mergeSort(items.slice(0, midpoint)) + const right = yield* mergeSort(items.slice(midpoint)) + const merged: Array = [] + let leftIndex = 0 + let rightIndex = 0 + while (leftIndex < left.length && rightIndex < right.length) { + // Treat a NaN comparator result as equal to preserve stable ordering. + const order = coerceToNumber(yield* apply([left[leftIndex], right[rightIndex]])) + if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++]) + else merged.push(right[rightIndex++]) + } + return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)] + }) + } + const defined = target.filter((item) => item !== undefined) + const undefinedCount = target.length - defined.length + return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)]) +} + // Array constructs identically with or without new, like JS. -export const arrayGlobal = (runner: Runner) => - new HostFunction({ +export const arrayGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.Array + const wrap = (items: Array) => new ProgramArray(proto, items) + const construct = (args: Array, into: ProgramObject, node: AstNode): ProgramArray => { + if (args.length !== 1) return new ProgramArray(into, [...args]) + const first = args[0] + if (typeof first !== "number") return new ProgramArray(into, [first]) + if (!Number.isInteger(first) || first < 0 || first > MAX_LENGTH) throw rangeError("Invalid array length.", node) + // Sparse like JS: Array(3) has holes, and combinator loops already skip them. + return new ProgramArray(into, new Array(first)) + } + const array = constructor(protos, proto, { name: "Array", - call: syncCall(constructArray), - construct: syncCall(constructArray), - instanceOf: (value) => value instanceof ProgramArray, - members: { - isArray: sync("Array.isArray", (args) => args[0] instanceof ProgramArray), - of: sync("Array.of", (args) => new ProgramArray([...args])), - from: new HostFunction({ name: "Array.from", call: (args, node) => arrayFrom(runner, args, node) }), - }, + length: 1, + call: (_, args, node) => Effect.sync(() => construct(args, proto, node)), + construct: (args, newTarget, node) => Effect.sync(() => construct(args, prototypeFrom(newTarget, proto), node)), }) + methods(protos, array, [ + ["isArray", 1, (_, args) => args[0] instanceof ProgramArray], + ["of", 0, (_, args) => wrap([...args])], + ["from", 1, (_, args, node) => arrayFrom(runner, args, node)], + ]) + + const self = (thisValue: unknown, name: string, node: AstNode) => + receiver(ProgramArray, thisValue, `Array.prototype.${name}`, node) + const optNumber = (name: string, value: unknown, label: string, node: AstNode): number | undefined => { + if (value === undefined) return undefined + if (typeof value !== "number") { + throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node) + } + return value + } + // Callback methods fix the iteration length while reading existing elements live. + const iterate = ( + name: string, + length: number, + body: ( + target: Array, + receiver: ProgramArray, + apply: (args: Array) => Effect.Effect, + args: Array, + node: AstNode, + ) => Effect.Effect, + ): Method => [ + name, + length, + (thisValue, args, node) => { + const target = self(thisValue, name, node) + return body(target.items, target, applyCollectionCallback(runner, args[0], `Array.${name}`, node), args, node) + }, + ] + + methods(protos, proto, [ + [ + "join", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "join", node).items + if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) { + throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node) + } + return target.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)) + }, + ], + [ + "toString", + 0, + (thisValue, _, node) => + self(thisValue, "toString", node) + .items.map((item) => coerceToString(item ?? "")) + .join(","), + ], + [ + "includes", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "includes", node).items + if (args.length === 0 || args.length > 2) { + throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node) + } + return target.includes(args[0], optNumber("includes", args[1], "start index", node)) + }, + ], + [ + "indexOf", + 1, + (thisValue, args, node) => + self(thisValue, "indexOf", node).items.indexOf(args[0], optNumber("indexOf", args[1], "start index", node)), + ], + [ + "lastIndexOf", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "lastIndexOf", node).items + return args[1] === undefined + ? target.lastIndexOf(args[0]) + : target.lastIndexOf(args[0], optNumber("lastIndexOf", args[1], "start index", node)) + }, + ], + [ + "at", + 1, + (thisValue, args, node) => self(thisValue, "at", node).items.at(optNumber("at", args[0], "index", node) ?? 0), + ], + [ + "slice", + 2, + (thisValue, args, node) => + wrap( + self(thisValue, "slice", node).items.slice( + optNumber("slice", args[0], "start", node), + optNumber("slice", args[1], "end", node), + ), + ), + ], + [ + "concat", + 1, + (thisValue, args, node) => + wrap( + self(thisValue, "concat", node).items.concat( + ...args.map((item) => (item instanceof ProgramArray ? item.items : item)), + ), + ), + ], + [ + "flat", + 0, + (thisValue, args, node) => { + const flatten = (items: Array, depth: number): Array => + items.flatMap((item) => (item instanceof ProgramArray && depth > 0 ? flatten(item.items, depth - 1) : [item])) + return wrap(flatten(self(thisValue, "flat", node).items, optNumber("flat", args[0], "depth", node) ?? 1)) + }, + ], + [ + "reverse", + 0, + (thisValue, _, node) => { + const target = self(thisValue, "reverse", node) + target.items.reverse() + return target + }, + ], + [ + "sort", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "sort", node) + const items = target.items + 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", node), (sorted) => { + sorted.slice(0, itemCount).forEach((item, index) => { + items[index] = item + }) + Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => { + Reflect.deleteProperty(items, index) + }) + return target + }) + }, + ], + [ + "toSorted", + 1, + (thisValue, args, node) => + Effect.map(sortArray(runner, self(thisValue, "toSorted", node).items, args[0], "Array.toSorted", node), wrap), + ], + ["toReversed", 0, (thisValue, _, node) => wrap([...self(thisValue, "toReversed", node).items].reverse())], + [ + "with", + 2, + (thisValue, args, node) => { + const target = self(thisValue, "with", node).items + const index = optNumber("with", args[0], "index", node) ?? 0 + const resolved = index < 0 ? target.length + index : index + if (resolved < 0 || resolved >= target.length) throw rangeError("Array.with index is out of range.", node) + const copied = [...target] + copied[resolved] = args[1] + return wrap(copied) + }, + ], + [ + "push", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "push", node) + // Validate all insertions before mutating to avoid partial cyclic updates. + for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node) + return target.items.push(...args) + }, + ], + [ + "unshift", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "unshift", node) + for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node) + return target.items.unshift(...args) + }, + ], + ["pop", 0, (thisValue, _, node) => self(thisValue, "pop", node).items.pop()], + ["shift", 0, (thisValue, _, node) => self(thisValue, "shift", node).items.shift()], + [ + "splice", + 2, + (thisValue, args, node) => { + const target = self(thisValue, "splice", node) + if (args.length === 0) return wrap(target.items.splice(0, 0)) + const start = optNumber("splice", args[0], "start", node) ?? 0 + if (args.length === 1) return wrap(target.items.splice(start)) + const deleteCount = optNumber("splice", args[1], "delete count", node) ?? 0 + const inserted = args.slice(2) + for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node) + return wrap(target.items.splice(start, deleteCount, ...inserted)) + }, + ], + [ + "toSpliced", + 2, + (thisValue, args, node) => { + const copied = [...self(thisValue, "toSpliced", node).items] + if (args.length === 0) return wrap(copied) + const start = optNumber("toSpliced", args[0], "start", node) ?? 0 + if (args.length === 1) copied.splice(start) + else copied.splice(start, optNumber("toSpliced", args[1], "delete count", node) ?? 0, ...args.slice(2)) + return wrap(copied) + }, + ], + [ + "fill", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "fill", node) + rejectCircularInsertion(target, args[0], "Array.fill result", node) + target.items.fill(args[0], optNumber("fill", args[1], "start", node), optNumber("fill", args[2], "end", node)) + return target + }, + ], + [ + "copyWithin", + 2, + (thisValue, args, node) => { + const target = self(thisValue, "copyWithin", node) + target.items.copyWithin( + optNumber("copyWithin", args[0], "target index", node) ?? 0, + optNumber("copyWithin", args[1], "start", node) ?? 0, + optNumber("copyWithin", args[2], "end", node), + ) + return target + }, + ], + ["keys", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "keys", node).items.keys()))], + ["values", 0, (thisValue, _, node) => wrap([...self(thisValue, "values", node).items])], + [ + "entries", + 0, + (thisValue, _, node) => + wrap(Array.from(self(thisValue, "entries", node).items.entries(), ([index, item]) => wrap([index, item]))), + ], + iterate("map", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + const values: Array = [] + values.length = length + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + values[index] = yield* apply([target[index], index, receiver]) + } + return wrap(values) + }), + ), + iterate("flatMap", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + const values: Array = [] + 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) + else values.push(mapped) + } + return wrap(values) + }), + ), + iterate("filter", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + const values: Array = [] + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + const item = target[index] + if (yield* apply([item, index, receiver])) values.push(item) + } + return wrap(values) + }), + ), + iterate("find", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + for (let index = 0; index < length; index += 1) { + const item = target[index] + if (yield* apply([item, index, receiver])) return item + } + return undefined + }), + ), + iterate("findIndex", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + for (let index = 0; index < length; index += 1) { + if (yield* apply([target[index], index, receiver])) return index + } + return -1 + }), + ), + iterate("findLast", 1, (target, receiver, apply) => + Effect.gen(function* () { + for (let index = target.length - 1; index >= 0; index -= 1) { + const item = target[index] + if (yield* apply([item, index, receiver])) return item + } + return undefined + }), + ), + iterate("findLastIndex", 1, (target, receiver, apply) => + Effect.gen(function* () { + for (let index = target.length - 1; index >= 0; index -= 1) { + if (yield* apply([target[index], index, receiver])) return index + } + return -1 + }), + ), + iterate("some", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + if (yield* apply([target[index], index, receiver])) return true + } + return false + }), + ), + iterate("every", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + for (let index = 0; index < length; index += 1) { + if (!(index in target)) continue + if (!(yield* apply([target[index], index, receiver]))) return false + } + return true + }), + ), + iterate("forEach", 1, (target, receiver, apply) => + Effect.gen(function* () { + const length = target.length + for (let index = 0; index < length; index += 1) { + if (index in target) yield* apply([target[index], index, receiver]) + } + return undefined + }), + ), + iterate("reduce", 1, (target, receiver, apply, args, node) => + Effect.gen(function* () { + const length = target.length + let start = 0 + let accumulator = args[1] + if (args.length < 2) { + while (start < length && !(start in target)) start += 1 + if (start === length) { + throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node) + } + accumulator = target[start] + start += 1 + } + for (let index = start; index < length; index += 1) { + if (!(index in target)) continue + accumulator = yield* apply([accumulator, target[index], index, receiver]) + } + return accumulator + }), + ), + iterate("reduceRight", 1, (target, receiver, apply, args, node) => + Effect.gen(function* () { + let start = target.length - 1 + let accumulator = args[1] + if (args.length < 2) { + while (start >= 0 && !(start in target)) start -= 1 + if (start < 0) { + throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node) + } + accumulator = target[start] + start -= 1 + } + for (let index = start; index >= 0; index -= 1) { + if (!(index in target)) continue + accumulator = yield* apply([accumulator, target[index], index, receiver]) + } + return accumulator + }), + ), + ]) + return array +} diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index 7742a040e01a..7ec3b86711fe 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -1,138 +1,95 @@ import { Effect } from "effect" -import { HostFunction, requiresNew } from "../interpreter/host.js" +import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" -import { getOwn, ProgramArray, ProgramObject, set } from "../interpreter/objects.js" +import { + define, + defineAccessor, + get, + getOwn, + hidden, + isWrapper, + ProgramArray, + ProgramMap, + ProgramObject, + ProgramPromise, + ProgramSet, +} from "../interpreter/objects.js" import { describeValue, isRuntimeReference } from "../interpreter/references.js" -import { applyCollectionCallback, preserveConsumerError, type Runner, toPrimitive } from "../interpreter/runner.js" -import { Values } from "../values.js" -import { coerceToString } from "./value.js" - -export const arrayMethods = new Set([ - "map", - "filter", - "find", - "findIndex", - "findLast", - "findLastIndex", - "some", - "every", - "includes", - "join", - "reduce", - "reduceRight", - "flatMap", - "forEach", - "sort", - "toSorted", - "slice", - "concat", - "indexOf", - "lastIndexOf", - "at", - "flat", - "reverse", - "toReversed", - "with", - "push", - "pop", - "shift", - "unshift", - "splice", - "toSpliced", - "fill", - "copyWithin", - "keys", - "values", - "entries", -]) - -export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"]) - -export const setMethods = new Set([ - "add", - "has", - "delete", - "clear", - "forEach", - "keys", - "values", - "entries", - "union", - "intersection", - "difference", - "symmetricDifference", - "isSubsetOf", - "isSupersetOf", - "isDisjointFrom", -]) +import { + applyCollectionCallback, + isSupportedCallback, + preserveConsumerError, + type Runner, + toPrimitiveNumber, + toPrimitiveString, +} from "../interpreter/runner.js" const coerceGroupByPropertyKey = ( runner: Runner, value: unknown, node: AstNode, ): Effect.Effect => { - if (value instanceof Values.Promise) return Effect.succeed("[object Promise]") - if (!Values.isValue(value) && isRuntimeReference(value)) { + if (value instanceof ProgramPromise) return Effect.succeed("[object Promise]") + if (!isWrapper(value) && isRuntimeReference(value)) { throw new InterpreterRuntimeError( `Object.groupBy callback must return a data value, received ${describeValue(value)}.`, node, "InvalidDataValue", ) } - return Effect.map(toPrimitive(runner, value, "string", node), coerceToString) + return toPrimitiveString(runner, value, node) } /** `Map.groupBy` and `Object.groupBy`: the same iteration, keyed into a Map or a data object. */ export const groupBy = (runner: Runner, namespace: "Map" | "Object") => - new HostFunction({ - name: `${namespace}.groupBy`, - call: (args, node) => { - const source = args[0] - if (source === null || source === undefined) { + fn(runner.prototypes, "groupBy", 2, (_, args, node) => { + const protos = runner.prototypes + const source = args[0] + if (source === null || source === undefined) { + throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node) + } + const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node) + return Effect.gen(function* () { + const cursor = yield* runner.syncIterator(source, node) + if (cursor === undefined) { throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node) } - const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node) - return Effect.gen(function* () { - const cursor = yield* runner.syncIterator(source, node) - if (cursor === undefined) { - throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node) - } - if (namespace === "Map") { - const result = new Values.Map() - let index = 0 - while (true) { - const step = yield* cursor.next - if (step.done) return result - 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([item])) - else (group as ProgramArray).items.push(item) - index += 1 - } - } - - const result = new ProgramObject() + if (namespace === "Map") { + const result = new ProgramMap(protos.Map) let index = 0 while (true) { const step = yield* cursor.next if (step.done) return result const item = step.value - const key = yield* preserveConsumerError( - cursor, - Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)), - ) - const group = getOwn(result, key) - if (group === undefined) set(result, key, new ProgramArray([item])) + 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) index += 1 } - }) - }, + } + + // Object.groupBy returns a null-prototype object, so group names never collide with inherited methods. + const result = new ProgramObject(null) + let index = 0 + while (true) { + const step = yield* cursor.next + if (step.done) return result + const item = step.value + const key = yield* preserveConsumerError( + cursor, + Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)), + ) + const group = getOwn(result, key) + if (group === undefined) define(result, key, new ProgramArray(protos.Array, [item])) + else (group as ProgramArray).items.push(item) + index += 1 + } + }) }) -const constructMap = (runner: Runner, init: unknown, node: AstNode) => { - const target = new Values.Map() +const constructMap = (runner: Runner, init: unknown, proto: ProgramObject, node: AstNode) => { + const target = new ProgramMap(proto) if (init === undefined || init === null) return Effect.succeed(target) return Effect.gen(function* () { const cursor = yield* runner.syncIterator(init, node) @@ -155,8 +112,8 @@ const constructMap = (runner: Runner, init: unknown, node: AstNode) => { }) } -const constructSet = (runner: Runner, init: unknown, node: AstNode) => { - const target = new Values.Set() +const constructSet = (runner: Runner, init: unknown, proto: ProgramObject, node: AstNode) => { + const target = new ProgramSet(proto) if (init === undefined || init === null) return Effect.succeed(target) return Effect.gen(function* () { const cursor = yield* runner.syncIterator(init, node) @@ -171,19 +128,256 @@ const constructSet = (runner: Runner, init: unknown, node: AstNode) => { }) } -export const mapGlobal = (runner: Runner) => - new HostFunction({ +export const mapGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.Map + const map = constructor(protos, proto, { name: "Map", call: requiresNew("Map"), - construct: (args, node) => constructMap(runner, args[0], node), - instanceOf: (value) => value instanceof Values.Map, - members: { groupBy: groupBy(runner, "Map") }, + construct: (args, newTarget, node) => constructMap(runner, args[0], prototypeFrom(newTarget, proto), node), + }) + define(map, "groupBy", groupBy(runner, "Map"), hidden) + const self = (thisValue: unknown, name: string, node: AstNode) => + receiver(ProgramMap, thisValue, `Map.prototype.${name}`, node) + const wrap = (items: Array) => new ProgramArray(protos.Array, items) + defineAccessor(proto, "size", (thisValue) => receiver(ProgramMap, thisValue, "Map.prototype.size").map.size) + methods(protos, proto, [ + ["get", 1, (thisValue, args, node) => self(thisValue, "get", node).map.get(args[0])], + ["has", 1, (thisValue, args, node) => self(thisValue, "has", node).map.has(args[0])], + [ + "set", + 2, + (thisValue, args, node) => { + const target = self(thisValue, "set", node) + target.map.set(args[0], args[1]) + return target + }, + ], + ["delete", 1, (thisValue, args, node) => self(thisValue, "delete", node).map.delete(args[0])], + [ + "clear", + 0, + (thisValue, _, node) => { + self(thisValue, "clear", node).map.clear() + return undefined + }, + ], + ["keys", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "keys", node).map.keys()))], + ["values", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "values", node).map.values()))], + [ + "entries", + 0, + (thisValue, _, node) => + wrap(Array.from(self(thisValue, "entries", node).map.entries(), ([key, item]) => wrap([key, item]))), + ], + [ + "forEach", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "forEach", node) + const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node) + return Effect.gen(function* () { + for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target]) + return undefined + }) + }, + ], + ]) + return map +} + +type SetRecord = { + readonly size: number + readonly has: (item: unknown) => Effect.Effect + readonly keys: () => Effect.Effect, unknown, R> +} + +const loadSetRecord = ( + runner: Runner, + source: unknown, + name: string, + node: AstNode, +): Effect.Effect, unknown, R> => { + if (source instanceof ProgramSet) { + 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) { + 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)) { + throw new InterpreterRuntimeError(`Set.${name} expects a Set-like object.`, node) + } + return Effect.gen(function* () { + const size = yield* toPrimitiveNumber(runner, get(source, "size"), node) + if (Number.isNaN(size)) { + throw new InterpreterRuntimeError(`Set.${name} received a Set-like object with an invalid size.`, node) + } + const has = get(source, "has") + const keys = get(source, "keys") + if (!isSupportedCallback(has) || !isSupportedCallback(keys)) { + throw new InterpreterRuntimeError(`Set.${name} expects callable 'has' and 'keys' methods.`, node) + } + return { + size: Math.max(Math.trunc(size), 0), + has: (item: unknown) => Effect.map(runner.invokeCallable(has, source, [item], node), Boolean), + keys: () => + Effect.flatMap(runner.invokeCallable(keys, source, [], node), (result) => { + if (result instanceof ProgramArray) return Effect.succeed(result.items) + throw new InterpreterRuntimeError(`Set.${name} expected 'keys' to return an iterator.`, node) + }), + } }) +} -export const setGlobal = (runner: Runner) => - new HostFunction({ +const setOperation = ( + runner: Runner, + target: ProgramSet, + name: string, + source: unknown, + node: AstNode, +): Effect.Effect => + Effect.gen(function* () { + const other = yield* loadSetRecord(runner, source, name, node) + const copy = () => { + const result = new ProgramSet(runner.prototypes.Set) + for (const item of target.set.values()) result.set.add(item) + return result + } + if (name === "union") { + const result = copy() + for (const item of yield* other.keys()) result.set.add(item) + return result + } + if (name === "intersection") { + const result = new ProgramSet(runner.prototypes.Set) + if (target.set.size <= other.size) { + for (const item of target.set.values()) { + if (yield* other.has(item)) result.set.add(item) + } + return result + } + for (const item of yield* other.keys()) { + if (target.set.has(item)) result.set.add(item) + } + return result + } + if (name === "difference") { + const result = copy() + if (target.set.size <= other.size) { + for (const item of result.set.values()) { + if (yield* other.has(item)) result.set.delete(item) + } + return result + } + for (const item of yield* other.keys()) result.set.delete(item) + return result + } + if (name === "symmetricDifference") { + const result = copy() + for (const item of yield* other.keys()) { + if (target.set.has(item)) result.set.delete(item) + else result.set.add(item) + } + return result + } + if (name === "isSubsetOf") { + if (target.set.size > other.size) return false + for (const item of target.set.values()) { + if (!(yield* other.has(item))) return false + } + return true + } + if (name === "isSupersetOf") { + if (target.set.size < other.size) return false + for (const item of yield* other.keys()) { + if (!target.set.has(item)) return false + } + return true + } + if (target.set.size <= other.size) { + for (const item of target.set.values()) { + if (yield* other.has(item)) return false + } + return true + } + for (const item of yield* other.keys()) { + if (target.set.has(item)) return false + } + return true + }) + +export const setGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.Set + const set = constructor(protos, proto, { name: "Set", call: requiresNew("Set"), - construct: (args, node) => constructSet(runner, args[0], node), - instanceOf: (value) => value instanceof Values.Set, + construct: (args, newTarget, node) => constructSet(runner, args[0], prototypeFrom(newTarget, proto), node), }) + const self = (thisValue: unknown, name: string, node: AstNode) => + receiver(ProgramSet, thisValue, `Set.prototype.${name}`, node) + const wrap = (items: Array) => new ProgramArray(protos.Array, items) + const operation = (name: string): Method => [ + name, + 1, + (thisValue, args, node) => setOperation(runner, self(thisValue, name, node), name, args[0], node), + ] + defineAccessor(proto, "size", (thisValue) => receiver(ProgramSet, thisValue, "Set.prototype.size").set.size) + methods(protos, proto, [ + ["has", 1, (thisValue, args, node) => self(thisValue, "has", node).set.has(args[0])], + [ + "add", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "add", node) + target.set.add(args[0]) + return target + }, + ], + ["delete", 1, (thisValue, args, node) => self(thisValue, "delete", node).set.delete(args[0])], + [ + "clear", + 0, + (thisValue, _, node) => { + self(thisValue, "clear", node).set.clear() + return undefined + }, + ], + ["keys", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "keys", node).set.values()))], + ["values", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "values", node).set.values()))], + [ + "entries", + 0, + (thisValue, _, node) => + wrap(Array.from(self(thisValue, "entries", node).set.values(), (item) => wrap([item, item]))), + ], + [ + "forEach", + 1, + (thisValue, args, node) => { + const target = self(thisValue, "forEach", node) + const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node) + return Effect.gen(function* () { + for (const item of Array.from(target.set.values())) yield* apply([item, item, target]) + return undefined + }) + }, + ], + operation("union"), + operation("intersection"), + operation("difference"), + operation("symmetricDifference"), + operation("isSubsetOf"), + operation("isSupersetOf"), + operation("isDisjointFrom"), + ]) + return set +} diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts index 14367a8890da..cbfc04cba69c 100644 --- a/packages/codemode/src/stdlib/console.ts +++ b/packages/codemode/src/stdlib/console.ts @@ -1,32 +1,51 @@ import { toData, toProgram } from "../data.js" -import { HostNamespace, sync } from "../interpreter/host.js" -import { get, ownEntries, ProgramArray, ProgramObject } from "../interpreter/objects.js" +import type { Prototypes } from "../interpreter/intrinsics.js" +import { type Method, methods } from "../interpreter/native.js" +import { + entries, + get, + ProgramArray, + ProgramDate, + ProgramMap, + ProgramObject, + ProgramPromise, + ProgramRegExp, + ProgramSet, + ProgramURL, + ProgramURLSearchParams, +} from "../interpreter/objects.js" import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js" -import { Values } from "../values.js" +import type { Runner } from "../interpreter/runner.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 = (logs: Array) => - new HostNamespace( - "console", - Object.fromEntries( - consoleMethods.map((name) => [ +export const consoleGlobal = (runner: Runner, logs: Array) => { + const protos = runner.prototypes + const console = new ProgramObject(protos.Object) + methods( + protos, + console, + consoleMethods.map( + (name): Method => [ name, - sync(`console.${name}`, (args) => { - logs.push(formatConsoleMessage(name, args)) + 0, + (_, args) => { + logs.push(formatConsoleMessage(protos, name, args)) return undefined - }), - ]), + }, + ], ), ) + return console +} const MAX_CONSOLE_DEPTH = 32 -const formatConsoleMessage = (name: string, args: Array): string => { +const formatConsoleMessage = (protos: Prototypes, name: string, args: Array): string => { if (name === "dir") return args.length === 0 ? "undefined" : formatConsoleArgument(args[0]) - if (name === "table") return formatConsoleTable(args[0], args[1]) + if (name === "table") return formatConsoleTable(protos, args[0], args[1]) const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : "" return `${prefix}${args.map((arg) => formatConsoleArgument(arg)).join(" ")}` } @@ -42,26 +61,26 @@ const formatConsoleValue = (value: unknown, seen: Set, depth: number): s if (typeof value === "string") return JSON.stringify(value) if (typeof value === "number" || typeof value === "boolean") return String(value) if (typeof value !== "object") return String(value) - if (value instanceof Values.Promise) return "[Promise (await it to get its value)]" - if (value instanceof Values.Date) return coerceToString(value) - if (value instanceof Values.RegExp) return coerceToString(value) - if (value instanceof Values.URL) return coerceToString(value) - if (value instanceof Values.URLSearchParams) return coerceToString(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 (depth > MAX_CONSOLE_DEPTH) return "..." if (seen.has(value)) return "[Circular]" - if (value instanceof Values.Map) { + if (value instanceof ProgramMap) { seen.add(value) try { - const entries = Array.from(value.map.entries(), ([key, item]) => new ProgramArray([key, item])) - return `Map(${value.map.size}) ${formatConsoleValue(new ProgramArray(entries), seen, depth + 1)}` + const items = Array.from(value.map.entries(), ([key, item]) => `[${formatItems([key, item], seen, depth + 1)}]`) + return `Map(${value.map.size}) [${items.join(",")}]` } finally { seen.delete(value) } } - if (value instanceof Values.Set) { + if (value instanceof ProgramSet) { seen.add(value) try { - return `Set(${value.set.size}) ${formatConsoleValue(new ProgramArray([...value.set.values()]), seen, depth + 1)}` + return `Set(${value.set.size}) [${formatItems([...value.set.values()], seen, depth + 1)}]` } finally { seen.delete(value) } @@ -69,11 +88,9 @@ const formatConsoleValue = (value: unknown, seen: Set, depth: number): s if (isRuntimeReference(value)) return "[opaque reference]" seen.add(value) try { - if (value instanceof ProgramArray) { - return `[${value.items.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]` - } + if (value instanceof ProgramArray) return `[${formatItems(value.items, seen, depth + 1)}]` if (!(value instanceof ProgramObject)) return "[object Object]" - return `{${ownEntries(value) + return `{${entries(value) .map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`) .join(",")}}` } finally { @@ -81,10 +98,13 @@ const formatConsoleValue = (value: unknown, seen: Set, depth: number): s } } -const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => { +const formatItems = (items: Array, seen: Set, depth: number): string => + items.map((item) => formatConsoleValue(item, seen, depth)).join(",") + +const formatConsoleTable = (protos: Prototypes, value: unknown, columnsArgument: unknown): string => { if (value === undefined) return "undefined" if (containsOpaqueReference(value)) return "[opaque reference]" - const data = toProgram(value, "console.table argument") + const data = toProgram(protos, 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)))) @@ -110,7 +130,7 @@ const consoleTableRows = ( return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) })) } if (data instanceof ProgramObject) { - return ownEntries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) })) + return entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) })) } return [{ index: "0", values: { Value: data } }] } @@ -118,7 +138,7 @@ const consoleTableRows = ( const consoleTableValues = (value: unknown, columns: ReadonlyArray | undefined): Record => { if (value instanceof ProgramObject && !(value instanceof ProgramArray)) { if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, get(value, column)])) - return Object.fromEntries(ownEntries(value)) + return Object.fromEntries(entries(value)) } return { Value: value } } diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index f09ac58c37aa..5fb06b89b4be 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -1,38 +1,30 @@ import { Effect } from "effect" -import { HostFunction, sync } from "../interpreter/host.js" -import { type AstNode, InterpreterRuntimeError, rangeError } from "../interpreter/model.js" -import { type Runner, toPrimitive } from "../interpreter/runner.js" -import { Values } from "../values.js" +import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js" +import { type AstNode, rangeError } from "../interpreter/model.js" +import { ProgramDate, ProgramObject } from "../interpreter/objects.js" +import { type Runner, toPrimitive, toPrimitiveNumber } from "../interpreter/runner.js" import { coerceToNumber, coerceToString } from "./value.js" -const dateSetterArguments = new Map([ - ["setTime", 1], - ["setMilliseconds", 1], - ["setUTCMilliseconds", 1], - ["setSeconds", 2], - ["setUTCSeconds", 2], - ["setMinutes", 3], - ["setUTCMinutes", 3], - ["setHours", 4], - ["setUTCHours", 4], - ["setDate", 1], - ["setUTCDate", 1], - ["setMonth", 2], - ["setUTCMonth", 2], - ["setFullYear", 3], - ["setUTCFullYear", 3], -]) +const constructDate = (runner: Runner, args: Array, proto: ProgramObject, node: AstNode) => { + if (args.length === 0) return Effect.succeed(new ProgramDate(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", node), (value) => + typeof value === "string" + ? new ProgramDate(proto, Date.parse(value)) + : new ProgramDate(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())) +} -export const dateMethods = new Set([ - "getTime", - "valueOf", - "toISOString", - "toJSON", - "toString", - "toDateString", - "toTimeString", - "toUTCString", - "toGMTString", +type Getter = keyof { + [K in keyof Date as Date[K] extends () => number ? K : never]: never +} + +const getters: ReadonlyArray = [ "getFullYear", "getMonth", "getDate", @@ -50,156 +42,92 @@ export const dateMethods = new Set([ "getUTCSeconds", "getUTCMilliseconds", "getTimezoneOffset", - ...dateSetterArguments.keys(), -]) +] -const constructDate = (runner: Runner, args: Array, node: AstNode) => { - if (args.length === 0) return Effect.succeed(new Values.Date(Date.now())) - if (args.length === 1) { - const arg = args[0] - if (arg instanceof Values.Date) return Effect.succeed(new Values.Date(arg.time)) - return Effect.map(toPrimitive(runner, arg, "number", node), (value) => - typeof value === "string" - ? new Values.Date(Date.parse(value)) - : new Values.Date(new Date(coerceToNumber(value)).getTime()), - ) - } - const parts = args.map((arg) => coerceToNumber(arg)) - return Effect.succeed(new Values.Date(new Date(...(parts as [number, number])).getTime())) +type Setter = keyof { + [K in keyof Date as Date[K] extends (value: number, ...rest: Array) => number ? K : never]: never } -export const dateGlobal = (runner: Runner) => - new HostFunction({ +const setters: ReadonlyArray = [ + ["setTime", 1], + ["setMilliseconds", 1], + ["setUTCMilliseconds", 1], + ["setSeconds", 2], + ["setUTCSeconds", 2], + ["setMinutes", 3], + ["setUTCMinutes", 3], + ["setHours", 4], + ["setUTCHours", 4], + ["setDate", 1], + ["setUTCDate", 1], + ["setMonth", 2], + ["setUTCMonth", 2], + ["setFullYear", 3], + ["setUTCFullYear", 3], +] + +export const dateGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.Date + const date = constructor(protos, 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, node) => constructDate(runner, args, node), - instanceOf: (value) => value instanceof Values.Date, - members: { - now: sync("Date.now", () => Date.now()), - parse: sync("Date.parse", (args) => Date.parse(coerceToString(args[0]))), - UTC: sync("Date.UTC", (args) => - Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters)), - ), - }, + construct: (args, newTarget, node) => constructDate(runner, args, prototypeFrom(newTarget, proto), node), }) + methods(protos, 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))], + ]) -export const dateSetterArgumentCount = (name: string): number | undefined => dateSetterArguments.get(name) - -export const invokeDateMethod = ( - value: Values.Date, - name: string, - args: Array, - node: AstNode, - initialTime = value.time, -): unknown => { - const hosted = new Date(initialTime) - switch (name) { - case "getTime": - case "valueOf": - return value.time - case "toISOString": - if (!Number.isFinite(value.time)) throw rangeError("Invalid time value.", node) - return hosted.toISOString() - case "toJSON": - return Number.isFinite(value.time) ? hosted.toISOString() : null - case "toString": - return coerceToString(value) - case "toDateString": - return hosted.toDateString() - case "toTimeString": - return hosted.toTimeString() - case "toUTCString": - case "toGMTString": - return hosted.toUTCString() - case "getFullYear": - return hosted.getFullYear() - case "getMonth": - return hosted.getMonth() - case "getDate": - return hosted.getDate() - case "getDay": - return hosted.getDay() - case "getHours": - return hosted.getHours() - case "getMinutes": - return hosted.getMinutes() - case "getSeconds": - return hosted.getSeconds() - case "getMilliseconds": - return hosted.getMilliseconds() - case "getUTCFullYear": - return hosted.getUTCFullYear() - case "getUTCMonth": - return hosted.getUTCMonth() - case "getUTCDate": - return hosted.getUTCDate() - case "getUTCDay": - return hosted.getUTCDay() - case "getUTCHours": - return hosted.getUTCHours() - case "getUTCMinutes": - return hosted.getUTCMinutes() - case "getUTCSeconds": - return hosted.getUTCSeconds() - case "getUTCMilliseconds": - return hosted.getUTCMilliseconds() - case "getTimezoneOffset": - return hosted.getTimezoneOffset() - case "setTime": - return updateDate(value, hosted.setTime(args[0])) - case "setMilliseconds": - return updateDate(value, hosted.setMilliseconds(args[0])) - case "setUTCMilliseconds": - return updateDate(value, hosted.setUTCMilliseconds(args[0])) - case "setSeconds": - if (args.length < 2) return updateDate(value, hosted.setSeconds(args[0])) - return updateDate(value, hosted.setSeconds(args[0], args[1])) - case "setUTCSeconds": - if (args.length < 2) return updateDate(value, hosted.setUTCSeconds(args[0])) - return updateDate(value, hosted.setUTCSeconds(args[0], args[1])) - case "setMinutes": - if (args.length < 2) return updateDate(value, hosted.setMinutes(args[0])) - if (args.length < 3) return updateDate(value, hosted.setMinutes(args[0], args[1])) - return updateDate(value, hosted.setMinutes(args[0], args[1], args[2])) - case "setUTCMinutes": - if (args.length < 2) return updateDate(value, hosted.setUTCMinutes(args[0])) - if (args.length < 3) return updateDate(value, hosted.setUTCMinutes(args[0], args[1])) - return updateDate(value, hosted.setUTCMinutes(args[0], args[1], args[2])) - case "setHours": - if (args.length < 2) return updateDate(value, hosted.setHours(args[0])) - if (args.length < 3) return updateDate(value, hosted.setHours(args[0], args[1])) - if (args.length < 4) return updateDate(value, hosted.setHours(args[0], args[1], args[2])) - return updateDate(value, hosted.setHours(args[0], args[1], args[2], args[3])) - case "setUTCHours": - if (args.length < 2) return updateDate(value, hosted.setUTCHours(args[0])) - if (args.length < 3) return updateDate(value, hosted.setUTCHours(args[0], args[1])) - if (args.length < 4) return updateDate(value, hosted.setUTCHours(args[0], args[1], args[2])) - return updateDate(value, hosted.setUTCHours(args[0], args[1], args[2], args[3])) - case "setDate": - return updateDate(value, hosted.setDate(args[0])) - case "setUTCDate": - return updateDate(value, hosted.setUTCDate(args[0])) - case "setMonth": - if (args.length < 2) return updateDate(value, hosted.setMonth(args[0])) - return updateDate(value, hosted.setMonth(args[0], args[1])) - case "setUTCMonth": - if (args.length < 2) return updateDate(value, hosted.setUTCMonth(args[0])) - return updateDate(value, hosted.setUTCMonth(args[0], args[1])) - case "setFullYear": - if (args.length < 2) return updateDate(value, hosted.setFullYear(args[0])) - if (args.length < 3) return updateDate(value, hosted.setFullYear(args[0], args[1])) - return updateDate(value, hosted.setFullYear(args[0], args[1], args[2])) - case "setUTCFullYear": - if (args.length < 2) return updateDate(value, hosted.setUTCFullYear(args[0])) - if (args.length < 3) return updateDate(value, hosted.setUTCFullYear(args[0], args[1])) - return updateDate(value, hosted.setUTCFullYear(args[0], args[1], args[2])) - default: - throw new InterpreterRuntimeError(`Date method '${name}' is not available.`, node) + const self = (thisValue: unknown, name: string, node: AstNode) => + receiver(ProgramDate, thisValue, `Date.prototype.${name}`, node) + const iso = (value: ProgramDate, node: AstNode) => { + if (!Number.isFinite(value.time)) throw rangeError("Invalid time value.", node) + return new Date(value.time).toISOString() } -} - -const updateDate = (value: Values.Date, time: number): number => { - value.time = time - return time + methods(protos, proto, [ + ["getTime", 0, (thisValue, _, node) => self(thisValue, "getTime", node).time], + ["valueOf", 0, (thisValue, _, node) => self(thisValue, "valueOf", node).time], + ["toISOString", 0, (thisValue, _, node) => iso(self(thisValue, "toISOString", node), node)], + [ + "toJSON", + 1, + (thisValue, _, node) => { + const value = self(thisValue, "toJSON", node) + return Number.isFinite(value.time) ? iso(value, node) : null + }, + ], + ["toString", 0, (thisValue, _, node) => coerceToString(self(thisValue, "toString", node))], + ["toDateString", 0, (thisValue, _, node) => new Date(self(thisValue, "toDateString", node).time).toDateString()], + ["toTimeString", 0, (thisValue, _, node) => new Date(self(thisValue, "toTimeString", node).time).toTimeString()], + ["toUTCString", 0, (thisValue, _, node) => new Date(self(thisValue, "toUTCString", node).time).toUTCString()], + ["toGMTString", 0, (thisValue, _, node) => new Date(self(thisValue, "toGMTString", node).time).toUTCString()], + ...getters.map( + (name): Method => [name, 0, (thisValue, _, node) => new Date(self(thisValue, name, node).time)[name]()], + ), + ...setters.map( + ([name, length]): Method => [ + name, + length, + (thisValue, args, node) => { + const target = self(thisValue, name, node) + // 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, node), { + concurrency: 1, + }), + (values) => { + target.time = hosted[name](...(values as [number, number, number, number])) + return target.time + }, + ) + }, + ], + ), + ]) + return date } diff --git a/packages/codemode/src/stdlib/json.ts b/packages/codemode/src/stdlib/json.ts index 75438324eb10..7145c6c05d41 100644 --- a/packages/codemode/src/stdlib/json.ts +++ b/packages/codemode/src/stdlib/json.ts @@ -1,17 +1,19 @@ import { Effect } from "effect" -import { HostFunction, HostNamespace } from "../interpreter/host.js" +import { methods } from "../interpreter/native.js" import { applyCollectionCallback, type Runner } from "../interpreter/runner.js" import { type AstNode, InterpreterRuntimeError, syntaxError } from "../interpreter/model.js" import { typeofValue } from "../interpreter/references.js" import { fromData, toData, toProgram } from "../data.js" -import { get, ownKeys, ProgramArray, ProgramObject, record, remove, set } from "../interpreter/objects.js" -import { Values } from "../values.js" +import { Callable, get, keys, ProgramArray, ProgramObject, record, remove, set } from "../interpreter/objects.js" -export const jsonGlobal = (runner: Runner) => - new HostNamespace("JSON", { - parse: new HostFunction({ name: "JSON.parse", call: (args, node) => parse(runner, args, node) }), - stringify: new HostFunction({ name: "JSON.stringify", call: (args, node) => stringify(runner, args, node) }), - }) +export const jsonGlobal = (runner: Runner) => { + const json = new ProgramObject(runner.prototypes.Object) + methods(runner.prototypes, json, [ + ["parse", 2, (_, args, node) => parse(runner, args, node)], + ["stringify", 3, (_, args, node) => stringify(runner, args, node)], + ]) + return json +} const parse = (runner: Runner, args: Array, node: AstNode): Effect.Effect => { const text = args[0] @@ -19,7 +21,7 @@ const parse = (runner: Runner, args: Array, node: AstNode): Effec const parsed = (() => { try { - return fromData(JSON.parse(text), "JSON.parse result") + return fromData(runner.prototypes, JSON.parse(text), "JSON.parse result") } catch (error) { throw syntaxError( `JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`, @@ -34,15 +36,15 @@ const parse = (runner: Runner, args: Array, node: AstNode): Effec Effect.gen(function* () { const value = get(holder, key) if (value instanceof ProgramObject) { - for (const name of ownKeys(value)) { - const revived = yield* visit(value, name as string) + for (const name of keys(value)) { + const revived = yield* visit(value, name) if (revived === undefined) remove(value, name) else set(value, name, revived) } } return yield* apply([key, value]) }) - return visit(record({ "": parsed }), "") + return visit(record(runner.prototypes.Object, { "": parsed }), "") } const stringify = (runner: Runner, args: Array, node: AstNode): Effect.Effect => { @@ -61,14 +63,14 @@ const stringify = (runner: Runner, args: Array, node: AstNode): E } // Validate up front; the replacer walk below reads the original value. - toProgram(args[0], "JSON.stringify value") + toProgram(runner.prototypes, args[0], "JSON.stringify value") const apply = applyCollectionCallback(runner, replacer, "JSON.stringify", node) const stack = new Set() const visit = (holder: ProgramObject, key: string): Effect.Effect => Effect.gen(function* () { - const value = yield* apply([key, toJSONValue(get(holder, key))]) + const value = yield* apply([key, yield* toJSONValue(runner, get(holder, key), key, node)]) if (value === undefined || typeofValue(value) === "function") return undefined - toProgram(value, "JSON.stringify replacer result") + toProgram(runner.prototypes, 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 {} @@ -83,8 +85,7 @@ const stringify = (runner: Runner, args: Array, node: AstNode): E return result } const result: Record = Object.create(null) - for (const name of ownKeys(value)) { - if (typeof name !== "string") continue + for (const name of keys(value)) { const item = yield* visit(value, name) if (item !== undefined) result[name] = item } @@ -92,13 +93,14 @@ const stringify = (runner: Runner, args: Array, node: AstNode): E return result }) - return Effect.map(visit(record({ "": args[0] }), ""), (value) => JSON.stringify(value, null, indent)) + return Effect.map(visit(record(runner.prototypes.Object, { "": args[0] }), ""), (value) => + JSON.stringify(value, null, indent), + ) } -const toJSONValue = (value: unknown): unknown => { - if (value instanceof Values.Date) { - return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null - } - if (value instanceof Values.URL) return value.url.href - return value +// SerializeJSONProperty step 2: a callable `toJSON` decides the value, as Date and URL define. +const toJSONValue = (runner: Runner, value: unknown, key: string, node: AstNode) => { + if (!(value instanceof ProgramObject)) return Effect.succeed(value) + const toJSON = get(value, "toJSON") + return toJSON instanceof Callable ? runner.invokeCallable(toJSON, value, [key], node) : Effect.succeed(value) } diff --git a/packages/codemode/src/stdlib/math.ts b/packages/codemode/src/stdlib/math.ts index 10a7378d843d..8978b75b81fc 100644 --- a/packages/codemode/src/stdlib/math.ts +++ b/packages/codemode/src/stdlib/math.ts @@ -1,7 +1,8 @@ import { Effect } from "effect" -import { HostFunction, HostNamespace, sync } from "../interpreter/host.js" -import { preserveConsumerError, type Runner } from "../interpreter/runner.js" +import { constants, type Method, methods } from "../interpreter/native.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { ProgramObject } from "../interpreter/objects.js" +import { preserveConsumerError, type Runner } from "../interpreter/runner.js" // Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types. declare global { @@ -19,50 +20,34 @@ const number = (name: string, args: Array, index: number, node: AstNode return arg } -const unary = (name: string, op: (a: number) => number) => - sync(`Math.${name}`, (args, node) => op(number(name, args, 0, node))) +const unary = (name: string, op: (a: number) => number): Method => [ + name, + 1, + (_, args, node) => op(number(name, args, 0, node)), +] -const binary = (name: string, op: (a: number, b: number) => number) => - sync(`Math.${name}`, (args, node) => op(number(name, args, 0, node), number(name, args, 1, node))) +const binary = (name: string, op: (a: number, b: number) => number): Method => [ + name, + 2, + (_, args, node) => op(number(name, args, 0, node), number(name, args, 1, node)), +] -const variadic = (name: string, op: (...values: Array) => number) => - sync(`Math.${name}`, (args, node) => +const variadic = (name: string, op: (...values: Array) => number): Method => [ + name, + 2, + (_, args, node) => op( ...args.map((arg) => { if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node) return arg }), ), - ) - -const sumPrecise = (runner: Runner) => - new HostFunction({ - name: "Math.sumPrecise", - call: (args, node) => - Effect.gen(function* () { - const cursor = yield* runner.syncIterator(args[0], node) - if (cursor === undefined) { - throw new InterpreterRuntimeError("Math.sumPrecise expects a synchronous iterable.", node) - } - const numbers: Array = [] - while (true) { - const step = yield* cursor.next - if (step.done) return Math.sumPrecise(numbers) - yield* preserveConsumerError( - cursor, - Effect.sync(() => { - if (typeof step.value !== "number") { - throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node) - } - numbers.push(step.value) - }), - ) - } - }), - }) +] -export const mathGlobal = (runner: Runner) => - new HostNamespace("Math", { +export const mathGlobal = (runner: Runner) => { + const protos = runner.prototypes + const math = new ProgramObject(protos.Object) + constants(math, { PI: Math.PI, E: Math.E, LN2: Math.LN2, @@ -71,41 +56,69 @@ export const mathGlobal = (runner: Runner) => LOG10E: Math.LOG10E, SQRT2: Math.SQRT2, SQRT1_2: Math.SQRT1_2, - random: sync("Math.random", () => Math.random()), - max: variadic("max", Math.max), - min: variadic("min", Math.min), - hypot: variadic("hypot", Math.hypot), - abs: unary("abs", Math.abs), - acos: unary("acos", Math.acos), - acosh: unary("acosh", Math.acosh), - asin: unary("asin", Math.asin), - asinh: unary("asinh", Math.asinh), - atan: unary("atan", Math.atan), - atan2: binary("atan2", Math.atan2), - atanh: unary("atanh", Math.atanh), - floor: unary("floor", Math.floor), - ceil: unary("ceil", Math.ceil), - round: unary("round", Math.round), - trunc: unary("trunc", Math.trunc), - sign: unary("sign", Math.sign), - sqrt: unary("sqrt", Math.sqrt), - cbrt: unary("cbrt", Math.cbrt), - pow: binary("pow", Math.pow), - cos: unary("cos", Math.cos), - cosh: unary("cosh", Math.cosh), - sin: unary("sin", Math.sin), - sinh: unary("sinh", Math.sinh), - tan: unary("tan", Math.tan), - tanh: unary("tanh", Math.tanh), - log: unary("log", Math.log), - log2: unary("log2", Math.log2), - log10: unary("log10", Math.log10), - log1p: unary("log1p", Math.log1p), - exp: unary("exp", Math.exp), - expm1: unary("expm1", Math.expm1), - f16round: unary("f16round", Math.f16round), - fround: unary("fround", Math.fround), - clz32: unary("clz32", Math.clz32), - imul: binary("imul", Math.imul), - sumPrecise: sumPrecise(runner), }) + methods(protos, math, [ + ["random", 0, () => Math.random()], + variadic("max", Math.max), + variadic("min", Math.min), + variadic("hypot", Math.hypot), + unary("abs", Math.abs), + unary("acos", Math.acos), + unary("acosh", Math.acosh), + unary("asin", Math.asin), + unary("asinh", Math.asinh), + unary("atan", Math.atan), + binary("atan2", Math.atan2), + unary("atanh", Math.atanh), + unary("floor", Math.floor), + unary("ceil", Math.ceil), + unary("round", Math.round), + unary("trunc", Math.trunc), + unary("sign", Math.sign), + unary("sqrt", Math.sqrt), + unary("cbrt", Math.cbrt), + binary("pow", Math.pow), + unary("cos", Math.cos), + unary("cosh", Math.cosh), + unary("sin", Math.sin), + unary("sinh", Math.sinh), + unary("tan", Math.tan), + unary("tanh", Math.tanh), + unary("log", Math.log), + unary("log2", Math.log2), + unary("log10", Math.log10), + unary("log1p", Math.log1p), + unary("exp", Math.exp), + unary("expm1", Math.expm1), + unary("f16round", Math.f16round), + unary("fround", Math.fround), + unary("clz32", Math.clz32), + binary("imul", Math.imul), + [ + "sumPrecise", + 1, + (_, args, node) => + Effect.gen(function* () { + const cursor = yield* runner.syncIterator(args[0], node) + if (cursor === undefined) { + throw new InterpreterRuntimeError("Math.sumPrecise expects a synchronous iterable.", node) + } + const numbers: Array = [] + while (true) { + const step = yield* cursor.next + if (step.done) return Math.sumPrecise(numbers) + yield* preserveConsumerError( + cursor, + Effect.sync(() => { + if (typeof step.value !== "number") { + throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node) + } + numbers.push(step.value) + }), + ) + } + }), + ], + ]) + return math +} diff --git a/packages/codemode/src/stdlib/number.ts b/packages/codemode/src/stdlib/number.ts index bad750cab6af..24846fed4e77 100644 --- a/packages/codemode/src/stdlib/number.ts +++ b/packages/codemode/src/stdlib/number.ts @@ -1,58 +1,16 @@ -import { toProgram } from "../data.js" -import { sync } from "../interpreter/host.js" +import { constructor, constants, methods } from "../interpreter/native.js" import { type AstNode, InterpreterRuntimeError, rangeError } from "../interpreter/model.js" +import type { Runner } from "../interpreter/runner.js" import { coercion, coerceToString } from "./value.js" -export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"]) - -export const invokeNumberMethod = (value: number, name: string, args: Array, node: AstNode): unknown => { - const optNum = (index: number): number | undefined => { - const arg = args[index] - if (arg === undefined) return undefined - if (typeof arg !== "number") throw new InterpreterRuntimeError(`Number.${name} expects a number argument.`, node) - return arg - } - let result: unknown - switch (name) { - case "toFixed": - result = value.toFixed(optNum(0)) - break - case "toExponential": - result = value.toExponential(optNum(0)) - break - case "toPrecision": { - const digits = optNum(0) - result = digits === undefined ? value.toString() : value.toPrecision(digits) - break - } - case "toString": { - const radix = optNum(0) - if (radix !== undefined && (radix < 2 || radix > 36)) { - throw rangeError("Number.toString radix must be between 2 and 36.", node) - } - result = value.toString(radix) - break - } - case "valueOf": - result = value - break - default: - throw new InterpreterRuntimeError(`Number method '${name}' is not available.`, node) - } - return toProgram(result, `Number.${name} result`) -} - -const parseIntStatic = sync("Number.parseInt", (args, node) => { - const radix = args[1] - if (radix !== undefined && typeof radix !== "number") { - throw new InterpreterRuntimeError("Number.parseInt expects a numeric radix.", node) - } - return parseInt(coerceToString(args[0]), radix) -}) - -export const numberGlobal = coercion("Number", { - instanceOf: () => false, - members: { +export const numberGlobal = (runner: Runner) => { + const protos = runner.prototypes + const number = constructor(protos, protos.Number, { + name: "Number", + length: 1, + call: coercion(runner, "Number").call, + }) + constants(number, { MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER, MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER, MAX_VALUE: Number.MAX_VALUE, @@ -61,11 +19,87 @@ export const numberGlobal = coercion("Number", { NaN: Number.NaN, POSITIVE_INFINITY: Number.POSITIVE_INFINITY, NEGATIVE_INFINITY: Number.NEGATIVE_INFINITY, - isInteger: sync("Number.isInteger", (args) => Number.isInteger(args[0])), - isFinite: sync("Number.isFinite", (args) => Number.isFinite(args[0])), - isNaN: sync("Number.isNaN", (args) => Number.isNaN(args[0])), - isSafeInteger: sync("Number.isSafeInteger", (args) => Number.isSafeInteger(args[0])), - parseInt: parseIntStatic, - parseFloat: sync("Number.parseFloat", (args) => parseFloat(coerceToString(args[0]))), - }, -}) + }) + methods(protos, number, [ + ["isInteger", 1, (_, args) => Number.isInteger(args[0])], + ["isFinite", 1, (_, args) => Number.isFinite(args[0])], + ["isNaN", 1, (_, args) => Number.isNaN(args[0])], + ["isSafeInteger", 1, (_, args) => Number.isSafeInteger(args[0])], + [ + "parseInt", + 2, + (_, args, node) => { + const radix = args[1] + if (radix !== undefined && typeof radix !== "number") { + throw new InterpreterRuntimeError("Number.parseInt expects a numeric radix.", node) + } + return parseInt(coerceToString(args[0]), radix) + }, + ], + ["parseFloat", 1, (_, args) => parseFloat(coerceToString(args[0]))], + ]) + + const self = (thisValue: unknown, name: string, node: AstNode): number => { + if (typeof thisValue === "number") return thisValue + throw new InterpreterRuntimeError(`Number.prototype.${name} requires that 'this' be a Number.`, node) + } + const optNum = (name: string, arg: unknown, node: AstNode): number | undefined => { + if (arg === undefined) return undefined + if (typeof arg !== "number") throw new InterpreterRuntimeError(`Number.${name} expects a number argument.`, node) + return arg + } + methods(protos, protos.Number, [ + [ + "toFixed", + 1, + (thisValue, args, node) => self(thisValue, "toFixed", node).toFixed(optNum("toFixed", args[0], node)), + ], + [ + "toExponential", + 1, + (thisValue, args, node) => + self(thisValue, "toExponential", node).toExponential(optNum("toExponential", args[0], node)), + ], + [ + "toPrecision", + 1, + (thisValue, args, node) => { + const value = self(thisValue, "toPrecision", node) + const digits = optNum("toPrecision", args[0], node) + return digits === undefined ? value.toString() : value.toPrecision(digits) + }, + ], + [ + "toString", + 1, + (thisValue, args, node) => { + const value = self(thisValue, "toString", node) + const radix = optNum("toString", args[0], node) + if (radix !== undefined && (radix < 2 || radix > 36)) { + throw rangeError("Number.toString radix must be between 2 and 36.", node) + } + return value.toString(radix) + }, + ], + ["valueOf", 0, (thisValue, _, node) => self(thisValue, "valueOf", node)], + ]) + return number +} + +export const booleanGlobal = (runner: Runner) => { + const protos = runner.prototypes + const boolean = constructor(protos, protos.Boolean, { + name: "Boolean", + length: 1, + call: coercion(runner, "Boolean").call, + }) + const self = (thisValue: unknown, name: string, node: AstNode): boolean => { + if (typeof thisValue === "boolean") return thisValue + throw new InterpreterRuntimeError(`Boolean.prototype.${name} requires that 'this' be a Boolean.`, node) + } + methods(protos, protos.Boolean, [ + ["toString", 0, (thisValue, _, node) => String(self(thisValue, "toString", node))], + ["valueOf", 0, (thisValue, _, node) => self(thisValue, "valueOf", node)], + ]) + return boolean +} diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index 442108d7698a..8c2c08d16cc5 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { toProgram } from "../data.js" -import { HostFunction, sync, syncCall } from "../interpreter/host.js" +import { constructor, methods, receiver } from "../interpreter/native.js" import { type AstNode, AsyncIteratorSymbol, @@ -8,25 +8,37 @@ import { IteratorSymbol, rangeError, } from "../interpreter/model.js" -import { getOwn, hasOwn, ownEntries, ownKeys, ProgramArray, ProgramObject, set } from "../interpreter/objects.js" import { - containsOpaqueReference, - describeValue, - rejectCircularInsertion, - typeofValue, -} from "../interpreter/references.js" + Callable, + define, + entries, + enumerableKeys, + getOwn, + hasOwn, + hasPrototype, + hidden, + keys, + own, + ProgramArray, + ProgramDate, + ProgramError, + ProgramObject, + ProgramPromise, + ProgramRegExp, + set, +} from "../interpreter/objects.js" +import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js" import { preserveConsumerError, type Runner } from "../interpreter/runner.js" import { ToolReference } from "../tool-runtime.js" -import { Values } from "../values.js" import { groupBy } from "./collections.js" import { coerceToString } from "./value.js" // ToObject for enumeration. -export const enumerableSource = (label: string, value: unknown, node: AstNode): ProgramObject => { +export const enumerableSource = (runner: Runner, label: string, value: unknown, node: AstNode): ProgramObject => { if (value === null || value === undefined) { throw new InterpreterRuntimeError(`${label} cannot convert ${describeValue(value)} to an object.`, node) } - if (value instanceof Values.Promise) { + if (value instanceof ProgramPromise) { throw new InterpreterRuntimeError( `${label} received an un-awaited Promise; await it before inspecting the result.`, node, @@ -40,12 +52,12 @@ export const enumerableSource = (label: string, value: unknown, node: AstNode): "InvalidDataValue", ) } - if (typeof value === "string") return new ProgramArray([...value]) + if (typeof value === "string") return new ProgramArray(runner.prototypes.Array, [...value]) if (value instanceof ProgramObject) return value - return new ProgramObject() + return new ProgramObject(runner.prototypes.Object) } -export const objectAssign = (args: Array, node: AstNode): unknown => { +export const objectAssign = (runner: Runner, args: Array, node: AstNode): unknown => { const target = args[0] // JS would box a primitive target; wrappers and primitives cannot hold fields here. if (!(target instanceof ProgramObject)) { @@ -57,12 +69,12 @@ export const objectAssign = (args: Array, node: AstNode): unknown => { const seen = new Set() for (const source of args.slice(1)) { if (source === null || source === undefined) continue - const from = enumerableSource("Object.assign(...)", source, node) - for (const key of ownKeys(from)) { - if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue + const from = enumerableSource(runner, "Object.assign(...)", source, node) + for (const key of enumerableKeys(from)) { rejectCircularInsertion(target, getOwn(from, key), "Object.assign result", node, seen) if (!set(target, key, getOwn(from, key))) { - throw rangeError("Invalid array length", node) + if (target instanceof ProgramArray && key === "length") throw rangeError("Invalid array length", node) + throw new InterpreterRuntimeError(`Cannot assign to read only property '${String(key)}'.`, node) } } } @@ -74,7 +86,7 @@ const objectFromEntries = ( source: unknown, node: AstNode, ): Effect.Effect => { - const out = new ProgramObject() + const out = new ProgramObject(runner.prototypes.Object) return Effect.gen(function* () { const cursor = yield* runner.syncIterator(source, node) if (cursor === undefined) { @@ -89,69 +101,136 @@ const objectFromEntries = ( if (!(step.value instanceof ProgramObject) || containsOpaqueReference(step.value)) { throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node) } - set(out, coerceToString(getOwn(step.value, 0)), getOwn(step.value, 1)) + define(out, coerceToString(getOwn(step.value, 0)), getOwn(step.value, 1)) }), ) } }) } -const constructObject = (args: Array, node: AstNode): unknown => { - const first = args[0] - if (first === null || first === undefined) return new ProgramObject() - if (typeof first === "object") return first - throw new InterpreterRuntimeError( - `Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`, - node, - ) +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 Callable) return "Function" + if (value instanceof ProgramError) return "Error" + if (value instanceof ProgramDate) return "Date" + if (value instanceof ProgramRegExp) return "RegExp" + if (typeof value === "string") return "String" + if (typeof value === "number") return "Number" + if (typeof value === "boolean") return "Boolean" + return "Object" } +const propertyKey = (value: unknown): PropertyKey => + value === AsyncIteratorSymbol || value === IteratorSymbol ? value : coerceToString(value) + // Object constructs identically with or without new, like JS. Only `keys` copies its result into the // program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values. -export const objectGlobal = (runner: Runner, toolKeys: (path: ReadonlyArray) => ReadonlyArray) => - new HostFunction({ +export const objectGlobal = ( + runner: Runner, + toolKeys: (path: ReadonlyArray) => ReadonlyArray, +) => { + const protos = runner.prototypes + const construct = (args: Array, node: AstNode): unknown => { + const first = args[0] + if (first === null || first === undefined) return new ProgramObject(protos.Object) + if (typeof first === "object") return first + throw new InterpreterRuntimeError( + `Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`, + node, + ) + } + const object = constructor(protos, protos.Object, { name: "Object", - call: syncCall(constructObject), - construct: syncCall(constructObject), - instanceOf: (value) => value !== null && (typeof value === "object" || typeofValue(value) === "function"), - members: { - keys: sync("Object.keys", (args, node) => + length: 1, + call: (_, args, node) => Effect.sync(() => construct(args, node)), + construct: (args, _, node) => Effect.sync(() => construct(args, node)), + }) + methods(protos, object, [ + [ + "keys", + 1, + (_, args, node) => toProgram( + protos, args[0] instanceof ToolReference ? [...toolKeys(args[0].path)] - : ownKeys(enumerableSource("Object.keys(...)", args[0], node)).filter((key) => typeof key === "string"), + : keys(enumerableSource(runner, "Object.keys(...)", args[0], node)), "Object.keys result", ), - ), - values: sync( - "Object.values", - (args, node) => - new ProgramArray(ownEntries(enumerableSource("Object.values(...)", args[0], node)).map((entry) => entry[1])), - ), - entries: sync( - "Object.entries", - (args, node) => - new ProgramArray( - ownEntries(enumerableSource("Object.entries(...)", args[0], node)).map((entry) => new ProgramArray(entry)), + ], + [ + "values", + 1, + (_, args, node) => + new ProgramArray( + protos.Array, + entries(enumerableSource(runner, "Object.values(...)", args[0], node)).map((entry) => entry[1]), + ), + ], + [ + "entries", + 1, + (_, args, node) => + new ProgramArray( + protos.Array, + entries(enumerableSource(runner, "Object.entries(...)", args[0], node)).map( + (entry) => new ProgramArray(protos.Array, entry), ), - ), - hasOwn: sync("Object.hasOwn", (args, node) => - hasOwn( - enumerableSource("Object.hasOwn(...)", args[0], node), - args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]), ), - ), - is: sync("Object.is", (args, node) => { + ], + [ + "hasOwn", + 2, + (_, args, node) => hasOwn(enumerableSource(runner, "Object.hasOwn(...)", args[0], node), propertyKey(args[1])), + ], + [ + "is", + 2, + (_, args, node) => { if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) { throw new InterpreterRuntimeError("Object.is requires data values.", node, "InvalidDataValue") } return Object.is(args[0], args[1]) - }), - assign: sync("Object.assign", objectAssign), - fromEntries: new HostFunction({ - name: "Object.fromEntries", - call: (args, node) => Effect.suspend(() => objectFromEntries(runner, args[0], node)), - }), - groupBy: groupBy(runner, "Object"), - }, - }) + }, + ], + ["assign", 2, (_, args, node) => objectAssign(runner, args, node)], + ["fromEntries", 1, (_, args, node) => objectFromEntries(runner, args[0], node)], + ]) + define(object, "groupBy", groupBy(runner, "Object"), hidden) + methods(protos, protos.Object, [ + [ + "hasOwnProperty", + 1, + (thisValue, args, node) => + hasOwn(receiver(ProgramObject, thisValue, "Object.prototype.hasOwnProperty", node), propertyKey(args[0])), + ], + [ + "isPrototypeOf", + 1, + (thisValue, args, node) => + hasPrototype(args[0], receiver(ProgramObject, thisValue, "Object.prototype.isPrototypeOf", node)), + ], + [ + "propertyIsEnumerable", + 1, + (thisValue, args, node) => + own(receiver(ProgramObject, thisValue, "Object.prototype.propertyIsEnumerable", node), propertyKey(args[0])) + ?.enumerable === true, + ], + ["toString", 0, (thisValue) => `[object ${classTag(thisValue)}]`], + ["toLocaleString", 0, (thisValue) => `[object ${classTag(thisValue)}]`], + [ + "valueOf", + 0, + (thisValue, _, node) => { + if (thisValue === null || thisValue === undefined) { + throw new InterpreterRuntimeError("Object.prototype.valueOf called on null or undefined.", node) + } + return thisValue + }, + ], + ]) + return object +} diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index c6e9c9e3b1a5..f2739c7e6186 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -1,15 +1,21 @@ -import { sync, syncCall } from "../interpreter/host.js" +import { Effect } from "effect" +import type { Prototypes } from "../interpreter/intrinsics.js" +import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js" import { type AstNode, InterpreterRuntimeError, syntaxError } from "../interpreter/model.js" -import { ProgramArray, record, set } from "../interpreter/objects.js" -import { Values } from "../values.js" +import { + define, + defineAccessor, + getOwn, + ProgramArray, + ProgramObject, + ProgramRegExp, + record, + set, +} from "../interpreter/objects.js" +import type { Runner } from "../interpreter/runner.js" import { coerceToNumber, coerceToString } from "./value.js" -export const regexpMethods = new Set(["test", "exec", "toString"]) - -export const regexpProperties = new Set([ - "source", - "flags", - "lastIndex", +const flagProperties = [ "hasIndices", "global", "ignoreCase", @@ -18,7 +24,7 @@ export const regexpProperties = new Set([ "unicode", "unicodeSets", "dotAll", -]) +] as const const regexFailureReason = (error: unknown): string => (error instanceof Error ? error.message : String(error)).replace(/^Invalid regular expression:\s*/i, "") @@ -29,7 +35,7 @@ const escapeRegexHint = export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFlags = ""): RegExp => { // Native parity: an undefined pattern behaves as an empty pattern. if (arg === undefined) return new RegExp("", extraFlags) - if (arg instanceof Values.RegExp) return arg.regex + if (arg instanceof ProgramRegExp) return arg.regex if (typeof arg === "string") { try { return new RegExp(arg, extraFlags) @@ -46,18 +52,26 @@ export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFl ) } -export const matchToValue = (match: RegExpMatchArray): ProgramArray => { - const result = new ProgramArray(Array.from(match, (group) => group)) - if (match.index !== undefined) set(result, "index", match.index) - if (match.input !== undefined) set(result, "input", match.input) - if (match.groups) set(result, "groups", record(match.groups)) - if (match.indices) set(result, "indices", indicesToValue(match.indices)) +export const matchToValue = (protos: Prototypes, match: RegExpMatchArray): ProgramArray => { + const result = new ProgramArray( + protos.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)) return result } -export const constructRegExp = (args: Array, node: AstNode): Values.RegExp => { +export const constructRegExp = ( + protos: Prototypes, + args: Array, + node: AstNode, + proto: ProgramObject = protos.RegExp, +): ProgramRegExp => { const first = args[0] - const pattern = first instanceof Values.RegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) + const pattern = first instanceof ProgramRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) const flagsArg = args[1] if (flagsArg !== undefined && typeof flagsArg !== "string") { throw syntaxError( @@ -65,9 +79,9 @@ export const constructRegExp = (args: Array, node: AstNode): Values.Reg node, ) } - const flags = flagsArg ?? (first instanceof Values.RegExp ? first.regex.flags : "") + const flags = flagsArg ?? (first instanceof ProgramRegExp ? first.regex.flags : "") try { - return new Values.RegExp(pattern, flags) + return new ProgramRegExp(proto, pattern, flags) } catch (error) { const reason = regexFailureReason(error) throw syntaxError( @@ -79,65 +93,73 @@ export const constructRegExp = (args: Array, node: AstNode): Values.Reg } } +const toLength = (value: unknown): number => { + const number = coerceToNumber(value) + if (Number.isNaN(number) || number <= 0) return 0 + return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) +} + // RegExp constructs identically with or without new, like JS. -export const regexpGlobal = sync("RegExp", constructRegExp, { - construct: syncCall(constructRegExp), - instanceOf: (value) => value instanceof Values.RegExp, - members: { - escape: sync("RegExp.escape", (args, node) => { - if (typeof args[0] !== "string") { - throw new InterpreterRuntimeError("RegExp.escape expects a string.", node) - } - return RegExp.escape(args[0]) - }), - }, -}) +export const regexpGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.RegExp + const regexp = constructor(protos, proto, { + name: "RegExp", + length: 2, + call: (_, args, node) => Effect.sync(() => constructRegExp(protos, args, node)), + construct: (args, newTarget, node) => + Effect.sync(() => constructRegExp(protos, args, node, prototypeFrom(newTarget, proto))), + }) + methods(protos, regexp, [ + [ + "escape", + 1, + (_, args, node) => { + if (typeof args[0] !== "string") throw new InterpreterRuntimeError("RegExp.escape expects a string.", node) + return RegExp.escape(args[0]) + }, + ], + ]) -export const invokeRegExpMethod = ( - value: Values.RegExp, - name: string, - args: Array, - node: AstNode, -): unknown => { - switch (name) { - case "test": - case "exec": { + const self = (thisValue: unknown, name: string, node?: AstNode) => + receiver(ProgramRegExp, thisValue, `RegExp.prototype.${name}`, node) + 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]) + // exec/test run the host regex from the program-visible lastIndex and write it back only when g or y is set. + const run = (name: "exec" | "test"): Method => [ + name, + 1, + (thisValue, args, node) => { + const value = self(thisValue, name, node) const input = coerceToString(args[0]) - const lastIndex = value.lastIndex const stateful = value.regex.global || value.regex.sticky - value.regex.lastIndex = toLength(lastIndex) - if (name === "test") { - const matched = value.regex.test(input) - if (!stateful) value.lastIndex = lastIndex - return matched - } + value.regex.lastIndex = toLength(getOwn(value, "lastIndex")) const matched = value.regex.exec(input) - if (!stateful) value.lastIndex = lastIndex - return matched === null ? null : matchToValue(matched) - } - case "toString": - return coerceToString(value) - default: - throw new InterpreterRuntimeError(`RegExp method '${name}' is not available.`, node) - } -} - -const toLength = (value: unknown): number => { - const number = coerceToNumber(value) - if (Number.isNaN(number) || number <= 0) return 0 - return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER) + if (stateful) set(value, "lastIndex", value.regex.lastIndex) + if (name === "test") return matched !== null + return matched === null ? null : matchToValue(protos, matched) + }, + ] + methods(protos, proto, [ + run("exec"), + run("test"), + ["toString", 0, (thisValue, _, node) => coerceToString(self(thisValue, "toString", node))], + ]) + return regexp } -const indicesToValue = (indices: RegExpIndicesArray): ProgramArray => { - const range = (pair: [number, number] | undefined) => (pair === undefined ? undefined : new ProgramArray([...pair])) - const result = new ProgramArray(Array.from(indices, range)) +const indicesToValue = (protos: Prototypes, indices: RegExpIndicesArray): ProgramArray => { + const range = (pair: [number, number] | undefined) => + pair === undefined ? undefined : new ProgramArray(protos.Array, [...pair]) + const result = new ProgramArray(protos.Array, Array.from(indices, range)) const groups = indices.groups - set( + define( result, "groups", groups === undefined ? undefined - : record(Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))), + : record(protos.Object, Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))), ) return result } diff --git a/packages/codemode/src/stdlib/string.ts b/packages/codemode/src/stdlib/string.ts index a22361eeda1d..4f844d608355 100644 --- a/packages/codemode/src/stdlib/string.ts +++ b/packages/codemode/src/stdlib/string.ts @@ -1,58 +1,268 @@ -import { sync } from "../interpreter/host.js" -import { InterpreterRuntimeError } from "../interpreter/model.js" -import { coercion } from "./value.js" - -export const stringMethods = new Set([ - "toLowerCase", - "toUpperCase", - "trim", - "trimStart", - "trimEnd", - "trimLeft", - "trimRight", - "split", - "slice", - "substring", - "substr", - "includes", - "startsWith", - "endsWith", - "indexOf", - "lastIndexOf", - "replace", - "replaceAll", - "repeat", - "padStart", - "padEnd", - "charAt", - "charCodeAt", - "codePointAt", - "at", - "concat", - "toString", - "match", - "matchAll", - "search", - "localeCompare", - "normalize", - "isWellFormed", - "toWellFormed", -]) - -const codeUnits = (name: string, op: (...codes: Array) => string) => - sync(`String.${name}`, (args, node) => - op( - ...args.map((arg) => { - if (typeof arg !== "number") throw new InterpreterRuntimeError(`String.${name} expects number arguments.`, node) - return arg - }), - ), - ) - -export const stringGlobal = coercion("String", { - instanceOf: () => false, - members: { - fromCharCode: codeUnits("fromCharCode", String.fromCharCode), - fromCodePoint: codeUnits("fromCodePoint", String.fromCodePoint), - }, -}) +import { Effect } from "effect" +import { toProgram } from "../data.js" +import { constructor, type Method, methods } from "../interpreter/native.js" +import { type AstNode, InterpreterRuntimeError, rangeError } from "../interpreter/model.js" +import { ProgramArray, ProgramPromise, ProgramRegExp, record } from "../interpreter/objects.js" +import { containsOpaqueReference, typeofValue } from "../interpreter/references.js" +import { applyCollectionCallback, isSupportedCallback, type Runner } from "../interpreter/runner.js" +import { matchToValue, toHostRegex } from "./regexp.js" +import { coerceToNumber, coerceToString, coercion } from "./value.js" + +// console is intercepted by the interpreter before reaching here. +const requireDataArgument = (name: string, index: number, arg: unknown, node: AstNode): unknown => { + if (containsOpaqueReference(arg)) { + throw new InterpreterRuntimeError( + `String.${name} expects argument ${index + 1} to be a data value.`, + node, + "InvalidDataValue", + ) + } + return arg +} + +const replaceAllNeedsGlobal = (pattern: RegExp, node: AstNode) => { + if (!pattern.global) { + throw new InterpreterRuntimeError( + `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`, + node, + ) + } +} + +const replaceWithCallback = ( + runner: Runner, + value: string, + name: "replace" | "replaceAll", + args: Array, + node: AstNode, +): Effect.Effect => { + const protos = runner.prototypes + const apply = applyCollectionCallback(runner, args[1], `String.${name}`, node) + const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array }> = [] + const collect = (...callbackArgs: Array): string => { + const match = callbackArgs[0] + const groups = callbackArgs[callbackArgs.length - 1] + const hasGroups = groups !== null && typeof groups === "object" + const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)] + if (typeof match !== "string" || typeof offset !== "number") { + throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node) + } + if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(protos.Object, groups as Record) + matches.push({ match, offset, args: callbackArgs }) + return match + } + + const pattern = args[0] + if (pattern instanceof ProgramRegExp) { + if (name === "replaceAll") replaceAllNeedsGlobal(pattern.regex, node) + if (name === "replace") value.replace(pattern.regex, collect) + else value.replaceAll(pattern.regex, collect) + } else { + const search = coerceToString(requireDataArgument(name, 0, pattern, node)) + if (name === "replace") value.replace(search, collect) + else value.replaceAll(search, collect) + } + + return Effect.gen(function* () { + const output: Array = [] + let end = 0 + for (const match of matches) { + const replacement = yield* apply(match.args) + output.push( + value.slice(end, match.offset), + replacement instanceof ProgramPromise + ? "[object Promise]" + : coerceToString(toProgram(protos, replacement, `String.${name} replacer result`)), + ) + end = match.offset + match.match.length + } + output.push(value.slice(end)) + return output.join("") + }) +} + +export const stringGlobal = (runner: Runner) => { + const protos = runner.prototypes + const string = constructor(protos, protos.String, { + name: "String", + length: 1, + call: coercion(runner, "String").call, + }) + const codeUnits = (name: string, op: (...codes: Array) => string): Method => [ + name, + 1, + (_, args, node) => + op( + ...args.map((arg) => { + if (typeof arg !== "number") { + throw new InterpreterRuntimeError(`String.${name} expects number arguments.`, node) + } + return arg + }), + ), + ] + methods(protos, string, [ + codeUnits("fromCharCode", String.fromCharCode), + codeUnits("fromCodePoint", String.fromCodePoint), + ]) + + const self = (thisValue: unknown, name: string, node: AstNode): string => { + if (typeof thisValue === "string") return thisValue + if (thisValue === null || thisValue === undefined) { + throw new InterpreterRuntimeError(`String.prototype.${name} called on null or undefined.`, node) + } + return coerceToString(thisValue) + } + // Coerce arguments like native JS; opaque runtime references still reject. + const str = (name: string, args: Array, index: number, node: AstNode): string => + coerceToString(requireDataArgument(name, index, args[index], node)) + const num = (name: string, args: Array, index: number, node: AstNode): number => + coerceToNumber(requireDataArgument(name, index, args[index], node)) + const optNum = (name: string, args: Array, index: number, node: AstNode): number | undefined => + args[index] === undefined ? undefined : num(name, args, index, node) + const optStr = (name: string, args: Array, index: number, node: AstNode): string | undefined => + args[index] === undefined ? undefined : str(name, args, index, node) + const rejectRegex = (name: string, args: Array, node: AstNode): void => { + if (args[0] instanceof ProgramRegExp) { + throw new InterpreterRuntimeError( + `String.${name} cannot take a regular expression; use regex.test(string) or String.search instead.`, + node, + ) + } + } + const simple = ( + name: string, + length: number, + op: (value: string, args: Array, node: AstNode) => unknown, + ): Method => [name, length, (thisValue, args, node) => op(self(thisValue, name, node), args, node)] + const replace = (name: "replace" | "replaceAll") => + simple(name, 2, (value, args, node) => { + if (isSupportedCallback(args[1])) return replaceWithCallback(runner, value, name, args, node) + if (typeofValue(args[1]) === "function") { + throw new InterpreterRuntimeError( + `String.${name} cannot use this callable as a replacer; wrap it in an arrow function, e.g. (match) => tools.ns.tool(match).`, + node, + ) + } + if (args[0] instanceof ProgramRegExp) { + const pattern = args[0].regex + const replacement = str(name, args, 1, node) + if (name === "replaceAll") replaceAllNeedsGlobal(pattern, node) + return name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement) + } + if (name === "replace") return value.replace(str(name, args, 0, node), str(name, args, 1, node)) + return value.replaceAll(str(name, args, 0, node), str(name, args, 1, node)) + }) + + methods(protos, protos.String, [ + simple("toString", 0, (value) => value), + simple("valueOf", 0, (value) => value), + simple("toLowerCase", 0, (value) => value.toLowerCase()), + simple("toUpperCase", 0, (value) => value.toUpperCase()), + simple("trim", 0, (value) => value.trim()), + simple("trimStart", 0, (value) => value.trimStart()), + simple("trimLeft", 0, (value) => value.trimStart()), + simple("trimEnd", 0, (value) => value.trimEnd()), + simple("trimRight", 0, (value) => value.trimEnd()), + // Locale/options are deliberately unsupported; comparison uses the host default locale. + simple("localeCompare", 1, (value, args, node) => value.localeCompare(str("localeCompare", args, 0, node))), + simple("normalize", 0, (value, args, node) => { + const form = optStr("normalize", args, 0, node) + try { + return value.normalize(form) + } catch { + throw rangeError( + `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`, + node, + ) + } + }), + simple("split", 2, (value, args, node) => { + const wrap = (parts: Array) => new ProgramArray(protos.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, node) + if (args[0] === undefined) { + return wrap(requestedLimit !== undefined && requestedLimit >>> 0 === 0 ? [] : [value]) + } + if (args[0] instanceof ProgramRegExp) return wrap(value.split(args[0].regex, requestedLimit)) + return wrap( + value.split(str("split", args, 0, node), requestedLimit === undefined ? undefined : requestedLimit >>> 0), + ) + }), + simple("slice", 2, (value, args, node) => + value.slice(optNum("slice", args, 0, node), optNum("slice", args, 1, node)), + ), + simple("includes", 1, (value, args, node) => { + rejectRegex("includes", args, node) + return value.includes(str("includes", args, 0, node), optNum("includes", args, 1, node)) + }), + simple("startsWith", 1, (value, args, node) => { + rejectRegex("startsWith", args, node) + return value.startsWith(str("startsWith", args, 0, node), optNum("startsWith", args, 1, node)) + }), + simple("endsWith", 1, (value, args, node) => { + rejectRegex("endsWith", args, node) + return value.endsWith(str("endsWith", args, 0, node), optNum("endsWith", args, 1, node)) + }), + simple("indexOf", 1, (value, args, node) => + value.indexOf(str("indexOf", args, 0, node), optNum("indexOf", args, 1, node)), + ), + simple("lastIndexOf", 1, (value, args, node) => + value.lastIndexOf(str("lastIndexOf", args, 0, node), optNum("lastIndexOf", args, 1, node)), + ), + replace("replace"), + replace("replaceAll"), + simple("match", 1, (value, args, node) => { + const pattern = toHostRegex(args[0], "match", node) + 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) + }), + simple("matchAll", 1, (value, args, node) => { + const pattern = toHostRegex(args[0], "matchAll", node, "g") + if (!pattern.global) { + throw new InterpreterRuntimeError( + `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`, + node, + ) + } + return new ProgramArray( + protos.Array, + Array.from(value.matchAll(pattern), (match) => matchToValue(protos, match)), + ) + }), + simple("search", 1, (value, args, node) => value.search(toHostRegex(args[0], "search", node))), + simple("repeat", 1, (value, args, node) => { + const count = num("repeat", args, 0, node) + if (!Number.isFinite(count) || count < 0) { + throw rangeError("String.repeat expects a finite non-negative count.", node) + } + return value.repeat(count) + }), + simple("padStart", 1, (value, args, node) => + value.padStart(num("padStart", args, 0, node), optStr("padStart", args, 1, node)), + ), + simple("padEnd", 1, (value, args, node) => + value.padEnd(num("padEnd", args, 0, node), optStr("padEnd", args, 1, node)), + ), + simple("charAt", 1, (value, args, node) => value.charAt(optNum("charAt", args, 0, node) ?? 0)), + simple("at", 1, (value, args, node) => value.at(optNum("at", args, 0, node) ?? 0)), + simple("substring", 2, (value, args, node) => + value.substring(optNum("substring", args, 0, node) ?? 0, optNum("substring", args, 1, node)), + ), + simple("substr", 2, (value, args, node) => + value.substr(optNum("substr", args, 0, node) ?? 0, optNum("substr", args, 1, node)), + ), + simple("isWellFormed", 0, (value) => value.isWellFormed()), + simple("toWellFormed", 0, (value) => value.toWellFormed()), + simple("charCodeAt", 1, (value, args, node) => value.charCodeAt(optNum("charCodeAt", args, 0, node) ?? 0)), + simple("codePointAt", 1, (value, args, node) => value.codePointAt(optNum("codePointAt", args, 0, node) ?? 0)), + simple("concat", 1, (value, args, node) => + value.concat(...args.map((_, index) => str("concat", args, index, node))), + ), + ]) + return string +} diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts index 0b29f4f60ab0..590ed6dcbfd3 100644 --- a/packages/codemode/src/stdlib/url.ts +++ b/packages/codemode/src/stdlib/url.ts @@ -1,14 +1,22 @@ import { Effect } from "effect" -import { toProgram } from "../data.js" -import { HostFunction, requiresNew, sync, syncCall } from "../interpreter/host.js" +import { toProgram, ToolRuntimeError } from "../data.js" +import type { Prototypes } from "../interpreter/intrinsics.js" +import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js" import { type AstNode, InterpreterRuntimeError, uriError } from "../interpreter/model.js" -import { ownEntries, ProgramObject } from "../interpreter/objects.js" +import { + defineAccessor, + entries, + isWrapper, + ProgramArray, + ProgramObject, + ProgramURL, + ProgramURLSearchParams, +} from "../interpreter/objects.js" import { isRuntimeReference } from "../interpreter/references.js" -import { preserveConsumerError, type Runner } from "../interpreter/runner.js" -import { Values } from "../values.js" +import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js" import { coerceToString } from "./value.js" -export const urlProperties = new Set([ +const urlProperties = [ "href", "origin", "protocol", @@ -20,38 +28,10 @@ export const urlProperties = new Set([ "pathname", "search", "hash", -]) +] as const -export const urlWritableProperties = new Set([ - "href", - "protocol", - "username", - "password", - "host", - "hostname", - "port", - "pathname", - "search", - "hash", -]) - -export const urlMethods = new Set(["toString", "toJSON"]) -export const urlSearchParamsMethods = new Set([ - "append", - "delete", - "get", - "getAll", - "has", - "set", - "sort", - "forEach", - "keys", - "values", - "entries", - "toString", -]) - -export const uriArgument = (value: unknown, label: string): string => coerceToString(toProgram(value, label)) +export const uriArgument = (protos: Prototypes, value: unknown, label: string): string => + coerceToString(toProgram(protos, value, label)) type UriFunction = "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent" @@ -62,9 +42,9 @@ const uriFunctions: Record string> = { decodeURIComponent, } -export const uriGlobal = (name: UriFunction) => - sync(name, (args, node) => { - const value = uriArgument(args[0], `${name} input`) +export const uriGlobal = (runner: Runner, name: UriFunction) => + fn(runner.prototypes, name, 1, (_, args, node) => { + const value = uriArgument(runner.prototypes, args[0], `${name} input`) try { return uriFunctions[name](value) } catch (error) { @@ -75,53 +55,79 @@ export const uriGlobal = (name: UriFunction) => } }) -export const urlArgument = (value: unknown, label: string): string => - value instanceof Values.URL ? value.url.href : uriArgument(value, label) +const urlArgument = (protos: Prototypes, value: unknown, label: string): string => + value instanceof ProgramURL ? value.url.href : uriArgument(protos, value, label) -const urlStatic = (name: "canParse" | "parse") => - sync(`URL.${name}`, (args, node) => { +export const urlGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.URL + const construct = (args: Array, into: ProgramObject, node: AstNode): ProgramURL => { if (args.length === 0) { - throw new InterpreterRuntimeError(`URL.${name} requires a URL argument.`, node) + throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node) } - const input = urlArgument(args[0], `URL.${name} input`) - const base = args[1] === undefined ? undefined : urlArgument(args[1], `URL.${name} base`) + const input = urlArgument(protos, args[0], "new URL input") + const base = args[1] === undefined ? undefined : urlArgument(protos, args[1], "new URL base") try { - const url = new URL(input, base) - return name === "canParse" ? true : new Values.URL(url) + return new ProgramURL(into, protos.URLSearchParams, new URL(input, base)) } catch { - return name === "canParse" ? false : null + throw new InterpreterRuntimeError( + `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, + node, + ) } + } + const url = constructor(protos, proto, { + name: "URL", + length: 1, + call: requiresNew("URL"), + construct: (args, newTarget, node) => Effect.sync(() => construct(args, prototypeFrom(newTarget, proto), node)), }) + const parse = (name: "canParse" | "parse"): Method => [ + name, + 1, + (_, args, node) => { + if (args.length === 0) throw new InterpreterRuntimeError(`URL.${name} requires a URL argument.`, node) + const input = urlArgument(protos, args[0], `URL.${name} input`) + const base = args[1] === undefined ? undefined : urlArgument(protos, args[1], `URL.${name} base`) + try { + const parsed = new URL(input, base) + return name === "canParse" ? true : new ProgramURL(proto, protos.URLSearchParams, parsed) + } catch { + return name === "canParse" ? false : null + } + }, + ] + methods(protos, url, [parse("canParse"), parse("parse")]) -const constructURL = (args: Array, node: AstNode): Values.URL => { - if (args.length === 0) { - throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node) - } - const input = urlArgument(args[0], "new URL input") - const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") - try { - return new Values.URL(new URL(input, base)) - } catch { - throw new InterpreterRuntimeError( - `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, - node, + const self = (thisValue: unknown, name: string, node?: AstNode) => + receiver(ProgramURL, thisValue, `URL.prototype.${name}`, node) + for (const name of urlProperties) { + defineAccessor( + proto, + name, + (thisValue) => self(thisValue, name).url[name], + name === "origin" + ? undefined + : (thisValue, value) => { + const target = self(thisValue, name) + try { + ;(target.url as unknown as Record)[name] = uriArgument(protos, value, `URL.${name} value`) + } catch (error) { + if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error + throw new InterpreterRuntimeError(`URL.${name} received an invalid value.`) + } + }, ) } + defineAccessor(proto, "searchParams", (thisValue) => self(thisValue, "searchParams").searchParams) + methods(protos, proto, [ + ["toString", 0, (thisValue, _, node) => self(thisValue, "toString", node).url.href], + ["toJSON", 0, (thisValue, _, node) => self(thisValue, "toJSON", node).url.href], + ]) + return url } -export const urlGlobal = new HostFunction({ - name: "URL", - call: requiresNew("URL"), - construct: syncCall(constructURL), - instanceOf: (value) => value instanceof Values.URL, - members: { canParse: urlStatic("canParse"), parse: urlStatic("parse") }, -}) - -const readURLSearchParamsPair = ( - runner: Runner, - value: unknown, - node: AstNode, -): Effect.Effect, unknown, R> => +const readPair = (runner: Runner, value: unknown, node: AstNode): Effect.Effect, unknown, R> => Effect.gen(function* () { const cursor = yield* runner.syncIterator(value, node) if (cursor === undefined) { @@ -134,7 +140,7 @@ const readURLSearchParamsPair = ( items.push( yield* preserveConsumerError( cursor, - Effect.sync(() => uriArgument(step.value, "URLSearchParams pair value")), + Effect.sync(() => uriArgument(runner.prototypes, step.value, "URLSearchParams pair value")), ), ) } @@ -143,31 +149,29 @@ const readURLSearchParamsPair = ( const constructURLSearchParams = ( runner: Runner, init: unknown, + proto: ProgramObject, node: AstNode, -): Effect.Effect => { - if (init === undefined) return Effect.succeed(new Values.URLSearchParams(new URLSearchParams())) - if (init instanceof Values.URLSearchParams) { - return Effect.succeed(new Values.URLSearchParams(new URLSearchParams(init.params))) - } - if (typeof init === "string") return Effect.succeed(new Values.URLSearchParams(new URLSearchParams(init))) +): Effect.Effect => { + const wrap = (params: URLSearchParams) => new ProgramURLSearchParams(proto, params) + if (init === undefined) return Effect.succeed(wrap(new URLSearchParams())) + if (init instanceof ProgramURLSearchParams) 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(new Values.URLSearchParams(new URLSearchParams(coerceToString(init)))) + return Effect.succeed(wrap(new URLSearchParams(coerceToString(init)))) } return Effect.gen(function* () { const cursor = yield* runner.syncIterator(init, node) if (cursor !== undefined) { - const entries: Array> = [] + const pairs: Array> = [] while (true) { const step = yield* cursor.next if (step.done) { - if (entries.some((entry) => entry.length !== 2)) { + if (pairs.some((entry) => entry.length !== 2)) { throw new InterpreterRuntimeError("new URLSearchParams(...) expects iterable [name, value] pairs.", node) } - return new Values.URLSearchParams( - new URLSearchParams(entries.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])), - ) + return wrap(new URLSearchParams(pairs.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""]))) } - entries.push(yield* preserveConsumerError(cursor, readURLSearchParamsPair(runner, step.value, node))) + pairs.push(yield* preserveConsumerError(cursor, readPair(runner, step.value, node))) } } if (isRuntimeReference(init)) { @@ -176,28 +180,129 @@ const constructURLSearchParams = ( node, ) } - if (Values.isValue(init)) return new Values.URLSearchParams(new URLSearchParams()) + if (isWrapper(init)) return wrap(new URLSearchParams()) if (!(init instanceof ProgramObject)) { throw new InterpreterRuntimeError( "new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.", node, ) } - return new Values.URLSearchParams( - new URLSearchParams(Object.fromEntries(ownEntries(init).map(([key, value]) => [key, coerceToString(value)]))), + return wrap( + new URLSearchParams(Object.fromEntries(entries(init).map(([key, value]) => [key, coerceToString(value)]))), ) }) } -export const urlSearchParamsGlobal = (runner: Runner) => - new HostFunction({ +export const urlSearchParamsGlobal = (runner: Runner) => { + const protos = runner.prototypes + const proto = protos.URLSearchParams + const searchParams = constructor(protos, proto, { name: "URLSearchParams", call: requiresNew("URLSearchParams"), - construct: (args, node) => constructURLSearchParams(runner, args[0], node), - instanceOf: (value) => value instanceof Values.URLSearchParams, + construct: (args, newTarget, node) => + constructURLSearchParams(runner, args[0], prototypeFrom(newTarget, proto), node), }) - -export const invokeURLMethod = (value: Values.URL, name: string, node: AstNode): string => { - if (name === "toString" || name === "toJSON") return value.url.href - throw new InterpreterRuntimeError(`URL method '${name}' is not available.`, node) + const self = (thisValue: unknown, name: string, node?: AstNode) => + receiver(ProgramURLSearchParams, thisValue, `URLSearchParams.prototype.${name}`, node) + const wrap = (items: Array) => new ProgramArray(protos.Array, items) + const arg = (name: string, args: Array, index: number): string => + uriArgument(protos, args[index], `URLSearchParams.${name} argument ${index + 1}`) + const requireArgs = (name: string, args: Array, count: number, node: AstNode): void => { + if (args.length < count) { + throw new InterpreterRuntimeError( + `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`, + node, + ) + } + } + defineAccessor(proto, "size", (thisValue) => self(thisValue, "size").params.size) + methods(protos, proto, [ + [ + "append", + 2, + (thisValue, args, node) => { + requireArgs("append", args, 2, node) + self(thisValue, "append", node).params.append(arg("append", args, 0), arg("append", args, 1)) + return undefined + }, + ], + [ + "delete", + 1, + (thisValue, args, node) => { + requireArgs("delete", args, 1, node) + const params = self(thisValue, "delete", node).params + if (args[1] !== undefined) params.delete(arg("delete", args, 0), arg("delete", args, 1)) + else params.delete(arg("delete", args, 0)) + return undefined + }, + ], + [ + "get", + 1, + (thisValue, args, node) => { + requireArgs("get", args, 1, node) + return self(thisValue, "get", node).params.get(arg("get", args, 0)) + }, + ], + [ + "getAll", + 1, + (thisValue, args, node) => { + requireArgs("getAll", args, 1, node) + return wrap(self(thisValue, "getAll", node).params.getAll(arg("getAll", args, 0))) + }, + ], + [ + "has", + 1, + (thisValue, args, node) => { + requireArgs("has", args, 1, node) + const params = self(thisValue, "has", node).params + return args[1] !== undefined + ? params.has(arg("has", args, 0), arg("has", args, 1)) + : params.has(arg("has", args, 0)) + }, + ], + [ + "set", + 2, + (thisValue, args, node) => { + requireArgs("set", args, 2, node) + self(thisValue, "set", node).params.set(arg("set", args, 0), arg("set", args, 1)) + return undefined + }, + ], + [ + "sort", + 0, + (thisValue, _, node) => { + self(thisValue, "sort", node).params.sort() + return undefined + }, + ], + ["keys", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "keys", node).params.keys()))], + ["values", 0, (thisValue, _, node) => wrap(Array.from(self(thisValue, "values", node).params.values()))], + [ + "entries", + 0, + (thisValue, _, node) => + wrap(Array.from(self(thisValue, "entries", node).params.entries(), ([key, value]) => wrap([key, value]))), + ], + ["toString", 0, (thisValue, _, node) => self(thisValue, "toString", node).params.toString()], + [ + "forEach", + 1, + (thisValue, args, node) => { + requireArgs("forEach", args, 1, node) + const target = self(thisValue, "forEach", node) + const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach", node) + return Effect.gen(function* () { + for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target]) + return undefined + }) + }, + ], + ]) + return searchParams } diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index 57955edb44de..236cb0b7e7d6 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -1,21 +1,34 @@ -import { type HostFunction, sync, type SyncOptions } from "../interpreter/host.js" -import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { toProgram } from "../data.js" -import { get, ProgramArray, ProgramError } from "../interpreter/objects.js" -import { Values } from "../values.js" +import { fn } from "../interpreter/native.js" +import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" +import { + get, + isWrapper, + type NativeFunction, + ProgramArray, + ProgramDate, + ProgramError, + ProgramMap, + ProgramRegExp, + ProgramSet, + ProgramURL, + ProgramURLSearchParams, +} from "../interpreter/objects.js" +import type { Runner } from "../interpreter/runner.js" export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="]) +/** The built-in string form of a value, without consulting program-defined `toString` methods. */ export const coerceToString = (value: unknown): string => { if (value === null) return "null" if (value === undefined) return "undefined" - if (value instanceof Values.Date) + if (value instanceof ProgramDate) return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date" - if (value instanceof Values.RegExp) return `/${value.regex.source}/${value.regex.flags}` - if (value instanceof Values.Map) return "[object Map]" - if (value instanceof Values.Set) return "[object Set]" - if (value instanceof Values.URL) return value.url.href - if (value instanceof Values.URLSearchParams) return value.params.toString() + 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) { // Match Error.prototype.toString: "name: message", or just one when the other is empty. const name = get(value, "name") @@ -34,15 +47,15 @@ export const coerceToString = (value: unknown): string => { } export const coerceToNumber = (value: unknown): number => { - if (value instanceof Values.Date) return value.time - if (Values.isValue(value)) return Number.NaN + if (value instanceof ProgramDate) return value.time + if (isWrapper(value)) return Number.NaN if (value instanceof ProgramArray) return Number(coerceToString(value)) return value !== null && typeof value === "object" ? Number.NaN : Number(value) } -type Coercion = "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN" +export type Coercion = "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN" -const coerce = (name: Coercion, args: Array, node: AstNode): unknown => { +const coerce = (runner: Runner, name: Coercion, args: Array, node: AstNode): 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) { @@ -50,7 +63,7 @@ const coerce = (name: Coercion, args: Array, node: AstNode): unknown => if (name === "String") return "" } const raw = args[0] - if (Values.isValue(raw)) { + if (isWrapper(raw)) { if (name === "Boolean") return true if (name === "Number") return coerceToNumber(raw) if (name === "String") return coerceToString(raw) @@ -59,7 +72,7 @@ const coerce = (name: Coercion, args: Array, node: AstNode): unknown => if (name === "parseInt") return parseInt(coerceToString(raw)) return parseFloat(coerceToString(raw)) } - const value = toProgram(raw, `${name} input`) + const value = toProgram(runner.prototypes, raw, `${name} input`) if (name === "Number") return coerceToNumber(value) if (name === "Boolean") return Boolean(value) if (name === "isFinite") return Number.isFinite(coerceToNumber(value)) @@ -76,5 +89,7 @@ const coerce = (name: Coercion, args: Array, node: AstNode): unknown => } /** A global coercion function such as `Number` or `parseInt`. */ -export const coercion = (name: Coercion, options: SyncOptions = {}): HostFunction => - sync(name, (args, node) => toProgram(coerce(name, args, node), `${name} result`), options) +export const coercion = (runner: Runner, name: Coercion, length = 1): NativeFunction => + fn(runner.prototypes, name, length, (_, args, node) => + toProgram(runner.prototypes, coerce(runner, name, args, node), `${name} result`), + ) diff --git a/packages/codemode/src/stdlib/web.ts b/packages/codemode/src/stdlib/web.ts index 0e2046182711..3d1e5bc55f6d 100644 --- a/packages/codemode/src/stdlib/web.ts +++ b/packages/codemode/src/stdlib/web.ts @@ -1,11 +1,13 @@ -import { HostNamespace, sync } from "../interpreter/host.js" +import { fn, methods } from "../interpreter/native.js" import { InterpreterRuntimeError } from "../interpreter/model.js" +import { ProgramObject } from "../interpreter/objects.js" +import type { Runner } from "../interpreter/runner.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. -const base64 = (name: "atob" | "btoa") => - sync(name, (args, node) => { +export const base64Global = (runner: Runner, name: "atob" | "btoa") => + fn(runner.prototypes, name, 1, (_, args, node) => { if (args.length === 0) throw new InterpreterRuntimeError(`${name} requires 1 argument (a string)`, node) const input = coerceToString(args[0]) try { @@ -15,9 +17,8 @@ const base64 = (name: "atob" | "btoa") => } }) -export const atobGlobal = base64("atob") -export const btoaGlobal = base64("btoa") - -export const cryptoGlobal = new HostNamespace("crypto", { - randomUUID: sync("crypto.randomUUID", () => crypto.randomUUID()), -}) +export const cryptoGlobal = (runner: Runner) => { + const object = new ProgramObject(runner.prototypes.Object) + methods(runner.prototypes, object, [["randomUUID", 0, () => crypto.randomUUID()]]) + return object +} diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f14be3dd2f36..63d576251a55 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -1,5 +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 { toolError } from "./tool-error.js" import { decodeInput as decodeToolInput, @@ -308,6 +309,7 @@ export type ToolRuntime = { /** Per-execution call state over tools prepared once for the runtime. */ export const make = ( prepared: Prepared, + prototypes: Prototypes, maxToolCalls: number | undefined, hooks?: ToolCallHooks, ): ToolRuntime => { @@ -373,7 +375,7 @@ export const make = ( }), ) return yield* Effect.try({ - try: () => fromData(decodeToolOutput(tool, raw), `Result from tool '${name}'`), + try: () => fromData(prototypes, decodeToolOutput(tool, raw), `Result from tool '${name}'`), catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`), }) }), @@ -386,7 +388,11 @@ export const make = ( keys: (path) => namespaceKeys(root, path), search: (args) => Effect.suspend(() => - executeTool("search", searchTool, args.map((arg) => toData(arg, "Arguments for tool 'search'"))), + executeTool( + "search", + searchTool, + args.map((arg) => toData(arg, "Arguments for tool 'search'")), + ), ), execute: (path, args) => Effect.gen(function* () { diff --git a/packages/codemode/src/values.ts b/packages/codemode/src/values.ts deleted file mode 100644 index c334208b82e3..000000000000 --- a/packages/codemode/src/values.ts +++ /dev/null @@ -1,59 +0,0 @@ -export * as Values from "./values.js" - -import type { Fiber } from "effect" - -/** - * Runtime values the interpreter recognizes by class. Each wraps the host value it stands for, - * so hosts construct these to hand a value to a program and receive them back unchanged. - */ - -export class Promise { - constructor(readonly fiber: Fiber.Fiber) {} -} - -export class Date { - constructor(public time: number) {} -} - -export class RegExp { - readonly regex: globalThis.RegExp - constructor(pattern: string, flags: string) { - this.regex = new globalThis.RegExp(pattern, flags) - } - - get lastIndex(): unknown { - return Reflect.get(this.regex, "lastIndex") - } - - set lastIndex(value: unknown) { - Reflect.set(this.regex, "lastIndex", value) - } -} - -export class Map { - readonly map = new globalThis.Map() -} - -export class Set { - readonly set = new globalThis.Set() -} - -export class URLSearchParams { - constructor(readonly params: globalThis.URLSearchParams) {} -} - -export class URL { - readonly searchParams: URLSearchParams - constructor(readonly url: globalThis.URL) { - this.searchParams = new URLSearchParams(url.searchParams) - } -} - -/** Data-like runtime values; excludes Promise, which never crosses a boundary. */ -export const isValue = (value: unknown): value is Date | RegExp | Map | Set | URL | URLSearchParams => - value instanceof Date || - value instanceof RegExp || - value instanceof Map || - value instanceof Set || - value instanceof URL || - value instanceof URLSearchParams diff --git a/packages/codemode/test/callbacks.test.ts b/packages/codemode/test/callbacks.test.ts index f257b732c0ca..00e4f5a9aca6 100644 --- a/packages/codemode/test/callbacks.test.ts +++ b/packages/codemode/test/callbacks.test.ts @@ -63,8 +63,10 @@ describe("built-in method references as callbacks", () => { expect(logs[1]).toContain("b") }) - test("intrinsic method references keep their receiver, unlike detached JS methods", async () => { - expect(await value(`return ["a", "z"].filter("abc".includes)`)).toEqual(["a"]) + test("detached method references lose their receiver, like JS", async () => { + expect((await error(`return ["a", "z"].filter("abc".includes)`)).message).toContain( + "String.prototype.includes called on null or undefined", + ) }) test("promise reactions accept built-in references", async () => { diff --git a/packages/codemode/test/enumeration.test.ts b/packages/codemode/test/enumeration.test.ts index 20608d34c6bb..b9daf647c059 100644 --- a/packages/codemode/test/enumeration.test.ts +++ b/packages/codemode/test/enumeration.test.ts @@ -96,7 +96,7 @@ describe("Object.keys over arrays", () => { expect((await error(`return Object.keys(tools.github.list_issues({ value: "x" }))`)).message).toContain( "received an un-awaited Promise", ) - expect((await error(`const { a } = new Map(); return a`)).message).toContain("received a Map.") + expect(await value(`const { a, size } = new Map(); return [a, size]`)).toEqual([null, 0]) expect((await error(`return Array.from(7)`)).message).toContain("received a number.") expect(await value(`return [(() => 1).x, Object.keys(() => 1)]`)).toEqual([null, []]) }) diff --git a/packages/codemode/test/generator-test262.test.ts b/packages/codemode/test/generator-test262.test.ts index 70528f51fcd3..523e73714a3a 100644 --- a/packages/codemode/test/generator-test262.test.ts +++ b/packages/codemode/test/generator-test262.test.ts @@ -1040,7 +1040,7 @@ describe("confined generators", () => { function* pair() { try { events.push("first") - yield (function* () {})() + yield Promise.resolve(1) events.push("second") yield 2 } finally { events.push("pair close") } diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 9a8431b551a3..0fca774185fa 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -242,7 +242,9 @@ describe("property deletion", () => { }) test("array length is not configurable", async () => { - expect(await value(`const values = [1, 2]; return [delete values.length, values.length]`)).toEqual([false, 2]) + expect((await error(`const values = [1, 2]; delete values.length`)).message).toContain( + "Cannot delete property 'length'", + ) }) test("arrays accept named properties like JS, and they stay out of the JSON form", async () => { @@ -420,7 +422,7 @@ describe("Error values and instanceof", () => { Object.keys(new RangeError("r")), ] `), - ).toEqual([true, true, false, true, true, true, ["message"]]) + ).toEqual([true, true, false, true, true, true, []]) }) test("Promise.allSettled rejection reasons are Error values", async () => { @@ -445,19 +447,18 @@ describe("Error values and instanceof", () => { ]) }) - test("errors serialize as { name, message } by brand; name is inherited and message is own", async () => { + test("errors serialize as { name, message } by brand; neither is enumerable", async () => { expect(await value(`return new Error("m")`)).toEqual({ name: "Error", message: "m" }) expect(await value(`return JSON.stringify(new Error("m"))`)).toBe('{"name":"Error","message":"m"}') - expect(await value(`try { throw new Error("m") } catch (e) { return [Object.keys(e), e.name] }`)).toEqual([ - ["message"], - "Error", - ]) - expect(await value(`return Object.keys(new Error())`)).toEqual([]) + expect( + await value(`try { throw new Error("m") } catch (e) { return [Object.keys(e), e.name, e.hasOwnProperty("message")] }`), + ).toEqual([[], "Error", true]) + expect(await value(`return new Error().hasOwnProperty("message")`)).toBe(false) }) test("spreading an error loses the brand, like losing the prototype in JS", async () => { expect(await value(`const e = new Error("m"); return ({ ...e }) instanceof Error`)).toBe(false) - expect(await value(`const e = new Error("m"); return { ...e }`)).toEqual({ message: "m" }) + expect(await value(`const e = new Error("m"); return { ...e }`)).toEqual({}) }) test("typeof Error is function; an unknown instanceof right-hand side is a catchable error", async () => { @@ -692,13 +693,14 @@ describe("destructuring assignment", () => { ).toEqual({ calls: 1, name: "Ada", rest: { role: "engineer" } }) }) - test("supports object patterns over arrays", async () => { + test("supports object patterns over arrays; detached methods lose their receiver like JS", async () => { expect( await value(` const { 0: first, length, slice, ...rest } = ["a", "b", "c"] - return { first, length, sliced: slice(1), rest } + return { first, length, sliced: slice === Array.prototype.slice, rest } `), - ).toEqual({ first: "a", length: 3, sliced: ["b", "c"], rest: { 1: "b", 2: "c" } }) + ).toEqual({ first: "a", length: 3, sliced: true, rest: { 1: "b", 2: "c" } }) + expect((await error(`const { slice } = [1]; slice(0)`)).message).toContain("Array.prototype.slice called on") }) test("preserves exact computed property names on arrays", async () => { @@ -932,13 +934,22 @@ describe("coercion parity: unknown static members read as undefined", () => { ) }) - test("prototype-named members on globals read as undefined like other unknown statics", async () => { - expect(await value(`return [Math.constructor, Number.constructor, Object.prototype, Array.__proto__]`)).toEqual([ - null, - null, - null, - null, - ]) + test("built-ins are objects on a real prototype chain", async () => { + expect( + await value(` + return [ + Math.constructor === Object, + Number.constructor === Function, + Object.prototype.hasOwnProperty === ({}).hasOwnProperty, + Array.prototype.push.name, + Array.prototype.push.length, + Math.max.length, + Object.keys(Math), + typeof Array.prototype.map, + Array.isArray(Array.prototype), + ] + `), + ).toEqual([true, true, true, "push", 1, 2, [], "function", true]) }) }) diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts index 63d8540e900e..14f3e504a064 100644 --- a/packages/codemode/test/promise.test.ts +++ b/packages/codemode/test/promise.test.ts @@ -1136,10 +1136,9 @@ describe("unsupported promise surface", () => { expect(diagnostic.message).toContain("await it first") }) - test("unknown Promise statics list what is available", async () => { + test("unknown Promise statics are not functions", async () => { const diagnostic = await error(`return await Promise.withResolvers()`) - expect(diagnostic.message).toContain("Promise.withResolvers is not available") - expect(diagnostic.message).toContain("Promise.any") + expect(diagnostic.message).toContain("Promise.withResolvers is not a function") }) }) diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index 969e3e38f2ff..390b66aef75f 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -245,9 +245,10 @@ describe("RegExp", () => { const stored = [pattern.lastIndex, typeof pattern.lastIndex] const match = pattern.exec("aacd2233ab12nm444ab42") pattern.lastIndex = 0 - return [stored, match[0], match.index, pattern.lastIndex, delete pattern.lastIndex] + return [stored, match[0], match.index, pattern.lastIndex] `), - ).toEqual([["12", "string"], "ab4", 17, 0, false]) + ).toEqual([["12", "string"], "ab4", 17, 0]) + expect((await error(`delete /a/.lastIndex`)).message).toContain("Cannot delete property 'lastIndex'") }) test("exec coerces CodeMode data objects assigned to lastIndex", async () => { @@ -728,7 +729,7 @@ describe("stdlib integration", () => { expect(await value(`const o = {}; o.constructor = 7; return o.constructor`)).toBe(7) expect(await value(`return new ([].constructor)(3).length`)).toBe(3) expect(await value(`return typeof ({}).constructor`)).toBe("function") - expect(await value(`return ({}).constructor.constructor`)).toBeNull() + expect(await value(`return ({}).constructor.constructor === Function`)).toBe(true) }) test("new dispatches on the constructor value, not its name", async () => { diff --git a/packages/codemode/test/test262/manifest.json b/packages/codemode/test/test262/manifest.json index c04936291461..5a2ae1055eab 100644 --- a/packages/codemode/test/test262/manifest.json +++ b/packages/codemode/test/test262/manifest.json @@ -8,7 +8,6 @@ "this": "\\bthis\\b", "arguments": "\\barguments\\b", "call/apply/bind": "\\.(call|apply|bind)\\s*\\(", - "prototype objects": "\\.prototype\\b", "accessor properties": "\\b(get|set)\\s+[\\w$\\[][^\\n(]*\\(", "property descriptors": "Object\\.(defineProperty|defineProperties|getOwnPropertyDescriptors?|getOwnPropertyNames|create|getPrototypeOf|setPrototypeOf|freeze|seal|preventExtensions|isFrozen|isSealed|isExtensible)\\b", "boxed primitives": "\\bnew\\s+(String|Number|Boolean)\\s*\\(", diff --git a/packages/codemode/test/test262/run.ts b/packages/codemode/test/test262/run.ts index 04b35cbc428f..72a1ac0edb54 100644 --- a/packages/codemode/test/test262/run.ts +++ b/packages/codemode/test/test262/run.ts @@ -6,10 +6,18 @@ import { Cause, Effect } from "effect" import { caughtErrorValue } from "../../src/interpreter/errors.js" import { executeProgram } from "../../src/interpreter/execute.js" import type { Host } from "../../src/interpreter/globals.js" -import { HostFunction } from "../../src/interpreter/host.js" import { ProgramThrow } from "../../src/interpreter/model.js" import { createErrorValue } from "../../src/interpreter/intrinsics.js" -import { get, hasPrototype, ProgramArray, ProgramObject, set } from "../../src/interpreter/objects.js" +import { constructor, fn, methods } from "../../src/interpreter/native.js" +import { + Callable, + define, + get, + hidden, + ProgramArray, + ProgramFunction, + ProgramObject, +} from "../../src/interpreter/objects.js" import { ToolRuntime } from "../../src/tool-runtime.js" export const root = import.meta.dir @@ -65,8 +73,9 @@ export const run = async (file: string): Promise => { } const harness = (host: Host, onDone: (error: unknown) => void): ReadonlyArray => { - const test262Prototype = new ProgramObject() - set(test262Prototype, "name", "Test262Error") + const protos = host.runner.prototypes + const test262Prototype = new ProgramObject(protos.Object) + define(test262Prototype, "name", "Test262Error", hidden) const test262 = (args: Array) => createErrorValue(test262Prototype, args[0] === undefined ? "" : String(args[0])) const fail = (message: string) => Effect.fail(new ProgramThrow(test262([message]))) @@ -76,84 +85,78 @@ const harness = (host: Host, onDone: (error: unknown) => void): ReadonlyAr b instanceof ProgramArray && a.items.length === b.items.length && a.items.every((value, i) => Object.is(value, b.items[i])) - const test262Error = new HostFunction({ + const test262Error = constructor(protos, test262Prototype, { name: "Test262Error", - call: (args) => Effect.succeed(test262(args)), + call: (_, args) => Effect.succeed(test262(args)), construct: (args) => Effect.succeed(test262(args)), - instanceOf: (value) => hasPrototype(value, test262Prototype), - members: { thrower: new HostFunction({ name: "Test262Error.thrower", call: (args) => fail(String(args[0])) }) }, - }) - set(test262Prototype, "constructor", test262Error) - const compareArray = new HostFunction({ - name: "compareArray", - call: (args) => Effect.succeed(compare(args[0], args[1])), - members: { - format: new HostFunction({ name: "compareArray.format", call: (args) => Effect.succeed(show(args[0])) }), - }, - }) - const assert = new HostFunction({ - name: "assert", - call: (args) => - args[0] === true - ? Effect.void - : fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])), - members: { - sameValue: new HostFunction({ - name: "assert.sameValue", - call: (args) => - Object.is(args[0], args[1]) - ? Effect.void - : fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`), - }), - notSameValue: new HostFunction({ - name: "assert.notSameValue", - call: (args) => - Object.is(args[0], args[1]) - ? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`) - : Effect.void, - }), - compareArray: new HostFunction({ - name: "assert.compareArray", - call: (args) => - compare(args[0], args[1]) - ? Effect.void - : fail( - `Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`, - ), - }), - throws: new HostFunction({ - name: "assert.throws", - call: (args, node) => { - const expected = args[0] instanceof HostFunction ? args[0].name : show(args[0]) - return host.runner.invokeCallable(args[1], [], node).pipe( - Effect.matchCauseEffect({ - onFailure: (cause) => { - if (cause.reasons.some(Cause.isInterruptReason)) return Effect.failCause(cause) - const thrown = caughtErrorValue(host.runner, Cause.squash(cause)) - if (!(thrown instanceof ProgramObject)) 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)}`) - }, - onSuccess: () => - fail(`${prefix(args[2])}Expected a ${expected} to be thrown but no exception was thrown at all`), - }), - ) - }, - }), - }, }) + methods(protos, test262Error, [["thrower", 1, (_, args) => fail(String(args[0]))]]) + const compareArray = fn(protos, "compareArray", 2, (_, args) => compare(args[0], args[1])) + methods(protos, compareArray, [["format", 1, (_, args) => show(args[0])]]) + const assert = fn(protos, "assert", 2, (_, args) => + args[0] === true + ? Effect.void + : fail(args[1] === undefined ? `Expected true but got ${show(args[0])}` : String(args[1])), + ) + methods(protos, assert, [ + [ + "sameValue", + 3, + (_, args) => + Object.is(args[0], args[1]) + ? Effect.void + : fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be true`), + ], + [ + "notSameValue", + 3, + (_, args) => + Object.is(args[0], args[1]) + ? fail(`${prefix(args[2])}Expected SameValue(«${show(args[0])}», «${show(args[1])}») to be false`) + : Effect.void, + ], + [ + "compareArray", + 3, + (_, args) => + compare(args[0], args[1]) + ? Effect.void + : fail( + `Actual ${show(args[0])} and expected ${show(args[1])} should have the same contents. ${prefix(args[2])}`, + ), + ], + [ + "throws", + 3, + (_, args, node) => { + const expected = args[0] instanceof Callable ? String(get(args[0], "name")) : show(args[0]) + return host.runner.invokeCallable(args[1], undefined, [], node).pipe( + Effect.matchCauseEffect({ + onFailure: (cause) => { + if (cause.reasons.some(Cause.isInterruptReason)) return Effect.failCause(cause) + const thrown = caughtErrorValue(host.runner, Cause.squash(cause)) + if (!(thrown instanceof ProgramObject)) 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)}`) + }, + onSuccess: () => + fail(`${prefix(args[2])}Expected a ${expected} to be thrown but no exception was thrown at all`), + }), + ) + }, + ], + ]) return [ ["assert", assert], ["compareArray", compareArray], ["Test262Error", test262Error], - ["$DONE", new HostFunction({ name: "$DONE", call: (args) => Effect.sync(() => onDone(args[0])) })], + ["$DONE", fn(protos, "$DONE", 1, (_, args) => onDone(args[0]))], [ "$DONOTEVALUATE", - new HostFunction({ - name: "$DONOTEVALUATE", - call: () => Effect.fail(new ProgramThrow("Test262: This statement should not be evaluated.")), - }), + fn(protos, "$DONOTEVALUATE", 0, () => + Effect.fail(new ProgramThrow("Test262: This statement should not be evaluated.")), + ), ], ] } @@ -162,7 +165,8 @@ 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 HostFunction) return value.name + if (value instanceof ProgramFunction) return "program function" + if (value instanceof Callable) return String(get(value, "name")) if (!(value instanceof ProgramObject)) return String(value) const message = get(value, "message") return typeof message === "string" ? `${String(get(value, "name") ?? "object")}: ${message}` : "object" diff --git a/packages/codemode/test/test262/skipped.txt b/packages/codemode/test/test262/skipped.txt index 26ef55b4f90c..180deb5e2311 100644 --- a/packages/codemode/test/test262/skipped.txt +++ b/packages/codemode/test/test262/skipped.txt @@ -1,23 +1,60 @@ +built-ins/Array/prototype/at/index-argument-tointeger.js # Array.at expects index to be a number. +built-ins/Array/prototype/at/index-non-numeric-argument-tointeger.js # Array.at expects index to be a number. +built-ins/Array/prototype/concat/S15.4.4.4_A2_T1.js # Array.prototype.concat called on incompatible receiver a data object. +built-ins/Array/prototype/concat/S15.4.4.4_A2_T2.js # Array.prototype.concat called on incompatible receiver a data object. +built-ins/Array/prototype/concat/S15.4.4.4_A3_T1.js # arr.hasOwnProperty("1") must return true Expected SameValue(«false», «true») to be true +built-ins/Array/prototype/concat/S15.4.4.4_A3_T2.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true +built-ins/Array/prototype/concat/S15.4.4.4_A3_T3.js # b.hasOwnProperty("2") must return true Expected SameValue(«false», «true») to be true built-ins/Array/prototype/concat/create-ctor-non-object.js # a.concat() throws a TypeError exception Expected a TypeError to be thrown but no exception was throw built-ins/Array/prototype/copyWithin/coerced-values-end.js # Array.copyWithin expects end to be a number. built-ins/Array/prototype/copyWithin/coerced-values-start-change-target.js # Array.copyWithin expects start to be a number. built-ins/Array/prototype/copyWithin/coerced-values-start.js # Array.copyWithin expects start to be a number. built-ins/Array/prototype/copyWithin/coerced-values-target.js # Array.copyWithin expects target index to be a number. -built-ins/Array/prototype/copyWithin/fill-holes.js # .hasOwnProperty is not a function. built-ins/Array/prototype/copyWithin/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a TypeError built-ins/Array/prototype/entries/iteration-mutable.js # .next is not a function. built-ins/Array/prototype/entries/iteration.js # .next is not a function. -built-ins/Array/prototype/every/15.4.4.16-7-7.js # .hasOwnProperty is not a function. +built-ins/Array/prototype/every/15.4.4.16-7-6.js # res Expected SameValue(«true», «false») to be true +built-ins/Array/prototype/every/15.4.4.16-7-c-i-8.js # [, , , ].every(callbackfn) Expected SameValue(«true», «false») to be true +built-ins/Array/prototype/every/15.4.4.16-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/every/15.4.4.16-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. +built-ins/Array/prototype/every/15.4.4.16-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/every/15.4.4.16-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/fill/coerced-indexes.js # Array.fill expects start to be a number. built-ins/Array/prototype/fill/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError built-ins/Array/prototype/fill/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError +built-ins/Array/prototype/filter/15.4.4.20-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/filter/15.4.4.20-10-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. +built-ins/Array/prototype/filter/15.4.4.20-6-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-6-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/filter/15.4.4.20-9-6.js # resArr.length Expected SameValue(«3», «4») to be true +built-ins/Array/prototype/filter/15.4.4.20-9-c-i-8.js # newArr.length Expected SameValue(«0», «1») to be true built-ins/Array/prototype/filter/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all built-ins/Array/prototype/flat/non-numeric-depth-should-not-throw.js # Array.flat expects depth to be a number. +built-ins/Array/prototype/flat/non-object-ctor-throws.js # null value Expected a TypeError to be thrown but no exception was thrown at all +built-ins/Array/prototype/forEach/15.4.4.18-7-5.js # callCnt Expected SameValue(«4», «5») to be true +built-ins/Array/prototype/forEach/15.4.4.18-7-c-i-8.js # testResult !== true +built-ins/Array/prototype/forEach/15.4.4.18-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/forEach/15.4.4.18-8-12.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. +built-ins/Array/prototype/forEach/15.4.4.18-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/forEach/15.4.4.18-8-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/includes/length-zero-returns-false.js # Array.includes expects a value and optional start index. built-ins/Array/prototype/includes/no-arg.js # Array.includes expects a value and optional start index. built-ins/Array/prototype/includes/return-abrupt-tointeger-fromindex.js # Expected a Test262Error but got a TypeError @@ -33,17 +70,22 @@ built-ins/Array/prototype/indexOf/15.4.4.14-5-21.js # Array.indexOf expects sta built-ins/Array/prototype/indexOf/15.4.4.14-5-22.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/indexOf/15.4.4.14-5-23.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/indexOf/15.4.4.14-5-24.js # toStringAccessed +built-ins/Array/prototype/indexOf/15.4.4.14-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/indexOf/15.4.4.14-5-3.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/indexOf/15.4.4.14-5-5.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/indexOf/15.4.4.14-9-7.js # Array assignment result contains a circular value. -built-ins/Array/prototype/indexOf/15.4.4.14-9-8.js # .toString is not a function. built-ins/Array/prototype/indexOf/15.4.4.14-9-a-3.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/indexOf/15.4.4.14-9-a-6.js # Array.indexOf expects start index to be a number. +built-ins/Array/prototype/indexOf/15.4.4.14-9-b-i-7.js # [, , , ].indexOf(true) Expected SameValue(«-1», «0») to be true built-ins/Array/prototype/indexOf/length-zero-returns-minus-one.js # Array.indexOf expects start index to be a number. built-ins/Array/prototype/join/S15.4.4.5_A1.2_T2.js # Array.join expects zero arguments or one string separator. +built-ins/Array/prototype/join/S15.4.4.5_A2_T1.js # Array.prototype.join called on incompatible receiver a data object. +built-ins/Array/prototype/join/S15.4.4.5_A2_T4.js # Array.prototype.join called on incompatible receiver a data object. built-ins/Array/prototype/join/S15.4.4.5_A3.1_T1.js # Array.join expects zero arguments or one string separator. built-ins/Array/prototype/join/S15.4.4.5_A3.1_T2.js # Array.join expects zero arguments or one string separator. built-ins/Array/prototype/join/S15.4.4.5_A3.2_T2.js # x.join() must return "*" Expected SameValue(«"[object Object]"», «"*"») to be true +built-ins/Array/prototype/join/S15.4.4.5_A4_T3.js # Array.prototype.join called on incompatible receiver a data object. +built-ins/Array/prototype/join/S15.4.4.5_A5_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.join() === "0,1". Actual: 0, built-ins/Array/prototype/keys/iteration-mutable.js # .next is not a function. built-ins/Array/prototype/keys/iteration.js # .next is not a function. built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js # Array.lastIndexOf expects start index to be a number. @@ -57,40 +99,155 @@ built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-21.js # Array.lastIndexOf exp built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-22.js # Array.lastIndexOf expects start index to be a number. built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-23.js # Array.lastIndexOf expects start index to be a number. built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-24.js # toStringAccessed +built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-25.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-3.js # Array.lastIndexOf expects start index to be a number. built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js # a.lastIndexOf(2,undefined) Expected SameValue(«1», «-1») to be true built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-5.js # Array.lastIndexOf expects start index to be a number. built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-7.js # Array assignment result contains a circular value. -built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-8.js # .toString is not a function. built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-3.js # Array.lastIndexOf expects start index to be a number. built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-6.js # Array.lastIndexOf expects start index to be a number. +built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-b-i-7.js # [, , , ].lastIndexOf(true) Expected SameValue(«-1», «0») to be true built-ins/Array/prototype/lastIndexOf/length-zero-returns-minus-one.js # Array.lastIndexOf expects start index to be a number. -built-ins/Array/prototype/map/15.4.4.19-8-7.js # .toString is not a function. +built-ins/Array/prototype/map/15.4.4.19-8-6.js # resArr[4] Expected SameValue(«undefined», «1») to be true +built-ins/Array/prototype/map/15.4.4.19-8-c-i-8.js # newArr[1] Expected SameValue(«13», «true») to be true +built-ins/Array/prototype/map/15.4.4.19-9-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/map/15.4.4.19-9-4.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. built-ins/Array/prototype/map/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all +built-ins/Array/prototype/pop/S15.4.4.6_A2_T1.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/pop/S15.4.4.6_A2_T4.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/pop/S15.4.4.6_A3_T1.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/pop/S15.4.4.6_A3_T2.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/pop/S15.4.4.6_A3_T3.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/pop/S15.4.4.6_A4_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.pop() === 1. Actual: undefined +built-ins/Array/prototype/pop/S15.4.4.6_A4_T2.js # Array.prototype.pop called on incompatible receiver a data object. +built-ins/Array/prototype/push/S15.4.4.7_A2_T1.js # Array.prototype.push called on incompatible receiver a data object. +built-ins/Array/prototype/push/S15.4.4.7_A2_T3.js # Array.prototype.push called on incompatible receiver a data object. built-ins/Array/prototype/push/S15.4.4.7_A3.js # The value of x[4294967295] is expected to be "x" Expected SameValue(«undefined», «"x"») to be true +built-ins/Array/prototype/push/S15.4.4.7_A4_T1.js # Array.prototype.push called on incompatible receiver a data object. +built-ins/Array/prototype/push/S15.4.4.7_A4_T2.js # Array.prototype.push called on incompatible receiver a data object. +built-ins/Array/prototype/push/S15.4.4.7_A4_T3.js # Array.prototype.push called on incompatible receiver a data object. +built-ins/Array/prototype/push/S15.4.4.7_A5_T1.js # Array.prototype.push called on incompatible receiver a data object. +built-ins/Array/prototype/reduce/15.4.4.21-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/reduce/15.4.4.21-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. -built-ins/Array/prototype/reduce/15.4.4.21-9-7.js # .hasOwnProperty is not a function. +built-ins/Array/prototype/reduce/15.4.4.21-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-5-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-7-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduce/15.4.4.21-8-b-iii-1-8.js # Array.reduce of an empty array with no initial value. +built-ins/Array/prototype/reduce/15.4.4.21-8-c-4.js # Array.reduce of an empty array with no initial value. +built-ins/Array/prototype/reduce/15.4.4.21-9-6.js # res Expected SameValue(«"123"», «"1235"») to be true +built-ins/Array/prototype/reduce/15.4.4.21-9-c-i-8.js # testResult !== true +built-ins/Array/prototype/reduceRight/15.4.4.22-10-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-10-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-10-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-10-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/reduceRight/15.4.4.22-10-8.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. -built-ins/Array/prototype/reduceRight/15.4.4.22-9-7.js # .hasOwnProperty is not a function. +built-ins/Array/prototype/reduceRight/15.4.4.22-5-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-5-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-7-9.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/reduceRight/15.4.4.22-8-b-iii-1-8.js # Array.reduceRight of an empty array with no initial value. +built-ins/Array/prototype/reduceRight/15.4.4.22-8-c-4.js # Array.reduceRight of an empty array with no initial value. +built-ins/Array/prototype/reduceRight/15.4.4.22-9-6.js # res Expected SameValue(«"91"», «"151"») to be true +built-ins/Array/prototype/reduceRight/15.4.4.22-9-c-i-8.js # testResult !== true +built-ins/Array/prototype/reverse/S15.4.4.8_A2_T1.js # Array.prototype.reverse called on incompatible receiver a data object. +built-ins/Array/prototype/reverse/S15.4.4.8_A3_T3.js # Array.prototype.reverse called on incompatible receiver a data object. +built-ins/Array/prototype/reverse/S15.4.4.8_A4_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.reverse(); x[0] === 1. Actual: undefined +built-ins/Array/prototype/reverse/S15.4.4.8_A4_T2.js # Array.prototype.reverse called on incompatible receiver a data object. +built-ins/Array/prototype/shift/S15.4.4.9_A2_T1.js # Array.prototype.shift called on incompatible receiver a data object. +built-ins/Array/prototype/shift/S15.4.4.9_A2_T4.js # Array.prototype.shift called on incompatible receiver a data object. +built-ins/Array/prototype/shift/S15.4.4.9_A2_T5.js # Array.prototype.shift called on incompatible receiver a data object. +built-ins/Array/prototype/shift/S15.4.4.9_A3_T3.js # Array.prototype.shift called on incompatible receiver a data object. +built-ins/Array/prototype/shift/S15.4.4.9_A4_T1.js # #2: Array.prototype[1] = 1; x = [0]; x.length = 2; x.shift(); x[0] === 1. Actual: undefined +built-ins/Array/prototype/shift/S15.4.4.9_A4_T2.js # Array.prototype.shift called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2.1_T5.js # Array.slice expects start to be a number. +built-ins/Array/prototype/slice/S15.4.4.10_A2.2_T5.js # Array.slice expects end to be a number. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T1.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T2.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T3.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T4.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T5.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A2_T6.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A3_T1.js # Expected a RangeError but got a TypeError +built-ins/Array/prototype/slice/S15.4.4.10_A3_T2.js # Expected a RangeError but got a TypeError +built-ins/Array/prototype/slice/S15.4.4.10_A3_T3.js # Array.prototype.slice called on incompatible receiver a data object. +built-ins/Array/prototype/slice/S15.4.4.10_A4_T1.js # #3: Array.prototype[1] = 1; x = [0]; x.length = 2; var arr = x.slice(); arr.hasOwnProperty('…') === built-ins/Array/prototype/slice/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all +built-ins/Array/prototype/some/15.4.4.17-7-6.js # res Expected SameValue(«false», «true») to be true +built-ins/Array/prototype/some/15.4.4.17-7-c-i-8.js # [, ].some(callbackfn) !== true +built-ins/Array/prototype/some/15.4.4.17-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/some/15.4.4.17-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator. +built-ins/Array/prototype/some/15.4.4.17-8-2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-3.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-4.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-5.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-6.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-7.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +built-ins/Array/prototype/some/15.4.4.17-8-8.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T3.js # #1: Check ToString operator built-ins/Array/prototype/sort/S15.4.4.11_A2.2_T3.js # #1: Check ToString operator +built-ins/Array/prototype/sort/S15.4.4.11_A3_T1.js # Array.prototype.sort called on incompatible receiver a data object. +built-ins/Array/prototype/sort/S15.4.4.11_A3_T2.js # Array.prototype.sort called on incompatible receiver a data object. +built-ins/Array/prototype/sort/S15.4.4.11_A4_T3.js # Array.prototype.sort called on incompatible receiver a data object. +built-ins/Array/prototype/sort/S15.4.4.11_A6_T2.js # Array.prototype.sort called on incompatible receiver a data object. built-ins/Array/prototype/sort/bug_596_1.js # #1: [object, object].sort(); counter < 2. Actual: 0 -built-ins/Array/prototype/sort/bug_596_2.js # .hasOwnProperty is not a function. +built-ins/Array/prototype/sort/precise-prototype-element.js # Expected SameValue(«undefined», «4») to be true +built-ins/Array/prototype/splice/S15.4.4.12_A2.1_T5.js # Array.splice expects start to be a number. +built-ins/Array/prototype/splice/S15.4.4.12_A2.2_T5.js # Array.splice expects delete count to be a number. +built-ins/Array/prototype/splice/S15.4.4.12_A2_T1.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A2_T2.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A2_T3.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A2_T4.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A3_T1.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A3_T3.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A4_T1.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A4_T2.js # Array.prototype.splice called on incompatible receiver a data object. +built-ins/Array/prototype/splice/S15.4.4.12_A4_T3.js # Array.prototype.splice called on incompatible receiver a data object. built-ins/Array/prototype/splice/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all -built-ins/Array/prototype/toLocaleString/S15.4.4.3_A1_T1.js # .toLocaleString is not a function. -built-ins/Array/prototype/toString/S15.4.4.2_A1_T1.js # .toString is not a function. -built-ins/Array/prototype/toString/S15.4.4.2_A1_T2.js # .toString is not a function. -built-ins/Array/prototype/toString/S15.4.4.2_A1_T3.js # .toString is not a function. -built-ins/Array/prototype/toString/S15.4.4.2_A1_T4.js # .toString is not a function. +built-ins/Array/prototype/toLocaleString/S15.4.4.3_A1_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; var arr = [undefined, obj, null, obj, o +built-ins/Array/prototype/toLocaleString/S15.4.4.3_A3_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; Array.prototype[1] = obj; x = [obj]; x. +built-ins/Array/prototype/toReversed/holes-not-preserved.js # Actual [4, undefined, 2, undefined, 0] and expected [4, 3, 2, undefined, 0] should have the same con +built-ins/Array/prototype/toSorted/holes-not-preserved.js # Actual [1, 3, 4, undefined, undefined] and expected [1, 2, 3, 4, undefined] should have the same con +built-ins/Array/prototype/toSpliced/holes-not-preserved.js # Actual [0, undefined, 2, undefined, 4] and expected [0, undefined, 2, 3, 4] should have the same con +built-ins/Array/prototype/toString/S15.4.4.2_A1_T4.js # Expected a Test262Error to be thrown but no exception was thrown at all +built-ins/Array/prototype/toString/S15.4.4.2_A3_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.toString() === "0,1". Actual: 0, +built-ins/Array/prototype/unshift/S15.4.4.13_A2_T1.js # Array.prototype.unshift called on incompatible receiver a data object. +built-ins/Array/prototype/unshift/S15.4.4.13_A2_T3.js # Array.prototype.unshift called on incompatible receiver a data object. +built-ins/Array/prototype/unshift/S15.4.4.13_A3_T2.js # Array.prototype.unshift called on incompatible receiver a data object. +built-ins/Array/prototype/unshift/S15.4.4.13_A4_T1.js # Array.prototype.unshift called on incompatible receiver a data object. +built-ins/Array/prototype/unshift/S15.4.4.13_A4_T2.js # #3: Array.prototype[0] = 1; x = []; x.length = 1; x.unshift(0); x[1] === 1. Actual: undefined built-ins/Array/prototype/values/iteration-mutable.js # .next is not a function. built-ins/Array/prototype/values/iteration.js # .next is not a function. language/statements/async-function/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran language/statements/async-generator/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/async-generator/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all language/statements/async-generator/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all +language/statements/async-generator/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/async-generator/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/async-generator/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/async-generator/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all @@ -101,6 +258,7 @@ language/statements/async-generator/dstr/ary-ptrn-elem-obj-val-undef.js # Expec language/statements/async-generator/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/async-generator/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/async-generator/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all +language/statements/async-generator/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/async-generator/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/async-generator/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/async-generator/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all @@ -135,6 +293,7 @@ language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-null.js # Expe language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/async-generator/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret" +language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/for-await-of/async-func-decl-dstr-array-elem-init-in.js # Failed to parse TypeScript: '…' expected. language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean. language/statements/for-await-of/async-func-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number. @@ -148,7 +307,6 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeErr language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string. language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number. language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string. -language/statements/for-in/12.6.4-1.js # .hasOwnProperty is not a function. language/statements/for-in/head-lhs-member.js # Unsupported for...in binding. language/statements/for-of/Array.prototype.Symbol.iterator.js # The called value is not a function. language/statements/for-of/dstr/array-elem-init-in.js # Failed to parse TypeScript: '…' expected. @@ -165,27 +323,43 @@ language/statements/for-of/dstr/array-rest-iter-rtrn-close-err.js # Iterator ne language/statements/for-of/dstr/array-rest-iter-rtrn-close-null.js # Iterator next must be a function. language/statements/for-of/dstr/array-rest-iter-thrw-close-err.js # Expected SameValue(«11», «0») to be true language/statements/for-of/dstr/array-rest-lref-err.js # Expected SameValue(«1», «0») to be true +language/statements/for-of/dstr/const-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all +language/statements/for-of/dstr/let-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/for-of/dstr/obj-empty-bool.js # Object destructuring requires a data object or array value, received a boolean. language/statements/for-of/dstr/obj-empty-num.js # Object destructuring requires a data object or array value, received a number. language/statements/for-of/dstr/obj-empty-string.js # Object destructuring requires a data object or array value, received a string. language/statements/for-of/dstr/obj-rest-number.js # Object destructuring requires a data object or array value, received a number. language/statements/for-of/dstr/obj-rest-str-val.js # Object destructuring requires a data object or array value, received a string. +language/statements/for-of/dstr/var-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/for-of/throw-from-catch.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct language/statements/for-of/throw-from-finally.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct language/statements/for-of/throw.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/for/dstr/const-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all +language/statements/for/dstr/let-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all +language/statements/for/dstr/var-ary-init-iter-get-err-array-prototype.js # Expected a TypeError but got a ReferenceError language/statements/function/13.2-19-b-3gs.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/function/13.2-2-s.js # Expected a TypeError to be thrown but no exception was thrown at all -language/statements/function/S13.2.1_A5_T1.js # .toString is not a function. -language/statements/function/S13.2_A7_T1.js # The called value is not a function. +language/statements/function/S13.2.2_A1_T1.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/function/S13.2.2_A1_T2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/function/S13.2.2_A2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/function/S13.2.2_A3_T1.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/function/S13.2.2_A3_T2.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct +language/statements/function/S13.2_A1_T1.js # #1: __func.prototype !== undefined +language/statements/function/S13.2_A1_T2.js # #1: __func.prototype !== undefined +language/statements/function/S13.2_A4_T1.js # Unknown identifier '…'. +language/statements/function/S13.2_A4_T2.js # #1: typeof __gunc.prototype === '…'. Actual: typeof __gunc.prototype ===undefined language/statements/function/S13_A3_T1.js # Unknown identifier '…'. language/statements/function/S13_A6_T1.js # Identifier '…' has already been declared. language/statements/function/S14_A2.js # Unknown identifier '…'. language/statements/function/S14_A5_T1.js # Identifier '…' has already been declared. language/statements/function/S14_A5_T2.js # Identifier '…' has already been declared. +language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all +language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/function/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran language/statements/generators/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/generators/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all language/statements/generators/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all +language/statements/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/generators/dstr/ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/generators/dstr/ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/generators/dstr/ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all @@ -196,6 +370,7 @@ language/statements/generators/dstr/ary-ptrn-elem-obj-val-undef.js # Expected a language/statements/generators/dstr/ary-ptrn-elision-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/generators/dstr/ary-ptrn-rest-id-elision-next-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/generators/dstr/ary-ptrn-rest-id-iter-step-err.js # Expected a Test262Error to be thrown but no exception was thrown at all +language/statements/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/generators/dstr/dflt-ary-init-iter-get-err.js # Expected a Test262Error to be thrown but no exception was thrown at all language/statements/generators/dstr/dflt-ary-ptrn-elem-ary-val-null.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/generators/dstr/dflt-ary-ptrn-elem-id-init-throws.js # Expected a Test262Error to be thrown but no exception was thrown at all @@ -228,10 +403,13 @@ language/statements/generators/dstr/obj-ptrn-prop-id-init-throws.js # Expected language/statements/generators/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all language/statements/generators/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/generators/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all -language/statements/generators/has-instance.js # The right-hand side of '…' must be a supported constructor: Error (or a specific error type like Typ +language/statements/generators/has-instance.js # The right-hand side of '…' has no '…' object. +language/statements/generators/prototype-typeof.js # Expected SameValue(«"undefined"», «"object"») to be true +language/statements/generators/prototype-uniqueness.js # Expected true but got false language/statements/generators/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran language/statements/labeled/value-await-non-module-escaped.js # Failed to parse TypeScript: Keywords cannot contain escape characters. language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected. +language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all language/statements/return/S12.9_A1_T1.js # expected SyntaxError but the program ran language/statements/return/S12.9_A1_T10.js # expected SyntaxError but the program ran language/statements/return/S12.9_A1_T2.js # expected SyntaxError but the program ran @@ -244,5 +422,5 @@ language/statements/return/S12.9_A1_T8.js # expected SyntaxError but the progra language/statements/return/S12.9_A1_T9.js # expected SyntaxError but the program ran language/statements/switch/S12.11_A1_T4.js # Switch discriminants must be data values. language/statements/switch/scope-lex-open-dflt.js # Switch discriminants must be data values. -language/statements/try/S12.14_A19_T1.js # .toString is not a function. -language/statements/try/S12.14_A19_T2.js # .toString is not a function. +language/statements/try/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all +language/statements/variable/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index b7b8a1874be2..560b3c6acb6a 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -176,7 +176,7 @@ describe("blocked member names on tool paths", () => { expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real") }) - test("prototype machinery is unreachable through data values", async () => { + test("prototypes are program objects; __proto__ is an ordinary key and the host is unreachable", async () => { expect( await value( runtime, @@ -185,21 +185,21 @@ describe("blocked member names on tool paths", () => { const array = [] object.__proto__ = { polluted: true } return [ - object.constructor === Object, array.constructor === Array, "".constructor === String, Math.constructor, - object.__proto__.polluted, ({}).polluted, array.__proto__, Object().__proto__, new Object().constructor === Object, - ({}).constructor.constructor, [].constructor.__proto__, typeof [].__proto__, + object.constructor === Object, array.constructor === Array, "".constructor === String, + Math.constructor === Object, object.__proto__.polluted, ({}).polluted, array.__proto__, + Object().__proto__, new Object().constructor === Object, ({}).constructor.constructor === Function, + [].constructor.__proto__, typeof [].__proto__, (() => 1).constructor === Function, ] `, ), - ).toEqual([true, true, true, null, true, null, null, null, true, null, null, "undefined"]) - expect(await value(runtime, `return [(() => 1).constructor, typeof (() => 1).__proto__]`)).toEqual([ - null, - "undefined", - ]) - const escape = await failure(runtime, `return ({}).constructor.constructor.constructor("return 1")()`) - expect(escape.message).toContain("Cannot access a property on a non-object value") + ).toEqual([true, true, true, true, true, null, null, null, true, true, null, "undefined", true]) + const escape = await failure(runtime, `return ({}).constructor.constructor("return 1")()`) + expect(escape.message).toContain("The Function constructor is not supported") const poisoned = await failure(runtime, `const o = {}; o.__proto__.constructor("return 1")`) - expect(poisoned.message).toContain("Cannot access a property on a non-object value") + expect(poisoned.message).toContain("Cannot read properties of undefined") + // Prototype mutation is confined to one run: the next program starts from fresh intrinsics. + expect(await value(runtime, `Object.prototype.polluted = 1; Array.prototype.push = 2; return ({}).polluted`)).toBe(1) + expect(await value(runtime, `return [({}).polluted, typeof [].push]`)).toEqual([null, "function"]) expect(Object.keys(Object.prototype)).toEqual([]) expect(Object.keys(Array.prototype)).toEqual([]) })