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
39 changes: 22 additions & 17 deletions packages/codemode/interpreter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`.
Expand Down
113 changes: 65 additions & 48 deletions packages/codemode/src/data.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Expand All @@ -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<object>): unknown => {
const copy = (
value: unknown,
label: string,
mode: Mode,
depth: number,
seen: Set<object>,
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}.`)
}
Expand All @@ -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)) {
Expand All @@ -116,36 +133,36 @@ 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
}
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))
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
}
Expand All @@ -155,24 +172,24 @@ 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<string, unknown> = {}
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
}

// 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 })
}
1 change: 0 additions & 1 deletion packages/codemode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading
Loading