refactor(codemode): give programs their own objects and arrays (#48527)

This commit is contained in:
Aiden Cline
2026-09-11 13:36:35 -05:00
committed by GitHub
parent 6e8ff0c2b7
commit 10ae321bf3
23 changed files with 571 additions and 640 deletions
+7 -6
View File
@@ -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.
+43 -25
View File
@@ -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 })
}
+6 -9
View File
@@ -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) {
+61 -58
View File
@@ -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
}
+2 -2
View File
@@ -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))
}
}
+12 -15
View File
@@ -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
}
+6 -4
View File
@@ -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")
+111 -134
View File
@@ -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 {
+17 -18
View File
@@ -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) }),
},
})
+10 -10
View File
@@ -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))
}),
)
}
+15 -14
View File
@@ -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 }
}
+28 -42
View File
@@ -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)
+30 -52
View File
@@ -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]),
),
+18 -33
View File
@@ -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
}
+3 -3
View File
@@ -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)]))),
)
})
}
+24 -27
View File
@@ -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 () => {
+14 -17
View File
@@ -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 () => {
+7 -105
View File
@@ -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 -4
View File
@@ -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"
}
+21 -38
View File
@@ -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.