refactor(codemode): name intrinsics deterministically

This commit is contained in:
James Long
2026-11-04 19:51:22 +00:00
Unverified
parent 72d958271d
commit 2daa79a85d
3 changed files with 148 additions and 1 deletions
@@ -128,6 +128,7 @@ import { describeValue, isOpaque, rejectCircularInsertion, typeofValue } from ".
import { fn } from "./native.js"
import { capability, promiseLike } from "./promises.js"
import { ScopeStack } from "./scope.js"
import { IntrinsicNames } from "./intrinsic-names.js"
import type { NodeId, Source } from "./source.js"
import { constructRegExp } from "../stdlib/regexp.js"
import { enumerableSource } from "../stdlib/object.js"
@@ -388,6 +389,8 @@ export class Interpreter<R> {
readonly runner: Runner<R>
readonly builtins: Builtins
readonly logs: Array<string>
/** Names for this runtime's intrinsics, taken before any program code can change a built-in. */
readonly intrinsics: IntrinsicNames
/** Template objects by site: a tag sees the same `strings` array every time its literal is evaluated, as in JS. */
readonly templates = new WeakMap<TaggedTemplateExpression, Arr>()
/** The parsed sources this execution runs, by id, so a `NodeId` resolves to its node. */
@@ -414,7 +417,9 @@ export class Interpreter<R> {
// Calling back into the program never reads frame state, so any frame serves; the root is always alive.
this.root = new Frame(this, new ScopeStack([globalScope]))
this.globalScope = globalScope
for (const [name, value] of [...globals(this), ...(options.globals?.(this) ?? [])]) {
const intrinsics = globals(this)
this.intrinsics = new IntrinsicNames(intrinsics, this.builtins)
for (const [name, value] of [...intrinsics, ...(options.globals?.(this) ?? [])]) {
globalScope.set(name, { mutable: false, value })
}
}
@@ -0,0 +1,78 @@
import type { Builtins } from "./intrinsics.js"
import { AsyncIteratorSymbol, IteratorSymbol } from "./model.js"
import { Obj, type Value } from "./objects.js"
/**
* Deterministic names for one runtime's intrinsic objects, so saved state can refer to a built-in by name instead of
* copying it: a breadth-first walk from the global names (`Array.prototype.map`), then `%Name%` for built-in
* prototypes the walk cannot reach (`%Generator%`) and what is reachable from them. The first path found names a value.
*
* The walk runs when the runtime is created, before any program can change a built-in; the names themselves are
* spelled out only when asked for.
*/
export class IntrinsicNames {
/** The intrinsics in walk order. */
readonly values: Array<Obj> = []
private readonly index = new Map<Obj, number>()
// How each value was reached: a root name, or its parent's index and the property key.
private readonly paths: Array<string | readonly [parent: number, key: string | symbol]> = []
private names: Array<string> | undefined
private lookup: Map<string, Obj> | undefined
constructor(globals: ReadonlyArray<readonly [string, Value]>, builtins: Builtins) {
let next = 0
const visit = (value: Value, path: string | readonly [number, string | symbol]) => {
if (!(value instanceof Obj) || this.index.has(value)) return
this.index.set(value, this.values.length)
this.values.push(value)
this.paths.push(path)
}
const drain = () => {
for (; next < this.values.length; next++) {
const item = this.values[next]!
for (const [key, slot] of item.props) if ("value" in slot) visit(slot.value, [next, key])
if (item.proto !== null) visit(item.proto, [next, prototypeKey])
}
}
for (const [name, value] of globals) visit(value, name)
drain()
for (const [name, value] of Object.entries(builtins)) {
visit(value, `%${name}%`)
drain()
}
}
/** The name of an intrinsic, or undefined for any other object. */
nameOf(value: Obj): string | undefined {
const at = this.index.get(value)
return at === undefined ? undefined : this.all()[at]
}
/** The intrinsic a name refers to. */
valueOf(name: string): Obj | undefined {
this.lookup ??= new Map(this.all().map((name, index) => [name, this.values[index]!]))
return this.lookup.get(name)
}
/** Every name, in walk order. */
all(): ReadonlyArray<string> {
if (this.names !== undefined) return this.names
const names: Array<string> = []
for (const path of this.paths) names.push(typeof path === "string" ? path : member(names[path[0]]!, path[1]))
this.names = names
return names
}
}
const prototypeKey: unique symbol = Symbol("[[Prototype]]")
const symbolNames = new Map<symbol, string>([
[IteratorSymbol, "[Symbol.iterator]"],
[AsyncIteratorSymbol, "[Symbol.asyncIterator]"],
[prototypeKey, ".[[Prototype]]"],
])
const member = (base: string, key: string | symbol): string => {
if (typeof key === "symbol") return `${base}${symbolNames.get(key) ?? `[${String(key)}]`}`
return /^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}[${JSON.stringify(key)}]`
}
+64
View File
@@ -1,9 +1,21 @@
import { describe, expect, test } from "bun:test"
import { Context } from "effect"
import { parseProgram } from "../src/interpreter/execute.js"
import { Interpreter } from "../src/interpreter/interpreter.js"
import { createBuiltins } from "../src/interpreter/intrinsics.js"
import { Native, Obj } from "../src/interpreter/objects.js"
import { ToolRuntime } from "../src/tool-runtime.js"
import { nodeId, sourceOf } from "../src/interpreter/source.js"
// Groundwork for suspending and resuming a run: everything the state refers to has a stable name.
const interpreter = () =>
new Interpreter<never>({
tools: ToolRuntime.make(ToolRuntime.prepare<never>({}), undefined, {}),
services: Context.empty(),
builtins: createBuiltins(),
})
const parse = (code: string) => {
const parsed = parseProgram(code)
if (!parsed.ok) throw new Error(parsed.error.message)
@@ -44,3 +56,55 @@ describe("node ids", () => {
expect(parse(code).source.node({ source: "other", index: 0 })).toBeUndefined()
})
})
describe("intrinsic names", () => {
test("name every intrinsic once, from the globals first", () => {
const names = interpreter().intrinsics
const all = names.all()
expect(new Set(all).size).toBe(all.length)
expect(new Set(names.values).size).toBe(names.values.length)
for (const name of [
"Array.prototype.map",
"Promise.prototype.then",
"String.prototype[Symbol.iterator]",
"JSON.parse",
]) {
expect(all).toContain(name)
}
for (const name of ["%Generator%", "%Generator%.next", "%AsyncGenerator%.return", "%IteratorHelper%.return"]) {
expect(all).toContain(name)
}
all.forEach((name, index) => {
expect(names.valueOf(name)).toBe(names.values[index]!)
expect(names.nameOf(names.values[index]!)).toBe(name)
})
})
test("cover everything reachable from a named intrinsic", () => {
const names = interpreter().intrinsics
for (const value of names.values) {
for (const slot of value.props.values()) {
if ("value" in slot && slot.value instanceof Obj) expect(names.nameOf(slot.value)).toBeDefined()
}
if (value.proto !== null) expect(names.nameOf(value.proto)).toBeDefined()
}
expect(names.values.filter((value) => value instanceof Native).length).toBeGreaterThan(300)
})
test("are identical across fresh runtimes", () => {
const shape = (names: Interpreter<never>["intrinsics"]) =>
names.all().map((name, index) => {
const value = names.values[index]!
return [name, value.constructor.name, [...value.props.keys()].map(String)]
})
expect(shape(interpreter().intrinsics)).toEqual(shape(interpreter().intrinsics))
})
test("are taken before the program can change a built-in", () => {
const ctx = interpreter()
const map = ctx.intrinsics.valueOf("Array.prototype.map")!
const array = ctx.intrinsics.valueOf("Array.prototype")!
array.props.delete("map")
expect(ctx.intrinsics.nameOf(map)).toBe("Array.prototype.map")
})
})