mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
refactor(codemode): name intrinsics deterministically
This commit is contained in:
@@ -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)}]`
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user