feat(codemode): add Object.freeze/seal/create/getPrototypeOf and structuredClone (#50834)

This commit is contained in:
Aiden Cline
2026-09-24 00:50:54 -05:00
committed by GitHub
parent bee5014f89
commit 318a8c1aba
11 changed files with 496 additions and 36 deletions
+34 -5
View File
@@ -102,6 +102,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
- [ ] `for...in` over inherited enumerable keys (`Object.create(proto)`), and skipping keys deleted during the loop.
- [x] Unlabeled `break` and `continue`.
- [x] `try`, `catch`, optional catch bindings, and `finally`.
- [x] `throw` with arbitrary values.
@@ -154,6 +155,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
- [x] Redeclaring a function in the same scope, or alongside a `var`, is allowed: the last declaration wins.
- [x] Generator functions have their own `prototype` (inheriting the shared generator prototype), so
`g() instanceof g` holds. Plain functions have none, since they cannot construct.
- [ ] `GeneratorFunction.prototype`: every function, generator or not, inherits directly from `Function.prototype`,
and a generator whose `prototype` was replaced by a non-object still creates from the shared generator prototype.
- [x] Generator and async generator functions bind parameters (defaults, destructuring) at the call and defer only the
body to the first `next()`, so a bad argument throws synchronously from the call site, as in JS.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
@@ -295,9 +298,27 @@ reject }` object.
- [x] Every value has a real prototype chain built fresh for each run: `Object.prototype`, `Array.prototype`,
`String.prototype`, `Error.prototype` → `TypeError.prototype`, and so on hold the built-in methods as
non-enumerable properties, and each constructor's `prototype` points at it (`[].constructor === Array`,
`Object.getPrototypeOf` is not exposed). Programs may read and even overwrite these prototypes; the change is
confined to that run. `__proto__` is an ordinary own data key, so `o.__proto__ = x` never changes the chain, and
`Object.groupBy` results have no prototype at all, as in JS.
`Object.getPrototypeOf([]) === Array.prototype`). Programs may read and even overwrite these prototypes; the
change is confined to that run. `__proto__` is an ordinary own data key, so `o.__proto__ = x` never changes the
chain, and `Object.groupBy` results have no prototype at all, as in JS.
- [x] `Object.getPrototypeOf`: the prototype of an object, or the built-in prototype for a string, number, or boolean
(`Object.getPrototypeOf("a") === String.prototype`); `null`, `undefined`, and the two confined symbols throw a
`TypeError`.
- [x] `Object.create(proto)` with an object or `null` prototype; any other prototype is a `TypeError`
(`Object prototype may only be an Object or null`). Inherited reads, `in`, `hasOwnProperty`, and own-only
`Object.keys` follow the chain as in JS, but `for...in` still enumerates own keys only. A second `properties`
argument other than `undefined` throws a `TypeError`: property descriptors are not supported (there is no
`Object.defineProperty` either).
- [x] `Object.freeze`, `Object.seal`, and `Object.preventExtensions`, with `isFrozen`, `isSealed`, and `isExtensible`.
Each returns its argument and passes primitives through (`Object.freeze(1) === 1`, `Object.isFrozen(1)`).
Programs run as strict code, so violations throw `TypeError`: `Cannot assign to read only property 'a'`,
`Cannot add property b, object is not extensible`, and `Cannot delete property 'a'`. Arrays enforce the same
rules for index writes, `length`, and the mutating methods (`Object.freeze([1]).push(2)` throws; a sealed array's
`pop()` throws; a non-extensible array's `pop()`, `sort()`, and `splice(0, 1, x)` work). The mutating methods are
checked once up front, so a `fill` or `sort` that would land on a hole of a non-extensible array succeeds here
where JS throws. Maps, Sets, and Dates freeze their properties only, not their contents, as in JS.
`Object.freeze(new Uint8Array(1))` and `seal` throw `TypeError` like JS (`preventExtensions` and empty typed
arrays are fine). Functions are objects and freeze like any other value.
- [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
@@ -325,8 +346,8 @@ reject }` object.
- [x] `keys`, `values`, `entries`, and `[Symbol.iterator]` (the same function as `values`) return live iterator objects
with `next()` and `[Symbol.iterator]`, as in JS. Iterator objects are opaque references: they print as
`[opaque reference]`, serialize to `{}`, and cannot be passed to extensions. Every built-in collection iterator
shares one prototype. JavaScript gives each collection its own; the difference is not observable here because
`Object.getPrototypeOf` is not exposed.
shares one prototype, where JavaScript gives each collection its own; `Object.getPrototypeOf` shows the
difference.
- [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`.
@@ -518,6 +539,12 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
- [x] `atob` and `btoa` with forgiving-base64 decoding and WebIDL string conversion; invalid input throws a
`TypeError`, since there is no `DOMException`.
- [x] `crypto.randomUUID()` and `crypto.getRandomValues(uint8Array)`.
- [x] `structuredClone(value)` deep-copies primitives, plain objects (own enumerable string keys onto a plain object;
the prototype is not kept), arrays (holes and extra keys kept), Map, Set, Date, RegExp (`lastIndex` reset to 0),
Uint8Array, and errors (standard `name`, `message`, `cause`, and `stack`; other names become `Error`, extra
fields drop). Shared references stay shared in the copy, and the copy is extensible even when the source was
frozen. Functions, symbols, promises, iterators, URL, Headers, and tool references cannot be cloned; without a
`DOMException`, the failure is a `TypeError` whose message starts with `DataCloneError:`.
- [x] `TextEncoder` and `TextDecoder` for UTF-8 only: any other label is a `RangeError`. `TextDecoder` accepts the
`fatal` and `ignoreBOM` options; `decode` takes a Uint8Array or nothing.
- [x] `new Headers()` from records, synchronous iterables of pairs, and Headers, wrapping the host's `Headers`: names
@@ -569,6 +596,8 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
non-enumerable: an extension Error carries it, and this JSON form does not. Errors have no `stack`; the diagnostic
carries a 1-based line and column in the submitted source instead.
- [x] `instanceof` against any constructor with a `prototype`, including every built-in and `Function`.
- [ ] The derived error constructors inheriting from `Error`: `Object.getPrototypeOf(TypeError)` is
`Function.prototype` here, while `TypeError.prototype` does inherit from `Error.prototype`.
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
`catch`.
+2 -1
View File
@@ -15,7 +15,7 @@ import { uriGlobal, urlGlobal, urlSearchParamsGlobal } from "../stdlib/url.js"
import { headersGlobal } from "../stdlib/headers.js"
import { iteratorGlobals } from "../stdlib/iterator.js"
import { coercion } from "../stdlib/value.js"
import { base64Global, cryptoGlobal } from "../stdlib/web.js"
import { base64Global, cryptoGlobal, structuredCloneGlobal } from "../stdlib/web.js"
import { ToolReference } from "../tool-runtime.js"
import { errorGlobal } from "./errors.js"
import { errorTypes } from "./intrinsics.js"
@@ -95,6 +95,7 @@ const table: Record<string, Factory> = {
atob: (ctx) => base64Global(ctx, "atob"),
btoa: (ctx) => base64Global(ctx, "btoa"),
crypto: (ctx) => cryptoGlobal(ctx),
structuredClone: (ctx) => structuredCloneGlobal(ctx),
...Object.fromEntries(errorTypes.map((type) => [type, <R>(ctx: Interpreter<R>) => errorGlobal(type, ctx)])),
}
@@ -2218,7 +2218,6 @@ class Frame<R> {
}
})()
if (written) return
if (target instanceof Arr && key === "length") throw rangeError("Invalid array length", node)
throw typeError(`Cannot assign to read only property '${String(key)}'.`, node)
}
+30 -6
View File
@@ -9,6 +9,8 @@ import {
type GeneratorRequestKind,
IteratorSymbol,
type PendingThrow,
rangeError,
typeError,
} from "./model.js"
/** Property attributes, as in a JS property descriptor. */
@@ -41,6 +43,8 @@ export const frozen: Attributes = { writable: false, enumerable: false, configur
*/
export class Obj {
readonly props = new Map<string | symbol, Slot>()
/** [[Extensible]]: cleared by `Object.preventExtensions`, `seal`, and `freeze`. */
extensible = true
constructor(public proto: Obj | null) {}
/** The class name `Object.prototype.toString` reports: `[object Map]`. */
@@ -91,6 +95,8 @@ export class Obj {
export class Arr extends Obj {
override readonly tag = "Array"
/** The attributes every live element shares; `seal` and `freeze` narrow them since elements have no slots. */
elements: Attributes = data
constructor(
proto: Obj,
readonly items: Array<Value> = [],
@@ -460,10 +466,11 @@ export const own = (target: Obj, key: PropertyKey): Slot | undefined => {
const at = index(target, name)
if (at !== undefined) {
const items = elements(target)
return at in items ? { value: items[at], ...data } : undefined
if (!(at in items)) return undefined
return { value: items[at], ...(target instanceof Arr ? target.elements : data) }
}
if (target instanceof Arr && name === "length") {
return { value: target.items.length, writable: true, enumerable: false, configurable: false }
return { value: target.items.length, writable: target.elements.writable, enumerable: false, configurable: false }
}
}
return target.props.get(name)
@@ -505,13 +512,19 @@ export const hasPrototype = (value: Value, proto: Obj): boolean => {
const writeElement = (target: Indexed, name: string | symbol, value: Value): boolean | undefined => {
const at = index(target, name)
if (at !== undefined) {
if (target instanceof Bytes) target.bytes[at] = typeof value === "number" ? value : Number(value)
else target.items[at] = value
if (target instanceof Bytes) {
target.bytes[at] = typeof value === "number" ? value : Number(value)
return true
}
if (!(at in target.items)) rejectAddition(target, at)
target.items[at] = value
return true
}
if (!(target instanceof Arr) || name !== "length") return undefined
const length = typeof value === "number" ? value : Number(value)
if (!Number.isInteger(length) || length < 0) return false
if (!Number.isInteger(length) || length < 0) throw rangeError("Invalid array length")
// Shrinking deletes elements, which a sealed array forbids.
if (length < target.items.length && !target.elements.configurable) return false
checkArrayLength(length)
target.items.length = length
return true
@@ -541,10 +554,17 @@ export const set = (target: Obj, key: PropertyKey, value: Value): boolean => {
const written = writeElement(target, name, value)
if (written !== undefined) return written
}
rejectAddition(target, name)
target.props.set(name, { value, ...data })
return true
}
/** Creating a property on a non-extensible object is the one [[Set]] failure with its own message. */
export const rejectAddition = (target: Obj, key: string | symbol | number): void => {
if (target.extensible) return
throw typeError(`Cannot add property ${String(key)}, object is not extensible.`)
}
/** [[DefineOwnProperty]] for a data property, ignoring the chain. */
export const define = (target: Obj, key: PropertyKey, value: Value, attrs: Attributes = data): void => {
const name = canonical(key)
@@ -560,7 +580,11 @@ export const remove = (target: Obj, key: PropertyKey): boolean => {
const name = canonical(key)
if (isIndexed(target)) {
const at = index(target, name)
if (at !== undefined) return target instanceof Bytes ? !(at in target.bytes) : delete target.items[at]
if (at !== undefined) {
if (target instanceof Bytes) return !(at in target.bytes)
if (at in target.items && !target.elements.configurable) return false
return delete target.items[at]
}
if (target instanceof Arr && name === "length") return false
}
const slot = target.props.get(name)
+30 -11
View File
@@ -14,6 +14,7 @@ import {
coerceToInteger,
coerceToNumber,
coerceToString,
rejectAddition,
type Value,
} from "../interpreter/objects.js"
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
@@ -124,6 +125,20 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
])
const self = (thisValue: Value, name: string) => receiver(Arr, thisValue, `Array.prototype.${name}`)
// A mutating method fails where its first element write, delete, or `length` write would on a frozen, sealed, or
// non-extensible array. `growth` is given by the methods that always write `length`, even when it is 0; holes a
// method would fill on a non-extensible array are not checked.
const mutable = (target: Arr, growth?: number): Arr => {
const length = target.items.length
if ((growth ?? 0) > 0) rejectAddition(target, length)
if ((growth ?? 0) < 0 && length > 0 && !target.elements.configurable) {
throw typeError(`Cannot delete property '${length - 1}'.`)
}
if (!target.elements.writable && (length > 0 || growth !== undefined)) {
throw typeError(`Cannot assign to read only property '${length > 0 ? 0 : "length"}'.`)
}
return target
}
const optNumber = (value: Value): number | undefined => (value === undefined ? undefined : coerceToInteger(value))
methods(builtins, proto, [
@@ -196,6 +211,8 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
0,
(thisValue) => {
const target = self(thisValue, "reverse")
// Fewer than two elements means no writes at all, so a frozen one-element array reverses fine.
if (target.items.length > 1) mutable(target)
target.items.reverse()
return target
},
@@ -204,7 +221,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"sort",
1,
(thisValue, args) => {
const target = self(thisValue, "sort")
const target = mutable(self(thisValue, "sort"))
const items = target.items
const length = items.length
const holeCount = Array.from({ length }, (_, index) => Object.hasOwn(items, index)).filter((o) => !o).length
@@ -244,7 +261,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"push",
1,
(thisValue, args) => {
const target = self(thisValue, "push")
const target = mutable(self(thisValue, "push"), args.length)
// Validate all insertions before mutating to avoid partial cyclic updates.
for (const item of args) rejectCircularInsertion(target, item, "Array.push result")
return target.items.push(...args)
@@ -254,25 +271,27 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"unshift",
1,
(thisValue, args) => {
const target = self(thisValue, "unshift")
const target = mutable(self(thisValue, "unshift"), args.length)
for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result")
return target.items.unshift(...args)
},
],
["pop", 0, (thisValue) => self(thisValue, "pop").items.pop()],
["shift", 0, (thisValue) => self(thisValue, "shift").items.shift()],
["pop", 0, (thisValue) => mutable(self(thisValue, "pop"), -1).items.pop()],
["shift", 0, (thisValue) => mutable(self(thisValue, "shift"), -1).items.shift()],
[
"splice",
2,
(thisValue, args) => {
const target = self(thisValue, "splice")
if (args.length === 0) return wrap(target.items.splice(0, 0))
const length = target.items.length
const start = optNumber(args[0]) ?? 0
if (args.length === 1) return wrap(target.items.splice(start))
const deleteCount = optNumber(args[1]) ?? 0
const from = start < 0 ? Math.max(length + start, 0) : Math.min(start, length)
const deleteCount =
args.length === 1 ? length - from : Math.min(Math.max(optNumber(args[1]) ?? 0, 0), length - from)
const inserted = args.slice(2)
for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result")
return wrap(target.items.splice(start, deleteCount, ...inserted))
mutable(target, inserted.length - deleteCount)
return wrap(target.items.splice(from, deleteCount, ...inserted))
},
],
[
@@ -291,7 +310,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"fill",
1,
(thisValue, args) => {
const target = self(thisValue, "fill")
const target = mutable(self(thisValue, "fill"))
rejectCircularInsertion(target, args[0], "Array.fill result")
target.items.fill(args[0], optNumber(args[1]), optNumber(args[2]))
return target
@@ -301,7 +320,7 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
"copyWithin",
2,
(thisValue, args) => {
const target = self(thisValue, "copyWithin")
const target = mutable(self(thisValue, "copyWithin"))
target.items.copyWithin(optNumber(args[0]) ?? 0, optNumber(args[1]) ?? 0, optNumber(args[2]))
return target
},
+65 -9
View File
@@ -1,13 +1,6 @@
import { Effect } from "effect"
import { constructor, methods, receiver } from "../interpreter/native.js"
import {
type AstNode,
AsyncIteratorSymbol,
invalidData,
IteratorSymbol,
rangeError,
typeError,
} from "../interpreter/model.js"
import { type AstNode, AsyncIteratorSymbol, invalidData, IteratorSymbol, typeError } from "../interpreter/model.js"
import {
define,
entries,
@@ -18,13 +11,16 @@ import {
hidden,
keys,
own,
ownKeys,
Arr,
Bytes,
Obj,
PromiseObj,
set,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { primitivePrototype } from "../interpreter/intrinsics.js"
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
import { invoke, preserveConsumerError } from "../interpreter/callback.js"
import type { Interpreter } from "../interpreter/interpreter.js"
@@ -63,7 +59,6 @@ export const objectAssign = <R>(ctx: Interpreter<R>, args: Array<Value>): Value
for (const key of enumerableKeys(from)) {
rejectCircularInsertion(target, getOwn(from, key), "Object.assign result", seen)
if (!set(target, key, getOwn(from, key))) {
if (target instanceof Arr && key === "length") throw rangeError("Invalid array length")
throw typeError(`Cannot assign to read only property '${String(key)}'.`)
}
}
@@ -107,6 +102,34 @@ const classTag = (value: Value): string => {
const propertyKey = (value: Value): PropertyKey =>
value === AsyncIteratorSymbol || value === IteratorSymbol ? value : coerceToString(value)
// SetIntegrityLevel: primitives pass through. A typed array's bytes cannot carry attributes, so JS throws after
// already making it non-extensible.
const restrict = (level: "freeze" | "seal" | "preventExtensions", value: Value): Value => {
if (!(value instanceof Obj)) return value
value.extensible = false
if (level === "preventExtensions") return value
if (value instanceof Bytes && value.bytes.length > 0) {
throw typeError(`Cannot ${level} array buffer views with elements.`)
}
for (const slot of value.props.values()) {
slot.configurable = false
if (level === "freeze" && "value" in slot) slot.writable = false
}
if (value instanceof Arr) value.elements = { writable: level === "seal", enumerable: true, configurable: false }
return value
}
// TestIntegrityLevel: array elements and `length` answer through `own`, so a non-extensible empty array is sealed
// but not frozen, as in JS.
const integrity = (value: Value, frozen: boolean): boolean => {
if (!(value instanceof Obj)) return true
if (value.extensible) return false
return ownKeys(value).every((key) => {
const slot = own(value, key)
return slot !== undefined && !slot.configurable && !(frozen && "value" in slot && slot.writable)
})
}
// Object constructs identically with or without new, like JS. Only `keys` copies its result into the
// program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values.
export const objectGlobal = <R>(ctx: Interpreter<R>) => {
@@ -157,6 +180,39 @@ export const objectGlobal = <R>(ctx: Interpreter<R>) => {
["is", 2, (_, args) => Object.is(args[0], args[1])],
["assign", 2, (_, args) => objectAssign(ctx, args)],
["fromEntries", 1, (_, args) => objectFromEntries(ctx, args[0])],
["freeze", 1, (_, args) => restrict("freeze", args[0])],
["seal", 1, (_, args) => restrict("seal", args[0])],
["preventExtensions", 1, (_, args) => restrict("preventExtensions", args[0])],
["isFrozen", 1, (_, args) => integrity(args[0], true)],
["isSealed", 1, (_, args) => integrity(args[0], false)],
["isExtensible", 1, (_, args) => args[0] instanceof Obj && args[0].extensible],
[
"getPrototypeOf",
1,
(_, args) => {
if (args[0] instanceof Obj) return args[0].proto
const proto = primitivePrototype(builtins, args[0])
if (proto === undefined) {
throw typeError(`Object.getPrototypeOf cannot convert ${describeValue(args[0])} to an object.`)
}
return proto
},
],
[
"create",
2,
(_, args) => {
if (args[0] !== null && !(args[0] instanceof Obj)) {
throw typeError("Object prototype may only be an Object or null.")
}
if (args[1] !== undefined) {
throw typeError(
"Object.create property descriptors are not supported; assign the fields after creating the object.",
)
}
return new Obj(args[0])
},
],
])
define(object, "groupBy", groupBy(ctx, "Object"), hidden)
methods(builtins, builtins.Object, [
+78 -1
View File
@@ -1,8 +1,28 @@
import { fn, methods } from "../interpreter/native.js"
import { typeError } from "../interpreter/model.js"
import { Bytes, Obj, coerceToString } from "../interpreter/objects.js"
import {
define,
entries,
get,
getOwn,
hasOwn,
hidden,
isRuntimeReference,
Arr,
Bytes,
DateObj,
ErrorObj,
MapObj,
Obj,
RegExpObj,
SetObj,
coerceToString,
type Value,
} from "../interpreter/objects.js"
import { createErrorValue, isErrorType } from "../interpreter/intrinsics.js"
import { describeValue } from "../interpreter/references.js"
import type { Interpreter } from "../interpreter/interpreter.js"
import { ToolReference } from "../tool-runtime.js"
// WebIDL DOMString conversion: a missing argument is a TypeError, anything else stringifies. Invalid input is a
// TypeError as well; browsers throw a DOMException named InvalidCharacterError, which CodeMode does not have.
@@ -17,6 +37,63 @@ export const base64Global = <R>(ctx: Interpreter<R>, name: "atob" | "btoa") =>
}
})
// HTML structured clone over CodeMode's data kinds. Shared references survive through the memo; prototypes and
// frozen state do not, as in JS. Without a DOMException, DataCloneError is a TypeError carrying that prefix.
export const structuredCloneGlobal = <R>(ctx: Interpreter<R>) =>
fn<R>(ctx.builtins, "structuredClone", 1, (_, args) => {
if (args.length === 0) throw typeError("structuredClone requires 1 argument")
const builtins = ctx.builtins
const memo = new Map<Obj, Obj>()
const clone = (value: Value): Value => {
if (typeof value === "symbol" || value instanceof ToolReference) {
throw typeError(`DataCloneError: ${describeValue(value)} could not be cloned.`)
}
if (!(value instanceof Obj)) return value
const seen = memo.get(value)
if (seen !== undefined) return seen
const remember = <T extends Obj>(copy: T): T => {
memo.set(value, copy)
return copy
}
if (value instanceof MapObj) {
const copy = remember(new MapObj(builtins.Map))
for (const [key, item] of value.map) copy.map.set(clone(key), clone(item))
return copy
}
if (value instanceof SetObj) {
const copy = remember(new SetObj(builtins.Set))
for (const item of value.set) copy.set.add(clone(item))
return copy
}
if (value instanceof DateObj) return remember(new DateObj(builtins.Date, value.time))
if (value instanceof RegExpObj) {
return remember(new RegExpObj(builtins.RegExp, value.regex.source, value.regex.flags))
}
if (value instanceof Bytes) return remember(new Bytes(builtins.Uint8Array, new Uint8Array(value.bytes)))
if (value instanceof ErrorObj) {
const name = get(value, "name")
const message = get(value, "message")
const copy = remember(
createErrorValue(
builtins[typeof name === "string" && isErrorType(name) ? name : "Error"],
message === undefined ? undefined : coerceToString(message),
),
)
const stack = getOwn(value, "stack")
if (typeof stack === "string") define(copy, "stack", stack, hidden)
if (hasOwn(value, "cause")) define(copy, "cause", clone(getOwn(value, "cause")), hidden)
return copy
}
if (isRuntimeReference(value)) throw typeError(`DataCloneError: ${describeValue(value)} could not be cloned.`)
const copy = remember(
value instanceof Arr ? new Arr(builtins.Array, new Array(value.items.length)) : new Obj(builtins.Object),
)
for (const [key, item] of entries(value)) define(copy, key, clone(item))
return copy
}
return clone(args[0])
})
export const cryptoGlobal = <R>(ctx: Interpreter<R>) => {
const object = new Obj(ctx.builtins.Object)
methods(ctx.builtins, object, [
+230
View File
@@ -1231,3 +1231,233 @@ describe("loose equality and operator gates on opaque references", () => {
).toEqual(["1,2", null, "[object Function]", true, true, "function", false])
})
})
describe("Object.freeze, seal, and preventExtensions", () => {
test("a frozen object rejects writes, additions, and deletes with TypeErrors", async () => {
expect(
await value(`
const o = Object.freeze({ a: 1 })
const errors = []
try { o.a = 2 } catch (e) { errors.push(e.name + ": " + e.message) }
try { o.b = 2 } catch (e) { errors.push(e.name + ": " + e.message) }
try { delete o.a } catch (e) { errors.push(e.name + ": " + e.message) }
return [errors, o.a, Object.isFrozen(o), Object.isSealed(o), Object.isExtensible(o)]
`),
).toEqual([
[
"TypeError: Cannot assign to read only property 'a'.",
"TypeError: Cannot add property b, object is not extensible.",
"TypeError: Cannot delete property 'a'.",
],
1,
true,
true,
false,
])
})
test("seal keeps writes, preventExtensions keeps deletes, and each returns its argument", async () => {
expect(
await value(`
const s = { a: 1 }, p = { a: 1 }
const errors = []
Object.seal(s).a = 2
try { delete s.a } catch (e) { errors.push(e.name) }
delete Object.preventExtensions(p).a
try { p.b = 1 } catch (e) { errors.push(e.name) }
return [errors, s.a, Object.keys(p), Object.isSealed(s), Object.isFrozen(s), Object.isSealed(p), Object.isFrozen(p)]
`),
).toEqual([["TypeError", "TypeError"], 2, [], true, false, true, true])
})
test("primitives pass through, and Object.assign honors the flags", async () => {
expect(
await value(`
return [Object.freeze(1) === 1, Object.isFrozen(1), Object.isSealed("a"), Object.isExtensible(null), Object.isFrozen({}), Object.isExtensible({})]
`),
).toEqual([true, true, true, false, false, true])
const failure = await error(`Object.assign(Object.freeze({ a: 1 }), { b: 1 })`)
expect(failure.message).toContain("Cannot add property b, object is not extensible")
})
test("frozen arrays reject element, length, and mutating-method writes like JS", async () => {
expect(
await value(`
const a = Object.freeze([1, 2])
const attempt = (f) => { try { f(); return "ok" } catch (e) { return e.name + ": " + e.message } }
return [
attempt(() => a.push(3)),
attempt(() => { a[0] = 9 }),
attempt(() => { a[5] = 9 }),
attempt(() => { a.length = 0 }),
attempt(() => a.pop()),
attempt(() => a.sort()),
attempt(() => a.reverse()),
attempt(() => a.fill(0)),
attempt(() => a.copyWithin(0, 1)),
attempt(() => a.splice(0, 1)),
attempt(() => a.unshift(0)),
a, a.map((x) => x * 2), Object.isFrozen(a), Object.isFrozen(Object.freeze([])),
]
`),
).toEqual([
"TypeError: Cannot add property 2, object is not extensible.",
"TypeError: Cannot assign to read only property '0'.",
"TypeError: Cannot add property 5, object is not extensible.",
"TypeError: Cannot assign to read only property 'length'.",
"TypeError: Cannot delete property '1'.",
"TypeError: Cannot assign to read only property '0'.",
"TypeError: Cannot assign to read only property '0'.",
"TypeError: Cannot assign to read only property '0'.",
"TypeError: Cannot assign to read only property '0'.",
"TypeError: Cannot delete property '1'.",
"TypeError: Cannot add property 2, object is not extensible.",
[1, 2],
[2, 4],
true,
true,
])
})
test("sealed and non-extensible arrays allow exactly the writes JS does", async () => {
expect(
await value(`
const s = Object.seal([1, 2]), p = Object.preventExtensions([1, 2, 3])
const attempt = (f) => { try { f(); return "ok" } catch (e) { return e.name } }
const results = [attempt(() => { s[0] = 5 }), attempt(() => s.pop()), attempt(() => { s.length = 0 }), attempt(() => s.push(1))]
p.pop(); p.sort(); p.reverse(); p.fill(0, 1); p.copyWithin(0, 1); p.length = 5
results.push(attempt(() => p.push(1)), attempt(() => p.splice(0, 1, 7, 8)), p.splice(0, 1, 7), attempt(() => { p[9] = 1 }))
return [results, s, p, Object.isSealed(p), Object.isFrozen(p), Object.isSealed(Object.preventExtensions([])), Object.isFrozen(Object.preventExtensions([]))]
`),
).toEqual([
["ok", "TypeError", "TypeError", "TypeError", "TypeError", "TypeError", [0], "TypeError"],
[5, 2],
[7, 0, null, null, null],
false,
false,
true,
false,
])
})
test("typed arrays with elements cannot be frozen; wrappers freeze their properties only", async () => {
const failure = await error(`Object.freeze(new Uint8Array([1]))`)
expect(failure.message).toContain("Cannot freeze array buffer views with elements")
expect(
await value(`
const empty = Object.freeze(new Uint8Array(0))
const bytes = Object.preventExtensions(new Uint8Array([1]))
const m = Object.freeze(new Map()); m.set(1, 2)
const f = Object.freeze(() => 1)
let named = "ok"
try { f.x = 1 } catch (e) { named = e.name }
return [Object.isFrozen(empty), Object.isFrozen(bytes), Object.isExtensible(bytes), m.size, Object.isFrozen(f), named, f()]
`),
).toEqual([true, false, false, 1, true, "TypeError", 1])
})
})
describe("Object.getPrototypeOf and Object.create", () => {
test("getPrototypeOf returns the built-in prototypes for objects and primitives", async () => {
expect(
await value(`
return [
Object.getPrototypeOf([]) === Array.prototype,
Object.getPrototypeOf({}) === Object.prototype,
Object.getPrototypeOf(Object.prototype),
Object.getPrototypeOf("a") === String.prototype,
Object.getPrototypeOf(1) === Number.prototype,
Object.getPrototypeOf(true) === Boolean.prototype,
Object.getPrototypeOf(new TypeError("x")) === TypeError.prototype,
Object.getPrototypeOf(TypeError.prototype) === Error.prototype,
Object.getPrototypeOf(() => 1) === Function.prototype,
]
`),
).toEqual([true, true, null, true, true, true, true, true, true])
})
test("getPrototypeOf rejects null, undefined, and symbols", async () => {
expect((await error(`Object.getPrototypeOf(null)`)).message).toContain("cannot convert null to an object")
expect((await error(`Object.getPrototypeOf(undefined)`)).message).toContain("cannot convert undefined to an object")
expect((await error(`Object.getPrototypeOf(Symbol.iterator)`)).message).toContain("cannot convert a symbol")
})
test("Object.create links the prototype: inherited reads, in, and own-only keys", async () => {
expect(
await value(`
const p = { greet(name) { return "hi " + name }, a: 1 }
const c = Object.create(p)
c.name = "x"
const seen = []
for (const key in c) seen.push(key)
return ["greet" in c, Object.keys(c), c.hasOwnProperty("a"), c.a, c.greet(c.name), Object.getPrototypeOf(c) === p, Object.getPrototypeOf(Object.create(null)), seen]
`),
).toEqual([true, ["name"], false, 1, "hi x", true, null, ["name"]])
})
test("Object.create rejects non-object prototypes and property descriptors", async () => {
expect((await error(`Object.create(1)`)).message).toContain("Object prototype may only be an Object or null")
expect((await error(`Object.create()`)).message).toContain("Object prototype may only be an Object or null")
expect((await error(`Object.create(null, { a: { value: 1 } })`)).message).toContain(
"Object.create property descriptors are not supported",
)
expect(await value(`return Object.keys(Object.create({}, undefined))`)).toEqual([])
})
})
describe("structuredClone", () => {
test("deep-copies data values and keeps shared references shared", async () => {
expect(
await value(`
const shared = { n: 1 }
const source = { a: shared, b: shared, list: [1, , shared], map: new Map([[1, { a: [1, 2] }]]), set: new Set([shared]), when: new Date(5), bytes: new Uint8Array([1, 2]) }
const c = structuredClone(source)
c.a.n = 2
return [
c !== source, c.a === c.b, c.a !== shared, shared.n, c.list[2] === c.a, 1 in c.list, c.list.length,
c.map.get(1).a, c.map !== source.map, c.set.has(c.a), c.when.getTime(), c.when instanceof Date,
c.bytes instanceof Uint8Array, c.bytes[1], Array.isArray(c.list),
]
`),
).toEqual([true, true, true, 1, true, false, 3, [1, 2], true, true, 5, true, true, 2, true])
})
test("regexps reset lastIndex, errors keep name, message, and cause, and extras are dropped", async () => {
expect(
await value(`
const r = /a/g
r.lastIndex = 3
const e = new TypeError("boom", { cause: { code: 1 } })
e.extra = 1
const custom = new Error("x")
custom.name = "Custom"
const [cr, ce, cc] = [structuredClone(r), structuredClone(e), structuredClone(custom)]
return [cr.source, cr.flags, cr.lastIndex, ce instanceof TypeError, ce.name, ce.message, ce.cause, ce.cause !== e.cause, ce.extra, Object.keys(ce), cc.name]
`),
).toEqual(["a", "g", 0, true, "TypeError", "boom", { code: 1 }, true, null, [], "Error"])
})
test("the clone is a plain extensible object: prototypes, symbols, undefined fields, and frozen state drop", async () => {
expect(
await value(`
const c = structuredClone(Object.freeze(Object.assign(Object.create({ inherited: 1 }), { a: undefined, [Symbol.iterator]: 1, b: 2 })))
return [Object.isFrozen(c), Object.getPrototypeOf(c) === Object.prototype, "a" in c, Symbol.iterator in c, c.b, c.inherited]
`),
).toEqual([false, true, true, false, 2, null])
})
test("functions, symbols, promises, wrappers, and tool references throw a DataCloneError TypeError", async () => {
for (const code of [
`structuredClone(() => 1)`,
`structuredClone({ deep: [Symbol.iterator] })`,
`structuredClone(Promise.resolve(1))`,
`structuredClone(new URL("http://x"))`,
`structuredClone(tools)`,
]) {
const failure = await error(code)
expect(failure.message).toMatch(/^TypeError: DataCloneError: .* could not be cloned\./)
}
expect(await value(`try { structuredClone(() => 1) } catch (e) { return e.name }`)).toBe("TypeError")
expect((await error(`structuredClone()`)).message).toContain("structuredClone requires 1 argument")
})
})
+1 -1
View File
@@ -24,7 +24,7 @@ Without them the runner registers no tests, so CI is unaffected. Licensed under
`script/sync-test262.ts` skips a file when its frontmatter declares a `flags`, `features`, or `includes` value the
manifest marks unsupported, or when its code matches one of the manifest's `boundaries` patterns. The sync checks the
checkout is at the pinned revision, so every machine runs the same 4478 files. Boundaries are
checkout is at the pinned revision, so every machine runs the same 4595 files. Boundaries are
intentional limits of the interpreter, not compatibility work: classes, `this`, `arguments`, prototype objects,
property descriptors, accessors, boxed primitives, sloppy mode, `eval`, `Symbol()`, and the `$262` host API. If one
of those decisions changes, delete its entry and re-sync; the tests are upstream, not lost.
+7 -1
View File
@@ -3,7 +3,13 @@
"directories": [
"built-ins/Array/prototype",
"built-ins/Iterator",
"built-ins/Object/freeze",
"built-ins/Object/getPrototypeOf",
"built-ins/Object/is",
"built-ins/Object/isExtensible",
"built-ins/Object/isFrozen",
"built-ins/Object/isSealed",
"built-ins/Object/preventExtensions",
"built-ins/String/raw",
"language/expressions/does-not-equals",
"language/expressions/equals",
@@ -18,7 +24,7 @@
"arguments": "\\barguments\\b",
"call/apply/bind": "\\.(call|apply|bind)\\s*\\(",
"accessor properties": "\\b(get|set)\\s+[\\w$\\[][^\\n(]*\\(",
"property descriptors": "Object\\.(defineProperty|defineProperties|getOwnPropertyDescriptors?|getOwnPropertyNames|create|getPrototypeOf|setPrototypeOf|freeze|seal|preventExtensions|isFrozen|isSealed|isExtensible)\\b",
"property descriptors": "Object\\.(defineProperty|defineProperties|getOwnPropertyDescriptors?|getOwnPropertyNames|setPrototypeOf)\\b",
"boxed primitives": "\\bnew\\s+(String|Number|Boolean)\\s*\\(",
"sloppy mode": "\\bwith\\s*\\(",
"eval": "\\b(eval|Function)\\b",
@@ -194,6 +194,7 @@ built-ins/Array/prototype/unshift/S15.4.4.13_A3_T2.js # Array.prototype.unshift
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T1.js # Array.prototype.unshift called on incompatible receiver a data object.
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T2.js # #3: Array.prototype[0] = 1; x = []; x.length = 1; x.unshift(0); x[1] === 1. Actual: undefined
built-ins/Iterator/from/return-method-returns-iterator-result.js # Iterator next must be a function.
built-ins/Iterator/prototype/Symbol.iterator/is-function.js # every built-in collection iterator shares one prototype, so its parent is Object.prototype
built-ins/Iterator/prototype/drop/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
built-ins/Iterator/prototype/drop/limit-tonumber.js # Iterator.prototype.drop expects a non-negative count, received NaN.
built-ins/Iterator/prototype/take/limit-tonumber-throws.js # Expected a Test262Error but got a RangeError
@@ -205,6 +206,20 @@ language/expressions/does-not-equals/S11.9.2_A7.9.js # #1: ({valueOf: function(
language/expressions/equals/S11.9.1_A7.8.js # #1: ({valueOf: function() {return 1}} == true) === true
language/expressions/equals/S11.9.1_A7.9.js # #1: (true == {valueOf: function() {return 1}}) === true
language/expressions/equals/S9.1_A1_T3.js # #1: var object = {valueOf: function() {return 1}, toString: function() {return 0}}; object + "" ===
built-ins/Object/freeze/throws-when-false.js # Unknown identifier 'Proxy'.
built-ins/Object/getPrototypeOf/15.2.3.2-2-12.js # Object.getPrototypeOf(EvalError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-13.js # Object.getPrototypeOf(RangeError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-14.js # Object.getPrototypeOf(ReferenceError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-15.js # Object.getPrototypeOf(SyntaxError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-16.js # Object.getPrototypeOf(TypeError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-17.js # Object.getPrototypeOf(URIError) Expected SameValue(«», «Error») to be true
built-ins/Object/getPrototypeOf/15.2.3.2-2-2.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/isExtensible/15.2.3.13-2-24.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/isExtensible/15.2.3.13-2-25.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/isExtensible/15.2.3.13-2-26.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/isExtensible/15.2.3.13-2-27.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/preventExtensions/15.2.3.10-3-24.js # … cannot be constructed: user-defined constructors and classes are not supported.
built-ins/Object/preventExtensions/throws-when-false.js # Unknown identifier 'Proxy'.
language/expressions/tagged-template/constructor-invocation.js # The called value cannot be constructed: user-defined constructors and classes are not supported.
language/expressions/tagged-template/template-object-frozen-strict.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/async-generator/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
@@ -221,6 +236,8 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeErr
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
language/statements/for-in/S12.6.4_A7_T2.js # for...in visits keys deleted during the loop
language/statements/for-in/order-enumerable-shadowed.js # Object.create property descriptors are not supported; assign the fields after creating the object.
language/statements/for-of/dstr/array-elem-iter-rtrn-close-err.js # Iterator next must be a function.
language/statements/for-of/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true
language/statements/for-of/dstr/array-elem-trlg-iter-list-thrw-close-err.js # Expected SameValue(«1», «0») to be true
@@ -258,8 +275,10 @@ language/statements/function/S13.2_A4_T1.js # Unknown identifier '…'.
language/statements/function/S13.2_A4_T2.js # #1: typeof __gunc.prototype === '…'. Actual: typeof __gunc.prototype ===undefined
language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/default-proto.js # generator functions have no GeneratorFunction.prototype
language/statements/generators/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/generators/prototype-relation-to-function.js # generator functions have no GeneratorFunction.prototype
language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected.
language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
language/statements/return/S12.9_A1_T1.js # expected SyntaxError but the program ran