Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions packages/codemode/interpreter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [ ] Destructuring a key that member access resolves through the owning built-in, such as
`const { constructor } = error`, reads `undefined`.
- [ ] Member expressions as `for...in` targets (`for (x.y in obj)`).
- [ ] `IteratorClose` during destructuring should throw a `TypeError` when `return()` yields a non-object.

## Statements and control flow

Expand Down Expand Up @@ -128,8 +129,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [ ] 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.
- [ ] Async generator functions evaluate parameter defaults and destructuring at the first `next()` rather than at the
call, so their errors are not thrown synchronously.
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
rather than at the call, so their errors are not thrown synchronously.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
Expand Down Expand Up @@ -173,8 +174,12 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [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.
- [ ] Operators, `switch` discriminants, and coercion helpers such as `String` and `isNaN` applied to functions and
namespaces; JavaScript coerces them, the interpreter rejects non-data operands.
- [ ] 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
`valueOf`/`toString` in spec order and surface their throws.
- [ ] Property keys follow ToPropertyKey: `x[null]`, `x[true]`, and objects (via `toString`) become string keys; only
strings and numbers are accepted.

## Promises and tools

Expand Down Expand Up @@ -239,15 +244,17 @@ 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. `x.constructor` without an own key resolves
to the owning built-in (`[].constructor === Array`, `new TypeError().constructor === TypeError`); prototype objects
are not observable, so `[].__proto__` and `Object.prototype` read as `undefined` and `o.__proto__ = x` sets an own
field.
- [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] 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`, and `hasOwnProperty`.
- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, and
`propertyIsEnumerable`.

## Arrays

Expand Down Expand Up @@ -298,6 +305,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
`repeat` still requires a finite non-negative count.
- [x] Native no-argument parity for `match()`, `matchAll()`, and `search()`; all behave as an empty pattern. Present
arguments must still be a regular expression or string pattern.
- [ ] `String.raw`.
- [ ] `match`, `search`, and `split` accept any value and coerce it (objects via `toString`), like JavaScript.

## Numbers and Math

Expand Down Expand Up @@ -352,6 +361,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
- [x] `toDateString` and `toTimeString` in the host's local timezone.
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
- [ ] Date setters and multi-argument construction coerce object arguments through `valueOf`/`toString` and surface
their throws.
- [x] Native Date loose-equality and default primitive-coercion semantics, using CodeMode's deterministic ISO string
representation for the string primitive.
- [x] Native `RangeError` branding for invalid `toISOString()` calls.
Expand Down Expand Up @@ -396,8 +407,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262

## Web platform helpers

- [x] `atob` and `btoa` with forgiving-base64 decoding and WebIDL string conversion; invalid input throws an Error
named `InvalidCharacterError`, since there is no `DOMException`.
- [x] `atob` and `btoa` with forgiving-base64 decoding and WebIDL string conversion; invalid input throws a
`TypeError`, since there is no `DOMException`.
- [x] `crypto.randomUUID()`.
- [ ] `crypto.getRandomValues` and `crypto.subtle`, `TextEncoder`/`TextDecoder`, and `Blob`: these need a binary
value type, which the JSON-like data model does not have yet.
Expand All @@ -421,5 +432,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
This is deliberate: the program should handle a failure the same way regardless of where it originated.
- [ ] Failures raised by the interpreter itself carry the generic `Error` name where JavaScript throws a `TypeError`,
`RangeError`, or `ReferenceError`, so `e instanceof TypeError` and `e.constructor === TypeError` are false.
- [x] Failures raised by the interpreter are `TypeError`s unless JavaScript names them otherwise (`RangeError`,
`ReferenceError`, `SyntaxError`, `URIError`), so `e instanceof TypeError` and `e.constructor === TypeError`
hold. Unsupported syntax reached at runtime is a `SyntaxError`; awaited tool failures stay plain `Error`.
7 changes: 7 additions & 0 deletions packages/codemode/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export * as Data from "./data.js"

import type { DiagnosticKind } from "./codemode.js"
import {
get,
ownEntries,
parseArrayIndex,
ProgramArray,
ProgramError,
ProgramFunction,
ProgramObject,
set,
Expand Down Expand Up @@ -120,6 +122,11 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
}
if (value instanceof ProgramObject) {
const copied: Record<string, unknown> = {}
// Errors serialize as { name, message, ...own }: both may be inherited, and neither is enumerable in JS.
if (value instanceof ProgramError) {
define(copied, "name", copy(get(value, "name"), label, mode, depth + 1, seen))
define(copied, "message", copy(get(value, "message"), label, mode, depth + 1, seen))
}
for (const [key, item] of ownEntries(value)) {
const next = copy(item, label, mode, depth + 1, seen)
if (next === undefined && mode === "json") continue
Expand Down
51 changes: 24 additions & 27 deletions packages/codemode/src/interpreter/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,10 @@ 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 { get, ProgramError, ProgramObject } from "./objects.js"
import { createErrorValue, type ErrorType, isErrorType } from "./intrinsics.js"
import { get, hasPrototype, ProgramArray, ProgramError, ProgramObject, set } from "./objects.js"
import { type Runner } from "./runner.js"
import {
coerceToString,
createAggregateErrorValue,
createErrorValue,
errorBrandName,
errorConstructors,
} from "../stdlib/value.js"
import { coerceToString } from "../stdlib/value.js"

export const normalizeError = (error: unknown): Diagnostic => {
if (error instanceof InterpreterRuntimeError) {
Expand Down Expand Up @@ -77,15 +72,19 @@ export const normalizeError = (error: unknown): Diagnostic => {
}
}

export const caughtErrorValue = (thrown: unknown): unknown => {
export const caughtErrorValue = <R>(runner: Runner<R>, thrown: unknown): unknown => {
if (thrown instanceof ProgramThrow) return thrown.value
if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error"
return createErrorValue(name, normalizeError(thrown).message)
const prototypes = runner.intrinsics.errors
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)
}

const constructErrorValue = (name: string, args: Array<unknown>): ProgramError =>
createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
export const createAggregateErrorValue = <R>(runner: Runner<R>, errors: Array<unknown>, message: string) => {
const value = createErrorValue(runner.intrinsics.errors.AggregateError, message)
set(value, "errors", new ProgramArray(errors))
return value
}

const constructAggregateErrorValue = <R>(
runner: Runner<R>,
Expand All @@ -95,33 +94,31 @@ const constructAggregateErrorValue = <R>(
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node).as(
"TypeError",
)
throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node)
}
const errors: Array<unknown> = []
while (true) {
const step = yield* cursor.next
if (step.done) {
return createAggregateErrorValue(errors, args[1] === undefined ? "" : coerceToString(args[1]))
return createAggregateErrorValue(runner, errors, args[1] === undefined ? "" : coerceToString(args[1]))
}
errors.push(step.value)
}
})

/** An error constructor such as `Error` or `TypeError`; callable with or without `new`, like JS. */
export const errorGlobal = <R>(name: string, runner: Runner<R>) => {
export const errorGlobal = <R>(type: ErrorType, runner: Runner<R>) => {
const prototype = runner.intrinsics.errors[type]
const construct: HostCall<R> = (args, node) =>
name === "AggregateError"
type === "AggregateError"
? constructAggregateErrorValue(runner, args, node)
: Effect.sync(() => constructErrorValue(name, args))
return new HostFunction<R>({
name,
: Effect.sync(() => createErrorValue(prototype, args[0] === undefined ? undefined : coerceToString(args[0])))
const fn = new HostFunction<R>({
name: type,
call: construct,
construct,
instanceOf: (value) => {
const brand = errorBrandName(value)
return brand !== undefined && (name === "Error" || brand === name)
},
instanceOf: (value) => hasPrototype(value, prototype),
})
set(prototype, "constructor", fn)
return fn
}
7 changes: 4 additions & 3 deletions packages/codemode/src/interpreter/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ 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, errorConstructors } from "../stdlib/value.js"
import { coercion } from "../stdlib/value.js"
import { atobGlobal, btoaGlobal, 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 { promiseGlobal, type PromiseRuntime } from "./promises.js"
Expand All @@ -35,7 +36,7 @@ const symbolGlobal = new HostFunction({
throw new InterpreterRuntimeError(
"Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.",
node,
).as("TypeError")
)
}),
callback: false,
members: { asyncIterator: AsyncIteratorSymbol, iterator: IteratorSymbol },
Expand Down Expand Up @@ -75,5 +76,5 @@ export const globals = <R>(host: Host<R>): ReadonlyArray<readonly [string, unkno
["atob", atobGlobal],
["btoa", btoaGlobal],
["crypto", cryptoGlobal],
...[...errorConstructors].map((name) => [name, errorGlobal(name, host.runner)] as const),
...errorTypes.map((type) => [type, errorGlobal(type, host.runner)] as const),
]
2 changes: 1 addition & 1 deletion packages/codemode/src/interpreter/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,5 @@ export const requiresNew =
(name: string): HostCall<never> =>
(_, node) =>
Effect.sync(() => {
throw new InterpreterRuntimeError(`Constructor ${name} requires 'new'.`, node).as("TypeError")
throw new InterpreterRuntimeError(`Constructor ${name} requires 'new'.`, node)
})
51 changes: 51 additions & 0 deletions packages/codemode/src/interpreter/intrinsics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ProgramError, ProgramObject, set } from "./objects.js"

export const errorTypes = [
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
] as const

export type ErrorType = (typeof errorTypes)[number]

export const isErrorType = (name: string): name is ErrorType => (errorTypes as ReadonlyArray<string>).includes(name)

/** The built-in prototype objects of one runtime. Constructors attach themselves as `constructor` when created. */
export type Intrinsics = {
readonly errors: Readonly<Record<ErrorType, ProgramObject>>
}

export const createErrorValue = (prototype: ProgramObject, message: string | undefined): ProgramError => {
const value = new ProgramError(prototype)
if (message !== undefined) set(value, "message", message)
return value
}

export const createIntrinsics = (): Intrinsics => {
const error = new ProgramObject()
set(error, "name", "Error")
set(error, "message", "")
const derived = (type: ErrorType) => {
const proto = new ProgramObject(error)
set(proto, "name", type)
set(proto, "message", "")
return proto
}
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"),
},
}
}
Loading
Loading