feat(codemode): resolve constructor to the owning built-in (#48233)

This commit is contained in:
Aiden Cline
2026-09-09 20:14:29 -05:00
committed by GitHub
parent f91c6d8b25
commit bdb66747e7
5 changed files with 118 additions and 12 deletions
+4 -3
View File
@@ -213,9 +213,10 @@ ultimate source of truth.
synchronous iterator support for `fromEntries`.
- [x] `Object.keys` over arrays and tool references.
- [x] Object identity is preserved by in-CodeMode Object helpers.
- [x] `__proto__`, `constructor`, and `prototype` are ordinary own data keys. Prototype machinery is not observable:
data objects have no prototype, so `({}).constructor` and `[].__proto__` read as `undefined` and `o.__proto__ = x`
sets an own field.
- [x] `__proto__`, `constructor`, and `prototype` are ordinary own data keys. `x.constructor` without an own key resolves
to the owning built-in (`[].constructor === Array`, `new TypeError().constructor === TypeError`); prototype objects
are not observable, so `[].__proto__` and `Object.prototype` read as `undefined` and `o.__proto__ = x` sets an own
field.
- [x] 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
+32 -4
View File
@@ -85,7 +85,7 @@ import { numberMethods } from "../stdlib/number.js"
import { constructRegExp, regexpMethods, regexpProperties } from "../stdlib/regexp.js"
import { stringMethods } from "../stdlib/string.js"
import { uriArgument, urlMethods, urlProperties, urlSearchParamsMethods, urlWritableProperties } from "../stdlib/url.js"
import { coerceToNumber, coerceToString, compoundOperators } from "../stdlib/value.js"
import { coerceToNumber, coerceToString, compoundOperators, errorBrandName } from "../stdlib/value.js"
import { Values } from "../values.js"
const MAX_ARRAY_LENGTH = 4_294_967_295
@@ -113,6 +113,25 @@ 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 Values.Date) return "Date"
if (value instanceof Values.RegExp) return "RegExp"
if (value instanceof Values.Map) return "Map"
if (value instanceof Values.Set) return "Set"
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
return errorBrandName(value) ?? "Object"
}
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
if (rhs instanceof HostFunction && rhs.instanceOf !== undefined) return rhs.instanceOf(lhs)
throw new InterpreterRuntimeError(
@@ -206,6 +225,8 @@ const promiseResolutionNode: AstNode = { type: "PromiseResolution", start: 0, en
/** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */
export class Runtime<R> {
readonly runner: Runner<R>
/** Built-in globals by name, unaffected by program shadowing. */
readonly builtins: ReadonlyMap<string, unknown>
private readonly root: Frame<R>
constructor(
@@ -224,7 +245,8 @@ export class Runtime<R> {
settlePromise: (promise) => this.root.settlePromise(promise),
syncIterator: (value, node) => this.root.syncIterator(value, node),
}
for (const [name, value] of globals(this)) globalScope.set(name, { mutable: false, value })
this.builtins = new Map(globals(this))
for (const [name, value] of this.builtins) globalScope.set(name, { mutable: false, value })
}
run(program: Program): Effect.Effect<unknown, unknown, R> {
@@ -2002,7 +2024,7 @@ class Frame<R> {
private getMemberReference(
node: MemberExpression,
operation: "read" | "delete" = "read",
operation: "read" | "write" | "delete" = "read",
): Effect.Effect<
| MemberReference
| ToolReference
@@ -2043,6 +2065,12 @@ class Frame<R> {
return new ComputedValue(objectValue.member(key, propertyNode))
}
// Values have no prototype chain, so `.constructor` resolves to the owning built-in directly.
if (operation === "read" && key === "constructor" && !hasOwn(objectValue, key)) {
const name = constructorName(objectValue)
if (name !== undefined) return new ComputedValue(self.runtime.builtins.get(name))
}
if (typeof objectValue === "string") {
if (key === "length") return new ComputedValue(objectValue.length)
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
@@ -2198,7 +2226,7 @@ class Frame<R> {
): Effect.Effect<unknown, unknown, R> {
const self = this
return Effect.gen(function* () {
const reference = yield* self.getMemberReference(node)
const reference = yield* self.getMemberReference(node, "write")
if (
reference === OptionalShortCircuit ||
reference instanceof ComputedValue ||
@@ -0,0 +1,66 @@
/**
* Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
* - test/built-ins/RegExp/S15.10.7_A3_T1.js
* - test/built-ins/RegExp/S15.10.7_A3_T2.js
* - test/built-ins/Object/S15.2.2.1_A1_T1.js
*
* Copyright 2009 the Sputnik authors. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*
* Only the instance-side assertions are ported. Test262 otherwise reaches `constructor` through
* `X.prototype.constructor`, boxed primitives (`new Object(1)`), `Function`, `isPrototypeOf`, or
* `.call`, none of which CodeMode exposes: values have no prototype chain, so `x.constructor`
* resolves directly to the owning built-in.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const value = async (code: string) => {
const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
describe("constructor Test262 parity", () => {
test("test/built-ins/RegExp/S15.10.7_A3_T1.js", async () => {
expect(
await value(`
const __re = /[^a]*/
return [typeof __re, __re.constructor === RegExp, __re instanceof RegExp]
`),
).toEqual(["object", true, true])
})
test("test/built-ins/RegExp/S15.10.7_A3_T2.js", async () => {
expect(
await value(`
const __re = new RegExp()
return [typeof __re, __re.constructor === RegExp, __re instanceof RegExp]
`),
).toEqual(["object", true, true])
})
test("test/built-ins/Object/S15.2.2.1_A1_T1.js", async () => {
expect(
await value(`
const obj = new Object()
return [obj !== undefined, obj.constructor === Object]
`),
).toEqual([true, true])
})
test("every built-in reports itself for its own values", async () => {
expect(
await value(`
return [
[].constructor === Array, "".constructor === String, (1).constructor === Number, true.constructor === Boolean,
new Date(0).constructor === Date, new Map().constructor === Map, new Set().constructor === Set,
new URL("https://a.b/").constructor === URL, new URLSearchParams("a=1").constructor === URLSearchParams,
Promise.resolve(1).constructor === Promise, new TypeError("x").constructor === TypeError,
new RangeError("x").constructor === RangeError, new AggregateError([]).constructor === AggregateError,
]
`),
).toEqual(Array(13).fill(true))
})
})
+11
View File
@@ -722,6 +722,17 @@ describe("Set", () => {
})
describe("stdlib integration", () => {
test("constructor follows own keys, shadowing, writes, and new", async () => {
expect(
await value(`return [JSON.parse('{"constructor":"Foo"}').constructor, ({ constructor: 1 }).constructor]`),
).toEqual(["Foo", 1])
expect(await value(`const Array = 5; return [].constructor.isArray([])`)).toBe(true)
expect(await value(`const o = {}; o.constructor = 7; return o.constructor`)).toBe(7)
expect(await value(`return new ([].constructor)(3).length`)).toBe(3)
expect(await value(`return typeof ({}).constructor`)).toBe("function")
expect(await value(`return ({}).constructor.constructor`)).toBeNull()
})
test("new dispatches on the constructor value, not its name", async () => {
expect(await value(`const D = Date; return new D(0) instanceof Date`)).toBe(true)
expect(await value(`const make = (C) => new C([["a", 1]]); return make(Map).get("a")`)).toBe(1)
+5 -5
View File
@@ -185,17 +185,17 @@ describe("blocked member names on tool paths", () => {
const array = []
object.__proto__ = { polluted: true }
return [
object.constructor, array.constructor, "".constructor, Math.constructor,
object.__proto__.polluted, ({}).polluted, array.__proto__, Object().__proto__, new Object().constructor,
typeof ({}).constructor, typeof [].__proto__,
object.constructor === Object, array.constructor === Array, "".constructor === String, Math.constructor,
object.__proto__.polluted, ({}).polluted, array.__proto__, Object().__proto__, new Object().constructor === Object,
({}).constructor.constructor, [].constructor.__proto__, typeof [].__proto__,
]
`,
),
).toEqual([null, null, null, null, true, null, null, null, null, "undefined", "undefined"])
).toEqual([true, true, true, null, true, null, null, null, true, null, null, "undefined"])
expect((await failure(runtime, `return (() => 1).constructor`)).message).toContain(
"Cannot read properties of a function",
)
const escape = await failure(runtime, `return ({}).constructor.constructor("return 1")()`)
const escape = await failure(runtime, `return ({}).constructor.constructor.constructor("return 1")()`)
expect(escape.message).toContain("Cannot access a property on a non-object value")
const poisoned = await failure(runtime, `const o = {}; o.__proto__.constructor("return 1")`)
expect(poisoned.message).toContain("Cannot access a property on a non-object value")