mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(codemode): give programs their own objects and arrays (#48527)
This commit is contained in:
@@ -243,7 +243,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [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 null-prototype results.
|
||||
and plain-object results.
|
||||
- [ ] `Object.prototype` methods on values: `toString`, `toLocaleString`, `valueOf`, and `hasOwnProperty`.
|
||||
|
||||
## Arrays
|
||||
@@ -264,12 +264,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
||||
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
||||
- [x] `Array.prototype.toSpliced`.
|
||||
- [x] Canonical array/string index parsing: keys such as `"01"` remain non-index properties rather than aliasing index
|
||||
`1`; arbitrary array-property assignment remains unsupported.
|
||||
- [x] Canonical array/string index parsing: keys such as `"01"` are ordinary properties rather than aliases of index
|
||||
`1`.
|
||||
- [x] `Array.prototype.sort` preserves trailing holes, while `toSorted` densifies holes into `undefined` elements,
|
||||
like JavaScript.
|
||||
- [ ] Assigning `length` to truncate or extend an array.
|
||||
- [ ] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`).
|
||||
- [x] Assigning `length` to truncate or extend an array; invalid lengths throw `RangeError`.
|
||||
- [x] Non-index own properties on arrays (`arr.foo = 1`, `arr.constructor = null`). They are excluded from the JSON
|
||||
form, like `JSON.stringify`.
|
||||
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
|
||||
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
|
||||
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
|
||||
@@ -358,7 +359,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `test`, `exec`, and `toString`.
|
||||
- [x] Readable `source`, `flags`, `lastIndex`, `hasIndices`, `global`, `ignoreCase`, `multiline`, `sticky`, `unicode`,
|
||||
`unicodeSets`, and `dotAll`.
|
||||
- [x] Captures, named groups, match `.index`, and stateful global matching.
|
||||
- [x] Captures, named groups, match `.index` and `.input`, and stateful global matching.
|
||||
- [x] Integration with supported String methods, including function replacers.
|
||||
- [x] Writable `lastIndex`.
|
||||
- [x] Match `indices` metadata for the `d` flag, including named groups on `exec`, `match`, and `matchAll` results.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
export * as Data from "./data.js"
|
||||
|
||||
import type { DiagnosticKind } from "./codemode.js"
|
||||
import { ownEntries, parseArrayIndex, ProgramArray, ProgramObject, set } from "./interpreter/objects.js"
|
||||
import { Values } from "./values.js"
|
||||
|
||||
/** A null-prototype object owned by the program. */
|
||||
export type SafeObject = Record<string, unknown>
|
||||
|
||||
const MAX_VALUE_DEPTH = 32
|
||||
|
||||
export class ToolRuntimeError extends Error {
|
||||
@@ -23,15 +21,15 @@ export class ToolRuntimeError extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings a host-produced runtime value into the program: runtime values pass through, their host
|
||||
* counterparts (Date, RegExp, Map, Set, URL, URLSearchParams) are wrapped, and objects become
|
||||
* null-prototype copies. Arrays keep extra enumerable properties such as `index` and `groups`.
|
||||
* 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.
|
||||
*/
|
||||
export const toProgram = (value: unknown, label: string): unknown => copy(value, label, "program", 0, new Set())
|
||||
|
||||
/**
|
||||
* Brings host data into the program: Date and URL become strings, other host collections become
|
||||
* empty objects, and objects become null-prototype copies. Used for tool results and parsed JSON.
|
||||
* 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())
|
||||
|
||||
@@ -44,8 +42,7 @@ export const fromData = (value: unknown, label: string): unknown => copy(value,
|
||||
export const toData = (value: unknown, label: string, undefinedAs: "json" | "result" = "json"): unknown =>
|
||||
copy(value, label, undefinedAs, 0, new Set())
|
||||
|
||||
// "program" and "data" build program-owned null-prototype objects; "json" and "result" build
|
||||
// ordinary objects for the host.
|
||||
// "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 => {
|
||||
@@ -65,8 +62,9 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
)
|
||||
}
|
||||
|
||||
const plain = mode === "program" || mode === "data"
|
||||
if (mode === "program") {
|
||||
if (Values.isValue(value)) return value
|
||||
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 (value instanceof Map) {
|
||||
@@ -85,7 +83,6 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
if (value instanceof URLSearchParams) return new Values.URLSearchParams(new URLSearchParams(value))
|
||||
}
|
||||
|
||||
const plain = mode === "program" || mode === "data"
|
||||
if (value instanceof Values.Date) 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
|
||||
@@ -98,7 +95,7 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
value instanceof Set ||
|
||||
value instanceof URLSearchParams
|
||||
) {
|
||||
return plain ? (Object.create(null) as SafeObject) : {}
|
||||
return plain ? new ProgramObject() : {}
|
||||
}
|
||||
|
||||
if (seen.has(value)) {
|
||||
@@ -106,27 +103,48 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
}
|
||||
seen.add(value)
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Host output densifies holes to null like JSON; program copies keep them.
|
||||
const copied = plain
|
||||
? value.map((item) => copy(item, label, mode, depth + 1, seen))
|
||||
: Array.from(value, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
|
||||
if (mode === "program") {
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (Object.hasOwn(copied, key)) continue
|
||||
define(copied, key, copy(item, label, mode, depth + 1, seen))
|
||||
}
|
||||
if (value instanceof ProgramArray) {
|
||||
const copied = Array.from(value.items, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
if (value instanceof ProgramObject) {
|
||||
const copied: Record<string, unknown> = {}
|
||||
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)
|
||||
}
|
||||
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)))
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (parseArrayIndex(key) === undefined) set(copied, key, copy(item, label, mode, depth + 1, seen))
|
||||
}
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
const copied = Array.from(value, (item) => copy(item, label, mode, depth + 1, seen) ?? null)
|
||||
seen.delete(value)
|
||||
return copied
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`)
|
||||
}
|
||||
|
||||
const copied: SafeObject = plain ? (Object.create(null) as SafeObject) : {}
|
||||
if (plain) {
|
||||
const copied = new ProgramObject()
|
||||
for (const [key, item] of Object.entries(value)) set(copied, key, copy(item, label, mode, depth + 1, seen))
|
||||
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
|
||||
@@ -136,8 +154,8 @@ const copy = (value: unknown, label: string, mode: Mode, depth: number, seen: Se
|
||||
return copied
|
||||
}
|
||||
|
||||
// Own data property regardless of the target's prototype, so a "__proto__" key on a host object or
|
||||
// array never reaches the Object.prototype setter.
|
||||
// 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 => {
|
||||
Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true })
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import type { Diagnostic } from "../codemode.js"
|
||||
import { ToolError } from "../tool-error.js"
|
||||
import { type SafeObject, toData, ToolRuntimeError } from "../data.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 { get, ProgramError, ProgramObject } from "./objects.js"
|
||||
import { type Runner } from "./runner.js"
|
||||
import {
|
||||
coerceToString,
|
||||
@@ -44,12 +45,8 @@ export const normalizeError = (error: unknown): Diagnostic => {
|
||||
message = "a non-data value"
|
||||
} else if (typeof value === "string") {
|
||||
message = value
|
||||
} else if (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
typeof (value as { message?: unknown }).message === "string"
|
||||
) {
|
||||
message = (value as { message: string }).message
|
||||
} else if (value instanceof ProgramObject && typeof get(value, "message") === "string") {
|
||||
message = get(value, "message") as string
|
||||
} else {
|
||||
try {
|
||||
message = JSON.stringify(toData(value, "Thrown value")) ?? String(value)
|
||||
@@ -87,14 +84,14 @@ export const caughtErrorValue = (thrown: unknown): unknown => {
|
||||
return createErrorValue(name, normalizeError(thrown).message)
|
||||
}
|
||||
|
||||
const constructErrorValue = (name: string, args: Array<unknown>): SafeObject =>
|
||||
const constructErrorValue = (name: string, args: Array<unknown>): ProgramError =>
|
||||
createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
|
||||
|
||||
const constructAggregateErrorValue = <R>(
|
||||
runner: Runner<R>,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<SafeObject, unknown, R> =>
|
||||
): Effect.Effect<ProgramError, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(args[0], node)
|
||||
if (cursor === undefined) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Effect } from "effect"
|
||||
import { type SafeObject, toProgram } from "../data.js"
|
||||
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, errorBrandName } from "../stdlib/value.js"
|
||||
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
|
||||
import { compareText } from "../tool-runtime.js"
|
||||
import { Values } from "../values.js"
|
||||
import { type AstNode, IntrinsicReference, InterpreterRuntimeError } 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"
|
||||
|
||||
@@ -16,6 +17,14 @@ export const invokeIntrinsic = <R>(
|
||||
ref: IntrinsicReference,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.map(invoke(runner, ref, args, node), (result) => (Array.isArray(result) ? new ProgramArray(result) : result))
|
||||
|
||||
const invoke = <R>(
|
||||
runner: Runner<R>,
|
||||
ref: IntrinsicReference,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (typeof ref.receiver === "string") {
|
||||
if (ref.name === "replace" || ref.name === "replaceAll") {
|
||||
@@ -32,7 +41,7 @@ export const invokeIntrinsic = <R>(
|
||||
if (typeof ref.receiver === "number") {
|
||||
return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
|
||||
}
|
||||
if (Array.isArray(ref.receiver)) {
|
||||
if (ref.receiver instanceof ProgramArray) {
|
||||
return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
|
||||
}
|
||||
if (ref.receiver instanceof Values.Date) {
|
||||
@@ -215,7 +224,7 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
|
||||
node,
|
||||
)
|
||||
}
|
||||
return Array.from(value.matchAll(pattern), matchToValue)
|
||||
return new ProgramArray(Array.from(value.matchAll(pattern), matchToValue))
|
||||
}
|
||||
case "search": {
|
||||
result = value.search(toHostRegex(args[0], name, node))
|
||||
@@ -288,13 +297,7 @@ const invokeStringReplacer = <R>(
|
||||
if (typeof match !== "string" || typeof offset !== "number") {
|
||||
throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node)
|
||||
}
|
||||
if (hasGroups) {
|
||||
const safeGroups: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, group] of Object.entries(groups)) {
|
||||
safeGroups[key] = group
|
||||
}
|
||||
callbackArgs[callbackArgs.length - 1] = safeGroups
|
||||
}
|
||||
if (hasGroups) callbackArgs[callbackArgs.length - 1] = record(groups as Record<string, unknown>)
|
||||
matches.push({ match, offset, args: callbackArgs })
|
||||
return match
|
||||
}
|
||||
@@ -320,14 +323,11 @@ const invokeStringReplacer = <R>(
|
||||
let end = 0
|
||||
for (const match of matches) {
|
||||
const replacement = yield* apply(match.args)
|
||||
// Error values are branded plain objects; toProgram would strip the brand before coercion.
|
||||
output.push(
|
||||
value.slice(end, match.offset),
|
||||
replacement instanceof Values.Promise
|
||||
? "[object Promise]"
|
||||
: errorBrandName(replacement)
|
||||
? coerceToString(replacement)
|
||||
: coerceToString(toProgram(replacement, `String.${name} replacer result`)),
|
||||
: coerceToString(toProgram(replacement, `String.${name} replacer result`)),
|
||||
)
|
||||
end = match.offset + match.match.length
|
||||
}
|
||||
@@ -365,7 +365,7 @@ const invokeMapMethod = <R>(
|
||||
case "values":
|
||||
return Effect.sync(() => Array.from(target.map.values()))
|
||||
case "entries":
|
||||
return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item]))
|
||||
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* () {
|
||||
@@ -404,7 +404,7 @@ const invokeSetMethod = <R>(
|
||||
case "values":
|
||||
return Effect.sync(() => Array.from(target.set.values()))
|
||||
case "entries":
|
||||
return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item]))
|
||||
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* () {
|
||||
@@ -518,28 +518,27 @@ const loadSetRecord = <R>(runner: Runner<R>, source: unknown, name: string, node
|
||||
keys: () => Effect.succeed(source.map.keys()),
|
||||
})
|
||||
}
|
||||
if (source === null || typeof source !== "object" || Values.isValue(source)) {
|
||||
if (!(source instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError(`Set.${name} expects a Set-like object.`, node).as("TypeError")
|
||||
}
|
||||
const object = source as Record<string, unknown>
|
||||
return Effect.gen(function* () {
|
||||
const size = yield* coerceNumericArgument(runner, object.size, node)
|
||||
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).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
if (!isSupportedCallback(object.has) || !isSupportedCallback(object.keys)) {
|
||||
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).as("TypeError")
|
||||
}
|
||||
const has = object.has
|
||||
const keys = object.keys
|
||||
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 (Array.isArray(result)) return Effect.succeed(result)
|
||||
if (result instanceof ProgramArray) return Effect.succeed(result.items)
|
||||
throw new InterpreterRuntimeError(`Set.${name} expected 'keys' to return an iterator.`, node).as("TypeError")
|
||||
}),
|
||||
}
|
||||
@@ -604,7 +603,7 @@ const invokeURLSearchParamsMethod = <R>(
|
||||
case "values":
|
||||
return Effect.sync(() => Array.from(target.params.values()))
|
||||
case "entries":
|
||||
return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value]))
|
||||
return Effect.sync(() => Array.from(target.params.entries(), ([key, value]) => new ProgramArray([key, value])))
|
||||
case "toString":
|
||||
return Effect.sync(() => target.params.toString())
|
||||
case "forEach": {
|
||||
@@ -622,11 +621,12 @@ const invokeURLSearchParamsMethod = <R>(
|
||||
|
||||
const invokeArrayMethod = <R>(
|
||||
runner: Runner<R>,
|
||||
target: Array<unknown>,
|
||||
receiver: ProgramArray,
|
||||
name: string,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const target = receiver.items
|
||||
const optNumber = (value: unknown, label: string): number | undefined => {
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== "number")
|
||||
@@ -638,9 +638,8 @@ const invokeArrayMethod = <R>(
|
||||
if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
|
||||
throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
|
||||
}
|
||||
const input = toProgram(target, "Array.join input") as Array<unknown>
|
||||
return Effect.succeed(
|
||||
input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
|
||||
target.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
|
||||
)
|
||||
}
|
||||
case "includes":
|
||||
@@ -660,11 +659,15 @@ const invokeArrayMethod = <R>(
|
||||
case "slice":
|
||||
return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end")))
|
||||
case "concat":
|
||||
return Effect.succeed(target.concat(...args))
|
||||
case "flat":
|
||||
return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
|
||||
return Effect.succeed(target.concat(...args.map((item) => (item instanceof ProgramArray ? item.items : item))))
|
||||
case "flat": {
|
||||
const flatten = (items: Array<unknown>, depth: number): Array<unknown> =>
|
||||
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":
|
||||
return Effect.succeed(target.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
|
||||
@@ -676,7 +679,7 @@ const invokeArrayMethod = <R>(
|
||||
Array.from({ length: holeCount }, (_, index) => itemCount + index).forEach((index) => {
|
||||
Reflect.deleteProperty(target, index)
|
||||
})
|
||||
return target
|
||||
return receiver
|
||||
})
|
||||
}
|
||||
case "toSorted":
|
||||
@@ -695,12 +698,12 @@ const invokeArrayMethod = <R>(
|
||||
}
|
||||
case "push": {
|
||||
// Validate all insertions before mutating to avoid partial cyclic updates.
|
||||
for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node)
|
||||
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(target, item, "Array.unshift result", node)
|
||||
for (const item of args) rejectCircularInsertion(receiver, item, "Array.unshift result", node)
|
||||
target.unshift(...args)
|
||||
return Effect.succeed(target.length)
|
||||
}
|
||||
@@ -714,7 +717,7 @@ const invokeArrayMethod = <R>(
|
||||
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(target, item, "Array.splice result", node)
|
||||
for (const item of inserted) rejectCircularInsertion(receiver, item, "Array.splice result", node)
|
||||
return Effect.succeed(target.splice(start, deleteCount, ...inserted))
|
||||
}
|
||||
case "toSpliced": {
|
||||
@@ -731,23 +734,23 @@ const invokeArrayMethod = <R>(
|
||||
return Effect.succeed(copied)
|
||||
}
|
||||
case "fill": {
|
||||
rejectCircularInsertion(target, args[0], "Array.fill result", node)
|
||||
return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
|
||||
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":
|
||||
return Effect.succeed(
|
||||
target.copyWithin(
|
||||
optNumber(args[0], "target index") ?? 0,
|
||||
optNumber(args[1], "start") ?? 0,
|
||||
optNumber(args[2], "end"),
|
||||
),
|
||||
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]): Array<unknown> => [index, item]))
|
||||
return Effect.succeed(Array.from(target.entries(), ([index, item]) => new ProgramArray([index, item])))
|
||||
}
|
||||
|
||||
const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node)
|
||||
@@ -760,7 +763,7 @@ const invokeArrayMethod = <R>(
|
||||
values.length = length
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
values[index] = yield* apply([target[index], index, target])
|
||||
values[index] = yield* apply([target[index], index, receiver])
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -768,8 +771,8 @@ const invokeArrayMethod = <R>(
|
||||
const values: Array<unknown> = []
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const mapped = yield* apply([target[index], index, target])
|
||||
if (Array.isArray(mapped)) values.push(...mapped)
|
||||
const mapped = yield* apply([target[index], index, receiver])
|
||||
if (mapped instanceof ProgramArray) values.push(...mapped.items)
|
||||
else values.push(mapped)
|
||||
}
|
||||
return values
|
||||
@@ -779,36 +782,36 @@ const invokeArrayMethod = <R>(
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
const item = target[index]
|
||||
if (yield* apply([item, index, target])) values.push(item)
|
||||
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, target])) return item
|
||||
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, target])) return index
|
||||
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, target])) return true
|
||||
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, target]))) return false
|
||||
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, target])
|
||||
if (index in target) yield* apply([target[index], index, receiver])
|
||||
}
|
||||
return undefined
|
||||
case "reduce": {
|
||||
@@ -825,7 +828,7 @@ const invokeArrayMethod = <R>(
|
||||
}
|
||||
for (let index = start; index < length; index += 1) {
|
||||
if (!(index in target)) continue
|
||||
accumulator = yield* apply([accumulator, target[index], index, target])
|
||||
accumulator = yield* apply([accumulator, target[index], index, receiver])
|
||||
}
|
||||
return accumulator
|
||||
}
|
||||
@@ -843,19 +846,19 @@ const invokeArrayMethod = <R>(
|
||||
}
|
||||
for (let index = start; index >= 0; index -= 1) {
|
||||
if (!(index in target)) continue
|
||||
accumulator = yield* apply([accumulator, target[index], index, target])
|
||||
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, target])) return item
|
||||
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, target])) return index
|
||||
if (yield* apply([target[index], index, receiver])) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BlockStatement, Expression, Node, Pattern } from "acorn"
|
||||
import type { Effect } from "effect"
|
||||
import type { DiagnosticKind } from "../codemode.js"
|
||||
import type { SafeObject } from "../data.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. */
|
||||
@@ -20,7 +20,7 @@ export type StatementResult =
|
||||
| { kind: "continue"; label?: string }
|
||||
|
||||
export type MemberReference = {
|
||||
target: SafeObject | Array<unknown> | Values.RegExp | Values.URL
|
||||
target: ProgramObject | Values.RegExp | Values.URL
|
||||
key: PropertyKey
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { AsyncIteratorSymbol, IteratorSymbol } from "./model.js"
|
||||
|
||||
/** An object owned by the program: own properties plus a prototype link. */
|
||||
export class ProgramObject {
|
||||
readonly props = new Map<PropertyKey, unknown>()
|
||||
constructor(public proto: ProgramObject | null = null) {}
|
||||
}
|
||||
|
||||
export class ProgramArray extends ProgramObject {
|
||||
constructor(readonly items: Array<unknown> = []) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgramError extends ProgramObject {
|
||||
constructor(readonly errorName: string) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_ARRAY_LENGTH = 4_294_967_295
|
||||
|
||||
export const parseArrayIndex = (key: string | number): number | undefined => {
|
||||
const property = String(key)
|
||||
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
|
||||
const index = Number(property)
|
||||
return index < MAX_ARRAY_LENGTH ? index : undefined
|
||||
}
|
||||
|
||||
const canonical = (key: PropertyKey): string | symbol => (typeof key === "symbol" ? key : String(key))
|
||||
|
||||
const index = (target: ProgramObject, key: string | symbol): number | undefined =>
|
||||
target instanceof ProgramArray && typeof key === "string" ? parseArrayIndex(key) : undefined
|
||||
|
||||
export const hasOwn = (target: ProgramObject, key: PropertyKey): boolean => {
|
||||
const name = canonical(key)
|
||||
const at = index(target, name)
|
||||
if (at !== undefined) return at in (target as ProgramArray).items
|
||||
if (name === "length" && target instanceof ProgramArray) return true
|
||||
return target.props.has(name)
|
||||
}
|
||||
|
||||
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 (name === "length" && target instanceof ProgramArray) return target.items.length
|
||||
return target.props.get(name)
|
||||
}
|
||||
|
||||
export const get = (target: ProgramObject, key: PropertyKey): unknown => {
|
||||
for (let current: ProgramObject | null = target; current !== null; current = current.proto) {
|
||||
if (hasOwn(current, key)) return getOwn(current, key)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
target.props.set(name, value)
|
||||
return 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
|
||||
target.props.delete(name)
|
||||
return true
|
||||
}
|
||||
|
||||
// JS order: array indexes, integer-like keys ascending, other strings, then symbols.
|
||||
export const ownKeys = (target: ProgramObject): Array<string | symbol> => {
|
||||
const strings = [...target.props.keys()].filter((key): key is string => typeof key === "string")
|
||||
const symbols = [...target.props.keys()].filter((key): key is symbol => typeof key === "symbol")
|
||||
return [
|
||||
...(target instanceof ProgramArray ? Object.keys(target.items) : []),
|
||||
...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)])
|
||||
|
||||
export const record = (entries: Record<string, unknown>): ProgramObject => {
|
||||
const target = new ProgramObject()
|
||||
for (const [key, value] of Object.entries(entries)) set(target, key, value)
|
||||
return target
|
||||
}
|
||||
|
||||
export const assign = (target: ProgramObject, source: ProgramObject, skip?: ReadonlySet<PropertyKey>): void => {
|
||||
for (const key of ownKeys(source)) {
|
||||
if (skip?.has(key)) continue
|
||||
if (typeof key === "symbol" && key !== IteratorSymbol && key !== AsyncIteratorSymbol) continue
|
||||
set(target, key, getOwn(source, key))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
|
||||
import type { Diagnostic } from "../codemode.js"
|
||||
import type { SafeObject } from "../data.js"
|
||||
import {
|
||||
type AstNode,
|
||||
CodeModeFunction,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
ProgramThrow,
|
||||
PromiseInstanceMethodReference,
|
||||
} from "./model.js"
|
||||
import { get, ProgramArray, ProgramObject, record } from "./objects.js"
|
||||
import { HostFunction, requiresNew, sync } from "./host.js"
|
||||
import { caughtErrorValue, normalizeError } from "./errors.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
@@ -111,8 +111,8 @@ export const resolvePromiseValue = <R>(
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (own?.promise !== undefined && value === own.promise) return Effect.fail(selfResolutionError(node))
|
||||
if (value instanceof Values.Promise) return runner.settlePromise(value)
|
||||
if (value === null || typeof value !== "object" || !Object.hasOwn(value, "then")) return Effect.succeed(value)
|
||||
const then = (value as SafeObject).then
|
||||
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
|
||||
const then = get(value, "then")
|
||||
if (typeofValue(then) !== "function") return Effect.succeed(value)
|
||||
|
||||
return Effect.gen(function* () {
|
||||
@@ -174,10 +174,12 @@ const invokePromiseMethod = <R>(
|
||||
}
|
||||
|
||||
if (name === "all") {
|
||||
return yield* settleAfterTurn(
|
||||
Effect.all(
|
||||
items.map((item) => Effect.flatten(promises.await(item))),
|
||||
{ concurrency: "unbounded" },
|
||||
return new ProgramArray(
|
||||
yield* settleAfterTurn(
|
||||
Effect.all(
|
||||
items.map((item) => Effect.flatten(promises.await(item))),
|
||||
{ concurrency: "unbounded" },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -186,19 +188,14 @@ const invokePromiseMethod = <R>(
|
||||
for (const item of items) {
|
||||
const exit = yield* promises.await(item)
|
||||
if (Exit.isSuccess(exit)) {
|
||||
outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }))
|
||||
outcomes.push(record({ status: "fulfilled", value: exit.value }))
|
||||
continue
|
||||
}
|
||||
if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
|
||||
outcomes.push(
|
||||
Object.assign(Object.create(null) as SafeObject, {
|
||||
status: "rejected",
|
||||
reason: caughtErrorValue(Cause.squash(exit.cause)),
|
||||
}),
|
||||
)
|
||||
outcomes.push(record({ status: "rejected", reason: caughtErrorValue(Cause.squash(exit.cause)) }))
|
||||
}
|
||||
yield* Effect.yieldNow
|
||||
return outcomes
|
||||
return new ProgramArray(outcomes)
|
||||
}
|
||||
if (name === "race") {
|
||||
if (items.length === 0) {
|
||||
|
||||
@@ -3,15 +3,14 @@ import { Values } from "../values.js"
|
||||
import { HostFunction, HostNamespace } from "./host.js"
|
||||
import {
|
||||
type AstNode,
|
||||
AsyncIteratorSymbol,
|
||||
CodeModeFunction,
|
||||
CodeModeGenerator,
|
||||
GeneratorMethodReference,
|
||||
InterpreterRuntimeError,
|
||||
IntrinsicReference,
|
||||
IteratorSymbol,
|
||||
PromiseInstanceMethodReference,
|
||||
} from "./model.js"
|
||||
import { getOwn, ownKeys, ProgramArray, ProgramObject } from "./objects.js"
|
||||
|
||||
export const isRuntimeReference = (value: unknown): boolean =>
|
||||
value instanceof HostFunction ||
|
||||
@@ -26,11 +25,8 @@ export const isRuntimeReference = (value: unknown): boolean =>
|
||||
Values.isValue(value)
|
||||
|
||||
function* childValues(value: object): Generator {
|
||||
for (const key of Reflect.ownKeys(value)) {
|
||||
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
|
||||
if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
|
||||
yield Reflect.get(value, key)
|
||||
}
|
||||
if (!(value instanceof ProgramObject)) return
|
||||
for (const key of ownKeys(value)) yield getOwn(value, key)
|
||||
}
|
||||
|
||||
// Depth-first search over a value tree. `match` stops the walk; `skip` prunes a subtree without matching it.
|
||||
@@ -79,7 +75,7 @@ export const rejectCircularInsertion = (
|
||||
|
||||
export const describeValue = (value: unknown): string => {
|
||||
if (value === null) return "null"
|
||||
if (Array.isArray(value)) return "an array"
|
||||
if (value instanceof ProgramArray) return "an array"
|
||||
if (value instanceof Values.Promise) return "an un-awaited Promise"
|
||||
if (value instanceof ToolReference) return "a tool reference"
|
||||
if (value instanceof Values.Date) return "a Date"
|
||||
@@ -108,12 +104,3 @@ export const typeofValue = (value: unknown): string => {
|
||||
if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
|
||||
return typeof value
|
||||
}
|
||||
|
||||
const MAX_ARRAY_LENGTH = 4_294_967_295
|
||||
|
||||
export const parseArrayIndex = (key: string | number): number | undefined => {
|
||||
const property = String(key)
|
||||
if (!/^(0|[1-9]\d*)$/.test(property)) return undefined
|
||||
const index = Number(property)
|
||||
return index < MAX_ARRAY_LENGTH ? index : undefined
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Values } from "../values.js"
|
||||
import { coerceToString } from "../stdlib/value.js"
|
||||
import { HostFunction } from "./host.js"
|
||||
import { type AstNode, CodeModeFunction, InterpreterRuntimeError, IntrinsicReference } from "./model.js"
|
||||
import { get, has, ProgramObject } from "./objects.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
|
||||
export type IteratorCursor<R> = {
|
||||
@@ -42,13 +43,14 @@ export const toPrimitive = <R>(
|
||||
if (Values.isValue(value)) {
|
||||
return Effect.succeed(value instanceof Values.Date && hint === "number" ? value.time : coerceToString(value))
|
||||
}
|
||||
const object = value as Record<string, unknown>
|
||||
if (!(value instanceof ProgramObject)) return Effect.succeed(value)
|
||||
const order = hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"]
|
||||
return Effect.gen(function* () {
|
||||
for (const method of order) {
|
||||
if (method === "toString" && !Object.hasOwn(object, "toString")) return coerceToString(value)
|
||||
if (!Object.hasOwn(object, method) || typeofValue(object[method]) !== "function") continue
|
||||
const result = yield* runner.invokeCallable(object[method], [], node)
|
||||
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 (result === null || (typeof result !== "object" && typeof result !== "function")) return result
|
||||
}
|
||||
throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node).as("TypeError")
|
||||
|
||||
@@ -41,7 +41,7 @@ import type {
|
||||
YieldExpression,
|
||||
} from "acorn"
|
||||
import { Cause, Deferred, Effect, Exit } from "effect"
|
||||
import { ToolRuntimeError, type SafeObject, toProgram } from "../data.js"
|
||||
import { ToolRuntimeError, toProgram } from "../data.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import {
|
||||
type AstNode,
|
||||
@@ -55,9 +55,7 @@ import {
|
||||
type GeneratorRequestKind,
|
||||
IntrinsicReference,
|
||||
InterpreterRuntimeError,
|
||||
isRecord,
|
||||
IteratorSymbol,
|
||||
IteratorSymbols,
|
||||
type MemberReference,
|
||||
OptionalShortCircuit,
|
||||
PromiseInstanceMethodReference,
|
||||
@@ -69,13 +67,24 @@ import { caughtErrorValue } from "./errors.js"
|
||||
import { globals, type Host } from "./globals.js"
|
||||
import { HostFunction, HostNamespace } from "./host.js"
|
||||
import { invokeIntrinsic } from "./methods.js"
|
||||
import {
|
||||
assign,
|
||||
get,
|
||||
has,
|
||||
ownKeys,
|
||||
parseArrayIndex,
|
||||
ProgramArray,
|
||||
ProgramObject,
|
||||
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,
|
||||
parseArrayIndex,
|
||||
rejectCircularInsertion,
|
||||
typeofValue,
|
||||
} from "./references.js"
|
||||
@@ -118,14 +127,11 @@ const calleeDescription = (callee: Expression | Super | undefined): string => {
|
||||
return "The called value"
|
||||
}
|
||||
|
||||
const hasOwn = (value: unknown, key: PropertyKey): boolean =>
|
||||
value !== null && typeof value === "object" && Object.hasOwn(value, key)
|
||||
|
||||
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 (Array.isArray(value)) return "Array"
|
||||
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"
|
||||
@@ -133,7 +139,7 @@ const constructorName = (value: unknown): string | undefined => {
|
||||
if (value instanceof Values.URL) return "URL"
|
||||
if (value instanceof Values.URLSearchParams) return "URLSearchParams"
|
||||
if (value instanceof Values.Promise) return "Promise"
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) return undefined
|
||||
if (!(value instanceof ProgramObject)) return undefined
|
||||
return errorBrandName(value) ?? "Object"
|
||||
}
|
||||
|
||||
@@ -230,7 +236,7 @@ const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...
|
||||
}
|
||||
|
||||
type CustomIterator = {
|
||||
iterator: SafeObject | CodeModeGenerator
|
||||
iterator: ProgramObject | CodeModeGenerator
|
||||
next: unknown
|
||||
asynchronous: boolean
|
||||
}
|
||||
@@ -247,13 +253,6 @@ const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference
|
||||
value instanceof IntrinsicReference ||
|
||||
value instanceof GeneratorMethodReference
|
||||
|
||||
const copyIteratorSymbols = (source: object, target: object, consumed?: ReadonlySet<PropertyKey>): void => {
|
||||
for (const symbol of IteratorSymbols) {
|
||||
if (!consumed?.has(symbol) && Object.hasOwn(source, symbol))
|
||||
Reflect.set(target, symbol, Reflect.get(source, symbol))
|
||||
}
|
||||
}
|
||||
|
||||
type GeneratorRequest = {
|
||||
kind: GeneratorRequestKind
|
||||
value: unknown
|
||||
@@ -723,22 +722,26 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
syncIterator(value: unknown, node: AstNode) {
|
||||
const iterator = Array.isArray(value)
|
||||
? value[Symbol.iterator]()
|
||||
: typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof Values.Map
|
||||
? value.map.entries()
|
||||
: value instanceof Values.Set
|
||||
? value.set.values()
|
||||
: value instanceof Values.URLSearchParams
|
||||
? value.params.entries()
|
||||
: undefined
|
||||
const iterator =
|
||||
value instanceof ProgramArray
|
||||
? value.items[Symbol.iterator]()
|
||||
: typeof value === "string"
|
||||
? value[Symbol.iterator]()
|
||||
: value instanceof Values.Map
|
||||
? value.map.entries()
|
||||
: value instanceof Values.Set
|
||||
? value.set.values()
|
||||
: value instanceof Values.URLSearchParams
|
||||
? value.params.entries()
|
||||
: undefined
|
||||
if (iterator !== undefined) {
|
||||
return Effect.succeed({
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return { done: Boolean(step.done), value: step.value }
|
||||
return {
|
||||
done: Boolean(step.done),
|
||||
value: Array.isArray(step.value) ? new ProgramArray(step.value) : step.value,
|
||||
}
|
||||
}),
|
||||
close: Effect.void,
|
||||
})
|
||||
@@ -763,9 +766,9 @@ class Frame<R> {
|
||||
asynchronous: value.asynchronous,
|
||||
})
|
||||
}
|
||||
if (!isRecord(value) || isRuntimeReference(value)) return Effect.undefined
|
||||
const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined
|
||||
const method = asyncMethod ?? Reflect.get(value, IteratorSymbol)
|
||||
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(
|
||||
@@ -777,7 +780,7 @@ class Frame<R> {
|
||||
next:
|
||||
object instanceof CodeModeGenerator
|
||||
? new GeneratorMethodReference(object, "next")
|
||||
: self.requireIteratorMethod(object.next, "Iterator next", node),
|
||||
: self.requireIteratorMethod(get(object, "next"), "Iterator next", node),
|
||||
asynchronous: asyncMethod !== undefined && asyncMethod !== null,
|
||||
}
|
||||
},
|
||||
@@ -793,7 +796,7 @@ class Frame<R> {
|
||||
"Iterator next() result",
|
||||
node,
|
||||
)
|
||||
return { done: Boolean(object.done), value: object.value }
|
||||
return { done: Boolean(get(object, "done")), value: get(object, "value") }
|
||||
}
|
||||
|
||||
const called = yield* Effect.exit(self.invokeCallable(iterator.next, [], node))
|
||||
@@ -804,7 +807,7 @@ class Frame<R> {
|
||||
const captured = yield* Effect.exit(
|
||||
Effect.sync(() => {
|
||||
const object = self.requireIteratorObject(called.value, "Iterator next() result", node)
|
||||
return { done: Boolean(object.done), value: object.value }
|
||||
return { done: Boolean(get(object, "done")), value: get(object, "value") }
|
||||
}),
|
||||
)
|
||||
if (!Exit.isSuccess(captured)) {
|
||||
@@ -824,7 +827,7 @@ class Frame<R> {
|
||||
const close =
|
||||
iterator.iterator instanceof CodeModeGenerator
|
||||
? new GeneratorMethodReference(iterator.iterator, "return")
|
||||
: iterator.iterator.return
|
||||
: get(iterator.iterator, "return")
|
||||
if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
@@ -844,7 +847,7 @@ class Frame<R> {
|
||||
return yield* Effect.failCause(called.cause)
|
||||
}
|
||||
const captured = yield* Effect.exit(
|
||||
Effect.sync(() => self.requireIteratorObject(called.value, "Iterator return() result", node).value),
|
||||
Effect.sync(() => get(self.requireIteratorObject(called.value, "Iterator return() result", node), "value")),
|
||||
)
|
||||
if (!Exit.isSuccess(captured)) {
|
||||
if (awaiting) yield* Effect.yieldNow
|
||||
@@ -854,12 +857,12 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private requireIteratorObject(value: unknown, context: string, node: AstNode): SafeObject {
|
||||
if (isRecord(value) && !isRuntimeReference(value)) return value
|
||||
private requireIteratorObject(value: unknown, context: string, node: AstNode): ProgramObject {
|
||||
if (value instanceof ProgramObject) return value
|
||||
throw new InterpreterRuntimeError(`${context} must be an object.`, node).as("TypeError")
|
||||
}
|
||||
|
||||
private requireIterator(value: unknown, node: AstNode): SafeObject | CodeModeGenerator {
|
||||
private requireIterator(value: unknown, node: AstNode): ProgramObject | CodeModeGenerator {
|
||||
return value instanceof CodeModeGenerator
|
||||
? value
|
||||
: this.requireIteratorObject(value, "Iterator method result", node)
|
||||
@@ -874,7 +877,7 @@ class Frame<R> {
|
||||
private enumerableKeys(value: unknown, node: AstNode): Array<string> {
|
||||
if (value instanceof ToolReference) return [...this.runtime.toolKeys(value.path)]
|
||||
if (value === null || value === undefined) return []
|
||||
return Object.keys(enumerableSource("for...in", value, node))
|
||||
return ownKeys(enumerableSource("for...in", value, node)).filter((key): key is string => typeof key === "string")
|
||||
}
|
||||
|
||||
private evaluateForInStatement(
|
||||
@@ -1053,7 +1056,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
if (!(value instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
@@ -1064,11 +1067,8 @@ class Frame<R> {
|
||||
const consumed = new Set<PropertyKey>()
|
||||
for (const property of pattern.properties) {
|
||||
if (property.type === "RestElement") {
|
||||
const rest: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, item] of Object.entries(value as SafeObject)) {
|
||||
if (!consumed.has(key)) rest[key] = item
|
||||
}
|
||||
copyIteratorSymbols(value, rest, consumed)
|
||||
const rest = new ProgramObject()
|
||||
assign(rest, value, consumed)
|
||||
yield* self.declarePattern(property.argument, rest, mutable, property, initialize)
|
||||
continue
|
||||
}
|
||||
@@ -1077,7 +1077,7 @@ class Frame<R> {
|
||||
consumed.add(typeof key === "symbol" ? key : String(key))
|
||||
yield* self.declarePattern(
|
||||
property.value,
|
||||
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||
self.destructuringPropertyValue(value, key),
|
||||
mutable,
|
||||
property,
|
||||
initialize,
|
||||
@@ -1116,7 +1116,7 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
if (pattern.type === "ObjectPattern") {
|
||||
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
|
||||
if (!(value instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
|
||||
pattern,
|
||||
@@ -1124,21 +1124,17 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
|
||||
const source = value as SafeObject | Array<unknown>
|
||||
const consumed = new Set<PropertyKey>()
|
||||
for (const property of pattern.properties) {
|
||||
if (property.type === "RestElement") {
|
||||
const rest: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, item] of Object.entries(source)) {
|
||||
if (!consumed.has(key)) rest[key] = item
|
||||
}
|
||||
copyIteratorSymbols(source, rest, consumed)
|
||||
const rest = new ProgramObject()
|
||||
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(source, key), property)
|
||||
yield* self.assignPattern(property.value, self.destructuringPropertyValue(value, key), property)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1172,7 +1168,7 @@ class Frame<R> {
|
||||
if (element === null) continue
|
||||
yield* consume(
|
||||
element.type === "RestElement" ? element.argument : element,
|
||||
element.type === "RestElement" ? [] : undefined,
|
||||
element.type === "RestElement" ? new ProgramArray() : undefined,
|
||||
element,
|
||||
)
|
||||
if (element.type === "RestElement") return
|
||||
@@ -1189,7 +1185,7 @@ class Frame<R> {
|
||||
done = next.done
|
||||
if (!done) rest.push(next.value)
|
||||
}
|
||||
yield* consume(element.argument, rest, element)
|
||||
yield* consume(element.argument, new ProgramArray(rest), element)
|
||||
return
|
||||
}
|
||||
const consumed = consume(element, step.done ? undefined : step.value, pattern)
|
||||
@@ -1212,11 +1208,9 @@ class Frame<R> {
|
||||
throw unsupportedSyntax(keyNode.type, keyNode)
|
||||
}
|
||||
|
||||
private destructuringPropertyValue(source: SafeObject | Array<unknown>, key: PropertyKey): unknown {
|
||||
if (!Array.isArray(source)) return Reflect.get(source, key)
|
||||
if (key === "length") return source.length
|
||||
if (typeof key === "number") return source[key]
|
||||
if (Object.hasOwn(source, key)) return Reflect.get(source, key)
|
||||
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
|
||||
}
|
||||
@@ -1374,11 +1368,10 @@ class Frame<R> {
|
||||
case ">>>":
|
||||
return (l as number) >>> (r as number)
|
||||
case "in":
|
||||
if (rhs === null || typeof rhs !== "object") {
|
||||
if (!(rhs instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
|
||||
}
|
||||
// Never expose properties inherited from host prototypes.
|
||||
return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey)
|
||||
return has(rhs, coerceOperand(lhs) as PropertyKey)
|
||||
default:
|
||||
throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
|
||||
}
|
||||
@@ -1639,7 +1632,13 @@ class Frame<R> {
|
||||
}
|
||||
for (const [index, parameter] of fn.parameters.entries()) {
|
||||
if (parameter.type === "RestElement") {
|
||||
yield* invocation.declarePattern(parameter.argument, args.slice(index), true, parameter, true)
|
||||
yield* invocation.declarePattern(
|
||||
parameter.argument,
|
||||
new ProgramArray(args.slice(index)),
|
||||
true,
|
||||
parameter,
|
||||
true,
|
||||
)
|
||||
break
|
||||
}
|
||||
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
|
||||
@@ -1691,12 +1690,12 @@ class Frame<R> {
|
||||
}
|
||||
if (state.completed) {
|
||||
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
|
||||
return Effect.succeed({ value: kind === "return" ? value : undefined, done: true })
|
||||
return Effect.succeed(record({ value: kind === "return" ? value : undefined, done: true }))
|
||||
}
|
||||
if (!state.started && kind !== "next") {
|
||||
state.completed = true
|
||||
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
|
||||
return Effect.succeed({ value, done: true })
|
||||
return Effect.succeed(record({ value, done: true }))
|
||||
}
|
||||
|
||||
state.pending.push(request)
|
||||
@@ -1726,7 +1725,7 @@ class Frame<R> {
|
||||
if (active) {
|
||||
Deferred.doneUnsafe(
|
||||
active.response,
|
||||
Exit.isSuccess(exit) ? Exit.succeed({ value: exit.value, done: true }) : exit,
|
||||
Exit.isSuccess(exit) ? Exit.succeed(record({ value: exit.value, done: true })) : exit,
|
||||
)
|
||||
}
|
||||
yield* invocation.completeGeneratorRequests(state, asynchronous)
|
||||
@@ -1753,13 +1752,13 @@ class Frame<R> {
|
||||
const resolved = yield* Effect.exit(self.awaitValue(pending.value))
|
||||
Deferred.doneUnsafe(
|
||||
pending.response,
|
||||
Exit.isSuccess(resolved) ? Exit.succeed({ value: resolved.value, done: true }) : resolved,
|
||||
Exit.isSuccess(resolved) ? Exit.succeed(record({ value: resolved.value, done: true })) : resolved,
|
||||
)
|
||||
continue
|
||||
}
|
||||
Deferred.doneUnsafe(
|
||||
pending.response,
|
||||
Exit.succeed({ value: pending.kind === "return" ? pending.value : undefined, done: true }),
|
||||
Exit.succeed(record({ value: pending.kind === "return" ? pending.value : undefined, done: true })),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1804,7 +1803,7 @@ class Frame<R> {
|
||||
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
|
||||
const state = this.generatorState
|
||||
if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node)
|
||||
Deferred.doneUnsafe(state.active.response, Exit.succeed({ value, done: false }))
|
||||
Deferred.doneUnsafe(state.active.response, Exit.succeed(record({ value, done: false })))
|
||||
state.active = undefined
|
||||
return Effect.flatMap(this.takeGeneratorRequest(state), (request) => {
|
||||
state.active = request
|
||||
@@ -1820,7 +1819,7 @@ class Frame<R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (
|
||||
Array.isArray(value) ||
|
||||
value instanceof ProgramArray ||
|
||||
typeof value === "string" ||
|
||||
value instanceof Values.Map ||
|
||||
value instanceof Values.Set ||
|
||||
@@ -1861,7 +1860,7 @@ class Frame<R> {
|
||||
? iterator.next
|
||||
: iterator.iterator instanceof CodeModeGenerator
|
||||
? new GeneratorMethodReference(iterator.iterator, kind)
|
||||
: iterator.iterator[kind]
|
||||
: 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)
|
||||
@@ -1879,11 +1878,11 @@ class Frame<R> {
|
||||
`Iterator ${kind}() result`,
|
||||
node,
|
||||
)
|
||||
const done = Boolean(result.done)
|
||||
const done = Boolean(get(result, "done"))
|
||||
const resultValue: unknown =
|
||||
self.generatorAsync && !iterator.asynchronous
|
||||
? yield* self.awaitAsyncFromSyncValue(iterator, result.value, node, kind !== "return" && !done)
|
||||
: result.value
|
||||
? yield* self.awaitAsyncFromSyncValue(iterator, get(result, "value"), node, kind !== "return" && !done)
|
||||
: get(result, "value")
|
||||
if (done) {
|
||||
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(resultValue))
|
||||
return resultValue
|
||||
@@ -1905,17 +1904,15 @@ class Frame<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<Record<string, unknown>, unknown, R> {
|
||||
const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
|
||||
private evaluateObjectExpression(node: ObjectExpression): Effect.Effect<ProgramObject, unknown, R> {
|
||||
const objectValue = new ProgramObject()
|
||||
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
|
||||
const from = enumerableSource("Object spread", spread, property)
|
||||
for (const [key, value] of Object.entries(from)) objectValue[key] = value
|
||||
if (typeof from === "object") copyIteratorSymbols(from, objectValue)
|
||||
assign(objectValue, enumerableSource("Object spread", spread, property))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1937,14 +1934,14 @@ class Frame<R> {
|
||||
throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode)
|
||||
}
|
||||
|
||||
Reflect.set(objectValue, key, yield* self.evaluateExpression(property.value))
|
||||
set(objectValue, key, yield* self.evaluateExpression(property.value))
|
||||
}
|
||||
|
||||
return objectValue
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<Array<unknown>, unknown, R> {
|
||||
private evaluateArrayExpression(node: ArrayExpression): Effect.Effect<ProgramArray, unknown, R> {
|
||||
const values: Array<unknown> = []
|
||||
|
||||
const self = this
|
||||
@@ -1969,7 +1966,7 @@ class Frame<R> {
|
||||
values.push(yield* self.evaluateExpression(element))
|
||||
}
|
||||
}
|
||||
return values
|
||||
return new ProgramArray(values)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2056,7 +2053,11 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
// Values have no prototype chain, so `.constructor` resolves to the owning built-in directly.
|
||||
if (operation === "read" && key === "constructor" && !hasOwn(objectValue, key)) {
|
||||
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))
|
||||
}
|
||||
@@ -2145,41 +2146,31 @@ class Frame<R> {
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof objectValue !== "object" || objectValue === null) {
|
||||
if (!(objectValue instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode)
|
||||
}
|
||||
|
||||
if (Array.isArray(objectValue)) {
|
||||
if (operation === "delete") return { target: objectValue, key }
|
||||
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
|
||||
if (key !== "length" && !(typeof key === "string" && arrayMethods.has(key)) && index === undefined) {
|
||||
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
|
||||
return new ComputedValue((objectValue as Record<string, unknown> & Array<unknown>)[key])
|
||||
}
|
||||
return new ComputedValue(undefined)
|
||||
}
|
||||
return { target: objectValue, key: index ?? key }
|
||||
}
|
||||
|
||||
return { target: objectValue as SafeObject, key }
|
||||
return { target: objectValue, key }
|
||||
})
|
||||
}
|
||||
|
||||
private readMember(node: MemberExpression): Effect.Effect<unknown, unknown, R> {
|
||||
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
|
||||
if (Array.isArray(reference.target)) {
|
||||
if (reference.key === "length") return reference.target.length
|
||||
if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key)
|
||||
return Reflect.get(reference.target, reference.key)
|
||||
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)
|
||||
}
|
||||
if (reference.target instanceof Values.RegExp) return reference.target.lastIndex
|
||||
if (reference.target instanceof Values.URL) {
|
||||
return Reflect.get(reference.target.url, reference.key)
|
||||
}
|
||||
return Reflect.get(reference.target, reference.key)
|
||||
return value
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2205,7 +2196,7 @@ class Frame<R> {
|
||||
if (reference.target instanceof Values.RegExp) {
|
||||
return Reflect.deleteProperty(reference.target.regex, reference.key)
|
||||
}
|
||||
return Reflect.deleteProperty(reference.target, reference.key)
|
||||
return remove(reference.target, reference.key)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2225,12 +2216,6 @@ class Frame<R> {
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Only data fields may be assigned.", node)
|
||||
}
|
||||
if (Array.isArray(reference.target)) {
|
||||
if (reference.key === "length") throw new InterpreterRuntimeError("Array length cannot be assigned.", node)
|
||||
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
|
||||
throw new InterpreterRuntimeError("Array methods cannot be assigned.", node)
|
||||
}
|
||||
}
|
||||
const key = reference.key
|
||||
const { write, next, result } = yield* compute(self.readReferenceValue(reference, key))
|
||||
if (write) self.assignToReference(reference, key, next, node)
|
||||
@@ -2243,23 +2228,10 @@ class Frame<R> {
|
||||
return Reflect.get(reference.target.url, key)
|
||||
}
|
||||
if (reference.target instanceof Values.RegExp) return reference.target.lastIndex
|
||||
return Reflect.get(reference.target, key)
|
||||
return get(reference.target, key)
|
||||
}
|
||||
|
||||
private assignToReference(reference: MemberReference, key: PropertyKey, next: unknown, node: AstNode): void {
|
||||
if (Array.isArray(reference.target)) {
|
||||
const target = reference.target
|
||||
if (typeof key !== "number" || parseArrayIndex(key) === undefined) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array assignment index must be a valid array index.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
rejectCircularInsertion(target, next, "Array assignment result", node)
|
||||
target[key] = next
|
||||
return
|
||||
}
|
||||
if (reference.target instanceof Values.URL) {
|
||||
const property = key as string
|
||||
if (!urlWritableProperties.has(property)) {
|
||||
@@ -2278,9 +2250,14 @@ class Frame<R> {
|
||||
reference.target.lastIndex = next
|
||||
return
|
||||
}
|
||||
const target = reference.target as SafeObject
|
||||
rejectCircularInsertion(target, next, "Object assignment result", node)
|
||||
Reflect.set(target, key, next)
|
||||
const target = reference.target
|
||||
rejectCircularInsertion(
|
||||
target,
|
||||
next,
|
||||
target instanceof ProgramArray ? "Array assignment result" : "Object assignment result",
|
||||
node,
|
||||
)
|
||||
if (!set(target, key, next)) throw new InterpreterRuntimeError("Invalid array length", node).as("RangeError")
|
||||
}
|
||||
|
||||
private toPropertyKey(value: unknown, node: AstNode): PropertyKey {
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { Effect } from "effect"
|
||||
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, CodeModeGenerator, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { get, ProgramArray, ProgramObject } from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
|
||||
const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> => {
|
||||
if (args.length !== 1) return [...args]
|
||||
const constructArray = (args: Array<unknown>, node: AstNode): ProgramArray => {
|
||||
if (args.length !== 1) return new ProgramArray([...args])
|
||||
const first = args[0]
|
||||
if (typeof first !== "number") return [first]
|
||||
if (typeof first !== "number") return new ProgramArray([first])
|
||||
if (!Number.isInteger(first) || first < 0 || first > 4294967295) {
|
||||
throw new InterpreterRuntimeError("Invalid array length.", node).as("RangeError")
|
||||
}
|
||||
// Sparse like JS: Array(3) has holes, and combinator loops already skip them.
|
||||
return new Array(first)
|
||||
return new ProgramArray(new Array(first))
|
||||
}
|
||||
|
||||
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
|
||||
if (
|
||||
source !== null &&
|
||||
typeof source === "object" &&
|
||||
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||
typeof (source as { length?: unknown }).length === "number"
|
||||
) {
|
||||
const length = (source as { length: number }).length
|
||||
const arrayLikeSource = (
|
||||
source: unknown,
|
||||
node: AstNode,
|
||||
): { readonly length: number; readonly source: ProgramObject } => {
|
||||
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")
|
||||
return { length: normalized, source }
|
||||
@@ -49,16 +48,16 @@ const arrayFrom = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
|
||||
const arrayLike = arrayLikeSource(source, node)
|
||||
const values: Array<unknown> = []
|
||||
for (let index = 0; index < arrayLike.length; index += 1) {
|
||||
const item = Reflect.get(arrayLike.source, index)
|
||||
const item = get(arrayLike.source, index)
|
||||
values.push(apply === undefined ? item : yield* apply([item, index]))
|
||||
}
|
||||
return values
|
||||
return new ProgramArray(values)
|
||||
}
|
||||
const values: Array<unknown> = []
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return values
|
||||
if (step.done) return new ProgramArray(values)
|
||||
values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
|
||||
index += 1
|
||||
}
|
||||
@@ -71,10 +70,10 @@ export const arrayGlobal = <R>(runner: Runner<R>) =>
|
||||
name: "Array",
|
||||
call: syncCall(constructArray),
|
||||
construct: syncCall(constructArray),
|
||||
instanceOf: (value) => Array.isArray(value),
|
||||
instanceOf: (value) => value instanceof ProgramArray,
|
||||
members: {
|
||||
isArray: sync("Array.isArray", (args) => Array.isArray(args[0])),
|
||||
of: sync("Array.of", (args) => [...args]),
|
||||
isArray: sync("Array.isArray", (args) => args[0] instanceof ProgramArray),
|
||||
of: sync("Array.of", (args) => new ProgramArray([...args])),
|
||||
from: new HostFunction<R>({ name: "Array.from", call: (args, node) => arrayFrom(runner, args, node) }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import type { SafeObject } from "../data.js"
|
||||
import { HostFunction, requiresNew } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { getOwn, ProgramArray, ProgramObject, set } 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"
|
||||
@@ -108,13 +108,13 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
|
||||
const item = step.value
|
||||
const key = yield* preserveConsumerError(cursor, apply([item, index]))
|
||||
const group = result.map.get(key)
|
||||
if (group === undefined) result.map.set(key, [item])
|
||||
else (group as Array<unknown>).push(item)
|
||||
if (group === undefined) result.map.set(key, new ProgramArray([item]))
|
||||
else (group as ProgramArray).items.push(item)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
const result: SafeObject = Object.create(null) as SafeObject
|
||||
const result = new ProgramObject()
|
||||
let index = 0
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
@@ -124,9 +124,9 @@ export const groupBy = <R>(runner: Runner<R>, namespace: "Map" | "Object") =>
|
||||
cursor,
|
||||
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)),
|
||||
)
|
||||
const group = result[key]
|
||||
if (group === undefined) result[key] = [item]
|
||||
else (group as Array<unknown>).push(item)
|
||||
const group = getOwn(result, key)
|
||||
if (group === undefined) set(result, key, new ProgramArray([item]))
|
||||
else (group as ProgramArray).items.push(item)
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
@@ -150,12 +150,12 @@ const constructMap = <R>(runner: Runner<R>, init: unknown, node: AstNode) => {
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (!isRecord(step.value) || isRuntimeReference(step.value)) {
|
||||
if (!(step.value instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs as entry objects.", node).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
target.map.set(step.value[0], step.value[1])
|
||||
target.map.set(getOwn(step.value, 0), getOwn(step.value, 1))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toData, toProgram } from "../data.js"
|
||||
import { HostNamespace, sync } from "../interpreter/host.js"
|
||||
import { get, ownEntries, ProgramArray, ProgramObject } from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js"
|
||||
import { Values } from "../values.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
@@ -51,8 +52,8 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
if (value instanceof Values.Map) {
|
||||
seen.add(value)
|
||||
try {
|
||||
const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
|
||||
return `Map(${value.map.size}) ${formatConsoleValue(entries, seen, depth + 1)}`
|
||||
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)}`
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
}
|
||||
@@ -60,7 +61,7 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
if (value instanceof Values.Set) {
|
||||
seen.add(value)
|
||||
try {
|
||||
return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
|
||||
return `Set(${value.set.size}) ${formatConsoleValue(new ProgramArray([...value.set.values()]), seen, depth + 1)}`
|
||||
} finally {
|
||||
seen.delete(value)
|
||||
}
|
||||
@@ -68,10 +69,11 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
|
||||
if (isRuntimeReference(value)) return "[opaque reference]"
|
||||
seen.add(value)
|
||||
try {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]`
|
||||
if (value instanceof ProgramArray) {
|
||||
return `[${value.items.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]`
|
||||
}
|
||||
return `{${Object.entries(value)
|
||||
if (!(value instanceof ProgramObject)) return "[object Object]"
|
||||
return `{${ownEntries(value)
|
||||
.map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
|
||||
.join(",")}}`
|
||||
} finally {
|
||||
@@ -104,20 +106,19 @@ const consoleTableRows = (
|
||||
data: unknown,
|
||||
columns: ReadonlyArray<string> | undefined,
|
||||
): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
|
||||
if (data instanceof ProgramArray) {
|
||||
return data.items.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
|
||||
}
|
||||
if (data !== null && typeof data === "object" && !Values.isValue(data)) {
|
||||
return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
|
||||
if (data instanceof ProgramObject) {
|
||||
return ownEntries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
|
||||
}
|
||||
return [{ index: "0", values: { Value: data } }]
|
||||
}
|
||||
|
||||
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
|
||||
if (value !== null && typeof value === "object" && !Array.isArray(value) && !Values.isValue(value)) {
|
||||
const source = value as Record<string, unknown>
|
||||
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
|
||||
return Object.fromEntries(Object.entries(source))
|
||||
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 { Value: value }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { HostFunction, HostNamespace } from "../interpreter/host.js"
|
||||
import { applyCollectionCallback, type Runner } from "../interpreter/runner.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { fromData, type SafeObject, toData, toProgram } from "../data.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"
|
||||
|
||||
export const jsonGlobal = <R>(runner: Runner<R>) =>
|
||||
@@ -29,28 +30,19 @@ const parse = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): Effec
|
||||
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
|
||||
|
||||
const apply = applyCollectionCallback(runner, args[1], "JSON.parse", node)
|
||||
const root: SafeObject = Object.create(null) as SafeObject
|
||||
root[""] = parsed
|
||||
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = holder[key as keyof typeof holder]
|
||||
if (Array.isArray(value)) {
|
||||
const length = value.length
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const revived = yield* visit(value, String(index))
|
||||
if (revived === undefined) Reflect.deleteProperty(value, index)
|
||||
else value[index] = revived
|
||||
}
|
||||
} else if (isPlainObject(value)) {
|
||||
for (const name of Object.keys(value)) {
|
||||
const revived = yield* visit(value, name)
|
||||
if (revived === undefined) Reflect.deleteProperty(value, name)
|
||||
else value[name] = revived
|
||||
const value = get(holder, key)
|
||||
if (value instanceof ProgramObject) {
|
||||
for (const name of ownKeys(value)) {
|
||||
const revived = yield* visit(value, name as string)
|
||||
if (revived === undefined) remove(value, name)
|
||||
else set(value, name, revived)
|
||||
}
|
||||
}
|
||||
return yield* apply([key, value])
|
||||
})
|
||||
return visit(root, "")
|
||||
return visit(record({ "": parsed }), "")
|
||||
}
|
||||
|
||||
const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> => {
|
||||
@@ -59,44 +51,41 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
|
||||
const replacer = args[1]
|
||||
|
||||
if (typeofValue(replacer) !== "function") {
|
||||
const properties = Array.isArray(replacer)
|
||||
? replacer
|
||||
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
|
||||
.map(String)
|
||||
: null
|
||||
const properties =
|
||||
replacer instanceof ProgramArray
|
||||
? replacer.items
|
||||
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
|
||||
.map(String)
|
||||
: null
|
||||
return Effect.succeed(JSON.stringify(toData(args[0], "JSON.stringify value"), properties, indent))
|
||||
}
|
||||
|
||||
// Validate up front; the replacer walk below reads the original value.
|
||||
toProgram(args[0], "JSON.stringify value")
|
||||
const apply = applyCollectionCallback(runner, replacer, "JSON.stringify", node)
|
||||
const root: SafeObject = Object.create(null) as SafeObject
|
||||
root[""] = args[0]
|
||||
const stack = new Set<object>()
|
||||
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
const visit = (holder: ProgramObject, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = yield* apply([key, toJSONValue(holder[key as keyof typeof holder])])
|
||||
const value = yield* apply([key, toJSONValue(get(holder, key))])
|
||||
if (value === undefined || typeofValue(value) === "function") return undefined
|
||||
toProgram(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 (Array.isArray(value)) {
|
||||
if (stack.has(value))
|
||||
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
|
||||
stack.add(value)
|
||||
if (!(value instanceof ProgramObject)) return {}
|
||||
if (stack.has(value))
|
||||
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
|
||||
stack.add(value)
|
||||
if (value instanceof ProgramArray) {
|
||||
const result: Array<unknown> = []
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
for (let index = 0; index < value.items.length; index += 1) {
|
||||
result.push((yield* visit(value, String(index))) ?? null)
|
||||
}
|
||||
stack.delete(value)
|
||||
return result
|
||||
}
|
||||
if (!isPlainObject(value)) return {}
|
||||
if (stack.has(value))
|
||||
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
|
||||
stack.add(value)
|
||||
const result: SafeObject = Object.create(null) as SafeObject
|
||||
for (const name of Object.keys(value)) {
|
||||
const result: Record<string, unknown> = Object.create(null)
|
||||
for (const name of ownKeys(value)) {
|
||||
if (typeof name !== "string") continue
|
||||
const item = yield* visit(value, name)
|
||||
if (item !== undefined) result[name] = item
|
||||
}
|
||||
@@ -104,7 +93,7 @@ const stringify = <R>(runner: Runner<R>, args: Array<unknown>, node: AstNode): E
|
||||
return result
|
||||
})
|
||||
|
||||
return Effect.map(visit(root, ""), (value) => JSON.stringify(value, null, indent))
|
||||
return Effect.map(visit(record({ "": args[0] }), ""), (value) => JSON.stringify(value, null, indent))
|
||||
}
|
||||
|
||||
const toJSONValue = (value: unknown): unknown => {
|
||||
@@ -114,6 +103,3 @@ const toJSONValue = (value: unknown): unknown => {
|
||||
if (value instanceof Values.URL) return value.url.href
|
||||
return value
|
||||
}
|
||||
|
||||
const isPlainObject = (value: unknown): value is SafeObject =>
|
||||
value !== null && typeof value === "object" && !Values.isValue(value)
|
||||
|
||||
@@ -2,11 +2,10 @@ import { Effect } from "effect"
|
||||
import { toProgram } from "../data.js"
|
||||
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
|
||||
import { getOwn, hasOwn, ownEntries, ownKeys, ProgramArray, ProgramObject, set } from "../interpreter/objects.js"
|
||||
import {
|
||||
containsOpaqueReference,
|
||||
describeValue,
|
||||
isRuntimeReference,
|
||||
parseArrayIndex,
|
||||
rejectCircularInsertion,
|
||||
typeofValue,
|
||||
} from "../interpreter/references.js"
|
||||
@@ -16,9 +15,8 @@ import { Values } from "../values.js"
|
||||
import { groupBy } from "./collections.js"
|
||||
import { coerceToString } from "./value.js"
|
||||
|
||||
// ToObject for enumeration. Strings return themselves: the host's Object.keys/entries/hasOwn index a
|
||||
// primitive string directly. Numbers, booleans, wrappers, and functions have no own enumerable keys.
|
||||
export const enumerableSource = (label: string, value: unknown, node: AstNode): Record<string, unknown> => {
|
||||
// ToObject for enumeration.
|
||||
export const enumerableSource = (label: string, value: unknown, node: AstNode): ProgramObject => {
|
||||
if (value === null || value === undefined) {
|
||||
throw new InterpreterRuntimeError(`${label} cannot convert ${describeValue(value)} to an object.`, node).as(
|
||||
"TypeError",
|
||||
@@ -38,58 +36,41 @@ export const enumerableSource = (label: string, value: unknown, node: AstNode):
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (typeof value === "string") return value as unknown as Record<string, unknown>
|
||||
if (typeof value !== "object" || Values.isValue(value) || isRuntimeReference(value)) return {}
|
||||
return value as Record<string, unknown>
|
||||
if (typeof value === "string") return new ProgramArray([...value])
|
||||
if (value instanceof ProgramObject) return value
|
||||
return new ProgramObject()
|
||||
}
|
||||
|
||||
export const objectAssign = (args: Array<unknown>, node: AstNode): unknown => {
|
||||
const target = args[0]
|
||||
// JS would box a primitive target; wrappers and primitives cannot hold fields here.
|
||||
if (target === null || typeof target !== "object" || Values.isValue(target) || isRuntimeReference(target)) {
|
||||
if (!(target instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.assign expects a data object or array target, received ${describeValue(target)}.`,
|
||||
node,
|
||||
).as("TypeError")
|
||||
}
|
||||
const out = target as Record<string, unknown>
|
||||
const seen = new Set<object>()
|
||||
const guardedSet = (key: PropertyKey, item: unknown): void => {
|
||||
// Arrays hold only indexed elements, as with direct assignment; Reflect.set would otherwise
|
||||
// reach Array's length and Object.prototype's __proto__ setter.
|
||||
if (Array.isArray(out) && (typeof key === "symbol" || parseArrayIndex(key) === undefined)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`Object.assign cannot assign '${String(key)}' to an array: only array indexes may be assigned.`,
|
||||
node,
|
||||
).as("TypeError")
|
||||
}
|
||||
rejectCircularInsertion(out, item, "Object.assign result", node, seen)
|
||||
if (!Reflect.set(out, key, item))
|
||||
throw new InterpreterRuntimeError(`Object.assign could not assign property '${String(key)}'.`, node).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
for (const source of args.slice(1)) {
|
||||
if (source === null || source === undefined) continue
|
||||
const from = enumerableSource("Object.assign(...)", source, node)
|
||||
if (typeof from !== "object") {
|
||||
for (const [key, item] of Object.entries(from)) guardedSet(key, item)
|
||||
continue
|
||||
}
|
||||
for (const key of Reflect.ownKeys(from)) {
|
||||
for (const key of ownKeys(from)) {
|
||||
if (typeof key === "symbol" && key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
|
||||
if (Object.prototype.propertyIsEnumerable.call(from, key)) guardedSet(key, Reflect.get(from, key))
|
||||
rejectCircularInsertion(target, getOwn(from, key), "Object.assign result", node, seen)
|
||||
if (!set(target, key, getOwn(from, key))) {
|
||||
throw new InterpreterRuntimeError("Invalid array length", node).as("RangeError")
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
return target
|
||||
}
|
||||
|
||||
const objectFromEntries = <R>(
|
||||
runner: Runner<R>,
|
||||
source: unknown,
|
||||
node: AstNode,
|
||||
): Effect.Effect<Record<string, unknown>, unknown, R> => {
|
||||
const out: Record<string, unknown> = Object.create(null)
|
||||
): Effect.Effect<ProgramObject, unknown, R> => {
|
||||
const out = new ProgramObject()
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source, node)
|
||||
if (cursor === undefined) {
|
||||
@@ -103,21 +84,12 @@ const objectFromEntries = <R>(
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (
|
||||
step.value === null ||
|
||||
typeof step.value !== "object" ||
|
||||
Values.isValue(step.value) ||
|
||||
containsOpaqueReference(step.value)
|
||||
) {
|
||||
if (!(step.value instanceof ProgramObject) || containsOpaqueReference(step.value)) {
|
||||
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
const entry = step.value as Record<string, unknown>
|
||||
toProgram(entry[0], "Object.fromEntries key")
|
||||
toProgram(entry[1], "Object.fromEntries value")
|
||||
const key = coerceToString(entry[0])
|
||||
out[key] = entry[1]
|
||||
set(out, coerceToString(getOwn(step.value, 0)), getOwn(step.value, 1))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -126,7 +98,7 @@ const objectFromEntries = <R>(
|
||||
|
||||
const constructObject = (args: Array<unknown>, node: AstNode): unknown => {
|
||||
const first = args[0]
|
||||
if (first === null || first === undefined) return Object.create(null)
|
||||
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.`,
|
||||
@@ -147,18 +119,24 @@ export const objectGlobal = <R>(runner: Runner<R>, toolKeys: (path: ReadonlyArra
|
||||
toProgram(
|
||||
args[0] instanceof ToolReference
|
||||
? [...toolKeys(args[0].path)]
|
||||
: Object.keys(enumerableSource("Object.keys(...)", args[0], node)),
|
||||
: ownKeys(enumerableSource("Object.keys(...)", args[0], node)).filter((key) => typeof key === "string"),
|
||||
"Object.keys result",
|
||||
),
|
||||
),
|
||||
values: sync("Object.values", (args, node) =>
|
||||
Object.values(enumerableSource("Object.values(...)", args[0], node)),
|
||||
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) =>
|
||||
Object.entries(enumerableSource("Object.entries(...)", args[0], node)).map(([key, item]) => [key, item]),
|
||||
entries: sync(
|
||||
"Object.entries",
|
||||
(args, node) =>
|
||||
new ProgramArray(
|
||||
ownEntries(enumerableSource("Object.entries(...)", args[0], node)).map((entry) => new ProgramArray(entry)),
|
||||
),
|
||||
),
|
||||
hasOwn: sync("Object.hasOwn", (args, node) =>
|
||||
Object.hasOwn(
|
||||
hasOwn(
|
||||
enumerableSource("Object.hasOwn(...)", args[0], node),
|
||||
args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]),
|
||||
),
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import type { SafeObject } from "../data.js"
|
||||
import { ProgramArray, record, set } from "../interpreter/objects.js"
|
||||
import { Values } from "../values.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
|
||||
type MatchValue = Array<unknown> & {
|
||||
index?: number
|
||||
groups?: SafeObject
|
||||
indices?: IndicesValue
|
||||
}
|
||||
|
||||
type IndicesValue = Array<unknown> & {
|
||||
groups?: SafeObject
|
||||
}
|
||||
|
||||
export const regexpMethods = new Set(["test", "exec", "toString"])
|
||||
|
||||
export const regexpProperties = new Set([
|
||||
@@ -56,17 +46,12 @@ export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFl
|
||||
)
|
||||
}
|
||||
|
||||
export const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
|
||||
const result: MatchValue = Array.from(match, (group) => group)
|
||||
if (match.index !== undefined) result.index = match.index
|
||||
if (match.groups) {
|
||||
const groups: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, group] of Object.entries(match.groups)) {
|
||||
groups[key] = group
|
||||
}
|
||||
result.groups = groups
|
||||
}
|
||||
if (match.indices) result.indices = indicesToValue(match.indices)
|
||||
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))
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -143,16 +128,16 @@ const toLength = (value: unknown): number => {
|
||||
return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
const indicesToValue = (indices: RegExpIndicesArray): IndicesValue => {
|
||||
const result: IndicesValue = Array.from(indices, (range) => (range === undefined ? undefined : [...range]))
|
||||
if (indices.groups) {
|
||||
const groups: SafeObject = Object.create(null) as SafeObject
|
||||
for (const [key, range] of Object.entries(indices.groups)) {
|
||||
groups[key] = range === undefined ? undefined : [...range]
|
||||
}
|
||||
result.groups = groups
|
||||
return result
|
||||
}
|
||||
result.groups = undefined
|
||||
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 groups = indices.groups
|
||||
set(
|
||||
result,
|
||||
"groups",
|
||||
groups === undefined
|
||||
? undefined
|
||||
: record(Object.fromEntries(Object.entries(groups).map(([key, pair]) => [key, range(pair)]))),
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Effect } from "effect"
|
||||
import { toProgram } from "../data.js"
|
||||
import { HostFunction, requiresNew, sync, syncCall } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { ownEntries, ProgramObject } from "../interpreter/objects.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
||||
import { Values } from "../values.js"
|
||||
@@ -183,15 +184,14 @@ const constructURLSearchParams = <R>(
|
||||
).as("TypeError")
|
||||
}
|
||||
if (Values.isValue(init)) return new Values.URLSearchParams(new URLSearchParams())
|
||||
const data = toProgram(init, "new URLSearchParams input")
|
||||
if (data === null || typeof data !== "object") {
|
||||
if (!(init instanceof ProgramObject)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
|
||||
node,
|
||||
).as("TypeError")
|
||||
}
|
||||
return new Values.URLSearchParams(
|
||||
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
|
||||
new URLSearchParams(Object.fromEntries(ownEntries(init).map(([key, value]) => [key, coerceToString(value)]))),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type HostFunction, sync, type SyncOptions } from "../interpreter/host.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { type SafeObject, toProgram } from "../data.js"
|
||||
import { toProgram } from "../data.js"
|
||||
import { get, ProgramArray, ProgramError, set } from "../interpreter/objects.js"
|
||||
import { Values } from "../values.js"
|
||||
|
||||
export const errorConstructors = new Set([
|
||||
@@ -16,21 +17,21 @@ export const errorConstructors = new Set([
|
||||
|
||||
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
|
||||
|
||||
const ErrorBrand: unique symbol = Symbol("codemode.error")
|
||||
|
||||
export const createErrorValue = (name: string, message: string): SafeObject => {
|
||||
const value = Object.assign(Object.create(null) as SafeObject, { name, message })
|
||||
Object.defineProperty(value, ErrorBrand, { value: name })
|
||||
export const createErrorValue = (name: string, message: string): ProgramError => {
|
||||
const value = new ProgramError(name)
|
||||
set(value, "name", name)
|
||||
set(value, "message", message)
|
||||
return value
|
||||
}
|
||||
|
||||
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): SafeObject =>
|
||||
Object.assign(createErrorValue("AggregateError", message), { errors })
|
||||
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): ProgramError => {
|
||||
const value = createErrorValue("AggregateError", message)
|
||||
set(value, "errors", new ProgramArray(errors))
|
||||
return value
|
||||
}
|
||||
|
||||
export const errorBrandName = (value: unknown): string | undefined =>
|
||||
value !== null && typeof value === "object"
|
||||
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
|
||||
: undefined
|
||||
value instanceof ProgramError ? value.errorName : undefined
|
||||
|
||||
export const coerceToString = (value: unknown): string => {
|
||||
if (value === null) return "null"
|
||||
@@ -42,29 +43,27 @@ export const coerceToString = (value: unknown): string => {
|
||||
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 (errorBrandName(value) !== undefined) {
|
||||
if (value instanceof ProgramError) {
|
||||
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
|
||||
const error = value as { name?: unknown; message?: unknown }
|
||||
const name = typeof error.name === "string" ? error.name : "Error"
|
||||
const message = typeof error.message === "string" ? error.message : ""
|
||||
if (message === "") return name
|
||||
if (name === "") return message
|
||||
return `${name}: ${message}`
|
||||
const name = get(value, "name")
|
||||
const message = get(value, "message")
|
||||
const shownName = typeof name === "string" ? name : "Error"
|
||||
const shownMessage = typeof message === "string" ? message : ""
|
||||
if (shownMessage === "") return shownName
|
||||
if (shownName === "") return shownMessage
|
||||
return `${shownName}: ${shownMessage}`
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
: "[object Object]"
|
||||
if (value instanceof ProgramArray) {
|
||||
return value.items.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
}
|
||||
if (typeof value === "object") return "[object Object]"
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export const coerceToNumber = (value: unknown): number => {
|
||||
if (value instanceof Values.Date) return value.time
|
||||
if (Values.isValue(value)) return Number.NaN
|
||||
// Arrays coerce through our own string coercion: host Number(array) joins with host
|
||||
// ToPrimitive, which throws on the null-prototype objects the interpreter produces.
|
||||
if (Array.isArray(value)) return Number(coerceToString(value))
|
||||
if (value instanceof ProgramArray) return Number(coerceToString(value))
|
||||
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
|
||||
}
|
||||
|
||||
@@ -78,8 +77,6 @@ const coerce = (name: Coercion, args: Array<unknown>, node: AstNode): unknown =>
|
||||
if (name === "String") return ""
|
||||
}
|
||||
const raw = args[0]
|
||||
// Error values are plain SafeObjects; the toProgram path below would strip their brand.
|
||||
if (name === "String" && errorBrandName(raw) !== undefined) return coerceToString(raw)
|
||||
if (Values.isValue(raw)) {
|
||||
if (name === "Boolean") return true
|
||||
if (name === "Number") return coerceToNumber(raw)
|
||||
|
||||
@@ -167,17 +167,14 @@ describe("Object.assign Test262 parity", () => {
|
||||
).toEqual([true, [1, 8, 9], true, [1, 8, 3], true, 5, true, 0])
|
||||
})
|
||||
|
||||
test("array targets accept only array indexes (deviation from target-Array.js)", async () => {
|
||||
test("test/built-ins/Object/assign/target-Array.js", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const target = [7]
|
||||
const out = []
|
||||
for (const source of [{ length: 0 }, { x: 1 }, { "1.5": 1 }, { "-0": 1 }, { ["__proto__"]: null }]) {
|
||||
try { Object.assign(target, source) } catch (error) { out.push(error.name) }
|
||||
}
|
||||
return [out, [...target], target.length, Object.keys(target)]
|
||||
for (const source of [{ x: 1 }, { "1.5": 1 }, { "-0": 1 }, { 1: 8 }, { length: 1 }]) Object.assign(target, source)
|
||||
return [[...target], target.length, Object.keys(target), target.x]
|
||||
`),
|
||||
).toEqual([["TypeError", "TypeError", "TypeError", "TypeError", "TypeError"], [7], 1, ["0"]])
|
||||
).toEqual([[7], 1, ["0", "x", "1.5", "-0"], 1])
|
||||
})
|
||||
|
||||
test("test/built-ins/Object/assign/Target-{Null,Undefined}.js", async () => {
|
||||
|
||||
@@ -81,27 +81,26 @@ describe("H3: array property access reads as undefined (not a throw)", () => {
|
||||
).toEqual(["b", "b", null, null, "a", null])
|
||||
})
|
||||
|
||||
test("noncanonical keys cannot mutate or delete an aliased element", async () => {
|
||||
test("noncanonical keys are ordinary properties that never alias an element", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = ["a", "b"]
|
||||
let writes = 0
|
||||
try { values["01"] = ++writes } catch {}
|
||||
values["01"] = "c"
|
||||
const before = [values["01"], values[1], values.length]
|
||||
const removed = delete values["01"]
|
||||
return [writes, removed, values]
|
||||
return [before, removed, values["01"], values]
|
||||
`),
|
||||
).toEqual([0, true, ["a", "b"]])
|
||||
).toEqual([["c", "b", 2], true, null, ["a", "b"]])
|
||||
})
|
||||
|
||||
test("the maximum array length is not accepted as an array index", async () => {
|
||||
test("the maximum array length is a property, not an index", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = []
|
||||
let writes = 0
|
||||
try { values["4294967295"] = ++writes } catch {}
|
||||
return [writes, values.length]
|
||||
values["4294967295"] = 1
|
||||
return [values["4294967295"], values.length]
|
||||
`),
|
||||
).toEqual([0, 0])
|
||||
).toEqual([1, 0])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -246,16 +245,14 @@ describe("property deletion", () => {
|
||||
expect(await value(`const values = [1, 2]; return [delete values.length, values.length]`)).toEqual([false, 2])
|
||||
})
|
||||
|
||||
test("does not broaden unsupported array property assignment", async () => {
|
||||
test("arrays accept named properties like JS, and they stay out of the JSON form", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const values = []
|
||||
let rightHandSideRuns = 0
|
||||
function next() { rightHandSideRuns++; return 1 }
|
||||
try { values.field = next() } catch {}
|
||||
return rightHandSideRuns
|
||||
const values = [1]
|
||||
values.field = 2
|
||||
return [values.field, Object.keys(values), values]
|
||||
`),
|
||||
).toBe(0)
|
||||
).toEqual([2, ["0", "field"], [1]])
|
||||
})
|
||||
|
||||
test("optional deletion short-circuits without evaluating the key", async () => {
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { CodeMode, Tool } from "../src/index.js"
|
||||
import { AsyncIteratorSymbol, IteratorSymbol } from "../src/interpreter/model.js"
|
||||
import { objectAssign } from "../src/stdlib/object.js"
|
||||
|
||||
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
|
||||
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
|
||||
@@ -770,13 +768,18 @@ describe("stdlib integration", () => {
|
||||
],
|
||||
])
|
||||
expect(await value(`const match = /a/.exec("ba"); return [Object.values(match), Object.entries(match)]`)).toEqual([
|
||||
["a", 1],
|
||||
["a", 1, "ba"],
|
||||
[
|
||||
["0", "a"],
|
||||
["index", 1],
|
||||
["input", "ba"],
|
||||
],
|
||||
])
|
||||
expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual(["0", "index"])
|
||||
expect(await value(`return Object.keys(Object.values({ match: /a/.exec("ba") })[0])`)).toEqual([
|
||||
"0",
|
||||
"index",
|
||||
"input",
|
||||
])
|
||||
})
|
||||
|
||||
test("Object.fromEntries accepts every supported entry collection", async () => {
|
||||
@@ -849,86 +852,6 @@ describe("stdlib integration", () => {
|
||||
expect(await value(`try { Object.assign(null, { a: 1 }); return false } catch { return true }`)).toBe(true)
|
||||
})
|
||||
|
||||
test("Object.assign ignores non-enumerable supported symbols without reading them", () => {
|
||||
const target = {}
|
||||
const reads: Array<boolean> = []
|
||||
const source = Object.defineProperty({}, IteratorSymbol, {
|
||||
get() {
|
||||
reads.push(true)
|
||||
return target
|
||||
},
|
||||
})
|
||||
expect(objectAssign([target, source], { type: "CallExpression", start: 0, end: 0 })).toBe(target)
|
||||
expect(reads).toEqual([])
|
||||
expect(Object.hasOwn(target, IteratorSymbol)).toBe(false)
|
||||
})
|
||||
|
||||
test("Object.assign ignores nested non-enumerable supported symbols during cycle checks", () => {
|
||||
const target = {}
|
||||
const reads: Array<boolean> = []
|
||||
const nested = Object.defineProperty({}, IteratorSymbol, {
|
||||
get() {
|
||||
reads.push(true)
|
||||
return target
|
||||
},
|
||||
})
|
||||
expect(objectAssign([target, { nested }], { type: "CallExpression", start: 0, end: 0 })).toBe(target)
|
||||
expect(reads).toEqual([])
|
||||
expect(target).toEqual({ nested })
|
||||
})
|
||||
|
||||
test("Object.assign rejects cycles through supported symbols on nested arrays", () => {
|
||||
const target = {}
|
||||
const nested = Object.defineProperty([], IteratorSymbol, { enumerable: true, value: target })
|
||||
expect(() => objectAssign([target, { nested }], { type: "CallExpression", start: 0, end: 0 })).toThrow(
|
||||
"Object.assign result contains a circular value.",
|
||||
)
|
||||
expect(Object.hasOwn(target, "nested")).toBe(false)
|
||||
})
|
||||
|
||||
test("Object.assign cycle checks traverse sparse keys lazily", () => {
|
||||
const target = {}
|
||||
const reads: Array<boolean> = []
|
||||
const nested = Object.defineProperties([], {
|
||||
4294967294: { enumerable: true, value: target },
|
||||
later: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
reads.push(true)
|
||||
return null
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(() => objectAssign([target, { nested }], { type: "CallExpression", start: 0, end: 0 })).toThrow(
|
||||
"Object.assign result contains a circular value.",
|
||||
)
|
||||
expect(reads).toEqual([])
|
||||
})
|
||||
|
||||
test("Object.assign stops after a supported symbol write fails", () => {
|
||||
const previous = () => ({ done: true })
|
||||
const target = Object.defineProperty({}, IteratorSymbol, { value: previous })
|
||||
const reads: Array<boolean> = []
|
||||
const source = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
[IteratorSymbol]: { enumerable: true, value: () => ({ done: false }) },
|
||||
[AsyncIteratorSymbol]: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
reads.push(true)
|
||||
return () => ({ done: true })
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
expect(() => objectAssign([target, source], { type: "CallExpression", start: 0, end: 0 })).toThrow(
|
||||
"Object.assign could not assign property",
|
||||
)
|
||||
expect(Reflect.get(target, IteratorSymbol)).toBe(previous)
|
||||
expect(reads).toEqual([])
|
||||
})
|
||||
|
||||
test("Object.assign rejects direct and nested cycles", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
@@ -1000,27 +923,6 @@ describe("stdlib integration", () => {
|
||||
).toEqual([true, true, true, 2])
|
||||
})
|
||||
|
||||
test("Object.assign traverses shared aliases once", () => {
|
||||
const reads: Array<boolean> = []
|
||||
const shared = Object.defineProperty({}, "value", {
|
||||
enumerable: true,
|
||||
get() {
|
||||
reads.push(true)
|
||||
return 1
|
||||
},
|
||||
})
|
||||
const target = {}
|
||||
expect(
|
||||
objectAssign([target, { left: shared, right: shared }], {
|
||||
type: "CallExpression",
|
||||
start: 0,
|
||||
end: 0,
|
||||
}),
|
||||
).toBe(target)
|
||||
expect(target).toEqual({ left: shared, right: shared })
|
||||
expect(reads).toEqual([true])
|
||||
})
|
||||
|
||||
test("assignment resolves and reads its left side before evaluating the right side", async () => {
|
||||
expect(await value(`let x = 1; x += (x = 5); return x`)).toBe(6)
|
||||
expect(await value(`let i = 0; const values = [9]; values[i++] = i; return [values, i]`)).toEqual([[1], 1])
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 { get, ProgramArray, ProgramObject } from "../../src/interpreter/objects.js"
|
||||
import { createErrorValue, errorBrandName } from "../../src/stdlib/value.js"
|
||||
import { ToolRuntime } from "../../src/tool-runtime.js"
|
||||
|
||||
@@ -67,7 +68,10 @@ const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyAr
|
||||
const fail = (message: string) => Effect.fail(new ProgramThrow(createErrorValue("Test262Error", message)))
|
||||
const prefix = (message: unknown) => (message === undefined ? "" : `${String(message)} `)
|
||||
const compare = (a: unknown, b: unknown) =>
|
||||
Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, i) => Object.is(value, b[i]))
|
||||
a instanceof ProgramArray &&
|
||||
b instanceof ProgramArray &&
|
||||
a.items.length === b.items.length &&
|
||||
a.items.every((value, i) => Object.is(value, b.items[i]))
|
||||
const test262Error = new HostFunction<R>({
|
||||
name: "Test262Error",
|
||||
call: (args) => Effect.succeed(createErrorValue("Test262Error", args[0] === undefined ? "" : String(args[0]))),
|
||||
@@ -154,9 +158,9 @@ const harness = <R>(host: Host<R>, onDone: (error: unknown) => void): ReadonlyAr
|
||||
const show = (value: unknown): string => {
|
||||
if (typeof value === "string") return JSON.stringify(value)
|
||||
if (Object.is(value, -0)) return "-0"
|
||||
if (Array.isArray(value)) return `[${value.map(show).join(", ")}]`
|
||||
if (value instanceof ProgramArray) return `[${value.items.map(show).join(", ")}]`
|
||||
if (value instanceof HostFunction) return value.name
|
||||
if (value === null || typeof value !== "object") return String(value)
|
||||
const message = (value as { message?: unknown }).message
|
||||
if (!(value instanceof ProgramObject)) return String(value)
|
||||
const message = get(value, "message")
|
||||
return typeof message === "string" ? `${errorBrandName(value) ?? "object"}: ${message}` : "object"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
built-ins/Array/prototype/concat/create-ctor-non-object.js # Only data fields may be assigned.
|
||||
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 length cannot be assigned.
|
||||
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.
|
||||
@@ -9,18 +9,15 @@ built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a T
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a Error
|
||||
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-4.js # Array length cannot be assigned.
|
||||
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-8-13.js # Only data fields may be assigned.
|
||||
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/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 Error
|
||||
built-ins/Array/prototype/fill/return-abrupt-from-start.js # Expected a Test262Error but got a Error
|
||||
built-ins/Array/prototype/filter/15.4.4.20-10-4.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/filter/15.4.4.20-9-4.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/filter/create-ctor-non-object.js # Only data fields may be assigned.
|
||||
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/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/forEach/15.4.4.18-7-3.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/forEach/15.4.4.18-8-12.js # Only data fields may be assigned.
|
||||
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/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 Error
|
||||
@@ -40,15 +37,13 @@ built-ins/Array/prototype/indexOf/15.4.4.14-5-3.js # Array.indexOf expects star
|
||||
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-9.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-3.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/indexOf/15.4.4.14-9-a-6.js # Array length cannot be assigned.
|
||||
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/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.1_T1.js # Array length cannot be assigned.
|
||||
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_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 # Array.join input must contain plain objects only.
|
||||
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/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.
|
||||
@@ -67,36 +62,24 @@ built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-4.js # a.lastIndexOf(2,undefi
|
||||
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-9.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-3.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-8-a-6.js # Array length cannot be assigned.
|
||||
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/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-4.js # Array length cannot be assigned.
|
||||
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-9-4.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/map/create-ctor-non-object.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/pop/S15.4.4.6_A1.1_T1.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/pop/S15.4.4.6_A1.2_T1.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/push/S15.4.4.7_A3.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/reduce/15.4.4.21-10-8.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/reduce/15.4.4.21-8-c-2.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/reduce/15.4.4.21-9-4.js # Array length cannot be assigned.
|
||||
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/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/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/reduceRight/15.4.4.22-10-8.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/reduceRight/15.4.4.22-8-c-2.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/reduceRight/15.4.4.22-9-4.js # Array length cannot be assigned.
|
||||
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/reverse/S15.4.4.8_A1_T2.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/shift/S15.4.4.9_A1.1_T1.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/shift/S15.4.4.9_A1.2_T1.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/slice/create-ctor-non-object.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/some/15.4.4.17-7-4.js # Array length cannot be assigned.
|
||||
built-ins/Array/prototype/some/15.4.4.17-8-13.js # Only data fields may be assigned.
|
||||
built-ins/Array/prototype/sort/S15.4.4.11_A2.1_T3.js # isNaN input must contain plain objects only.
|
||||
built-ins/Array/prototype/sort/S15.4.4.11_A2.2_T3.js # String input must contain plain objects only.
|
||||
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-8-13.js # Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.
|
||||
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/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/splice/create-ctor-non-object.js # Only data fields may be assigned.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user