From 2daa79a85ddf63fd677079329a89a3026092f700 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 4 Nov 2026 19:51:22 +0000 Subject: [PATCH] refactor(codemode): name intrinsics deterministically --- .../codemode/src/interpreter/interpreter.ts | 7 +- .../src/interpreter/intrinsic-names.ts | 78 +++++++++++++++++++ packages/codemode/test/resumable.test.ts | 64 +++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/codemode/src/interpreter/intrinsic-names.ts diff --git a/packages/codemode/src/interpreter/interpreter.ts b/packages/codemode/src/interpreter/interpreter.ts index 6e397518ab6..97846dd47ba 100644 --- a/packages/codemode/src/interpreter/interpreter.ts +++ b/packages/codemode/src/interpreter/interpreter.ts @@ -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 { readonly runner: Runner readonly builtins: Builtins readonly logs: Array + /** 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() /** The parsed sources this execution runs, by id, so a `NodeId` resolves to its node. */ @@ -414,7 +417,9 @@ export class Interpreter { // 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 }) } } diff --git a/packages/codemode/src/interpreter/intrinsic-names.ts b/packages/codemode/src/interpreter/intrinsic-names.ts new file mode 100644 index 00000000000..840a55f878b --- /dev/null +++ b/packages/codemode/src/interpreter/intrinsic-names.ts @@ -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 = [] + private readonly index = new Map() + // How each value was reached: a root name, or its parent's index and the property key. + private readonly paths: Array = [] + private names: Array | undefined + private lookup: Map | undefined + + constructor(globals: ReadonlyArray, 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 { + if (this.names !== undefined) return this.names + const names: Array = [] + 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([ + [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)}]` +} diff --git a/packages/codemode/test/resumable.test.ts b/packages/codemode/test/resumable.test.ts index 38f66fe79e9..c6fda649c45 100644 --- a/packages/codemode/test/resumable.test.ts +++ b/packages/codemode/test/resumable.test.ts @@ -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({ + tools: ToolRuntime.make(ToolRuntime.prepare({}), 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["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") + }) +})