mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 12:41:37 +03:00
refactor(codemode): split the interpreter into a per-execution Runtime and per-call Frame (#48082)
This commit is contained in:
committed by
GitHub
Unverified
parent
d55d941f3c
commit
c0aa963c13
@@ -9,7 +9,7 @@ import { ToolRuntime } from "../tool-runtime.js"
|
||||
import { normalizeError } from "./errors.js"
|
||||
import { InterpreterRuntimeError } from "./model.js"
|
||||
import { PromiseRuntime } from "./promises.js"
|
||||
import { Interpreter } from "./runtime.js"
|
||||
import { Runtime } from "./runtime.js"
|
||||
|
||||
export const executeProgram = <R>(
|
||||
code: string,
|
||||
@@ -39,8 +39,7 @@ export const executeProgram = <R>(
|
||||
Effect.gen(function* () {
|
||||
const program = parseProgram(code)
|
||||
const promises = new PromiseRuntime<R>(scope)
|
||||
const interpreter = new Interpreter<R>(tools.execute, tools.search, tools.keys, promises, logs)
|
||||
const value = yield* interpreter.run(program)
|
||||
const value = yield* new Runtime<R>(tools.execute, tools.search, tools.keys, promises, logs).run(program)
|
||||
const result = toData(value, "Execution result", "result") as DataValue
|
||||
returned = { value: result, promises }
|
||||
const warnings = yield* promises.interrupt()
|
||||
|
||||
@@ -197,43 +197,45 @@ type GeneratorState = {
|
||||
|
||||
const promiseResolutionNode: AstNode = { type: "PromiseResolution", start: 0, end: 0 }
|
||||
|
||||
export class Interpreter<R> {
|
||||
private scopes: ScopeStack
|
||||
private readonly executeTool: (
|
||||
path: ReadonlyArray<string>,
|
||||
args: Array<unknown>,
|
||||
) => Effect.Effect<unknown, unknown, R>
|
||||
private readonly invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||
private readonly logs: Array<string>
|
||||
private readonly promises: PromiseRuntime<R>
|
||||
private generatorState?: GeneratorState
|
||||
private generatorAsync = false
|
||||
private readonly runner: Runner<R> = {
|
||||
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
|
||||
invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node),
|
||||
settlePromise: (promise) => this.settlePromise(promise),
|
||||
syncIterator: (value, node) => this.syncIterator(value, node),
|
||||
}
|
||||
/** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */
|
||||
export class Runtime<R> {
|
||||
readonly runner: Runner<R>
|
||||
private readonly root: Frame<R>
|
||||
|
||||
constructor(
|
||||
executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||
promises: PromiseRuntime<R>,
|
||||
logs: Array<string> = [],
|
||||
readonly executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
|
||||
readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
|
||||
readonly promises: PromiseRuntime<R>,
|
||||
readonly logs: Array<string> = [],
|
||||
) {
|
||||
const globalScope = new Map<string, Binding>()
|
||||
this.scopes = new ScopeStack([globalScope])
|
||||
this.executeTool = executeTool
|
||||
this.invokeSearch = invokeSearch
|
||||
this.toolKeys = toolKeys
|
||||
this.logs = logs
|
||||
this.promises = promises
|
||||
const host = { runner: this.runner, promises, search: invokeSearch, toolKeys, logs }
|
||||
for (const [name, value] of globals(host)) globalScope.set(name, { mutable: false, value })
|
||||
// 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.runner = {
|
||||
invokeFunction: (fn, args) => this.root.invokeFunction(fn, args),
|
||||
invokeCallable: (callable, args, node) => this.root.invokeCallable(callable, args, node),
|
||||
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 })
|
||||
}
|
||||
|
||||
run(program: Program): Effect.Effect<unknown, unknown, R> {
|
||||
return this.root.run(program)
|
||||
}
|
||||
}
|
||||
|
||||
/** One activation: the top-level program or a single function call, evaluating against its own scope chain. */
|
||||
class Frame<R> {
|
||||
private generatorState?: GeneratorState
|
||||
private generatorAsync = false
|
||||
|
||||
constructor(
|
||||
private readonly runtime: Runtime<R>,
|
||||
private scopes: ScopeStack,
|
||||
) {}
|
||||
|
||||
run(program: Program): Effect.Effect<unknown, unknown, R> {
|
||||
const self = this
|
||||
// Keep top-level declarations separate so they can shadow builtins.
|
||||
@@ -260,7 +262,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
// The implicit async body adopts returned promises before copy-out.
|
||||
value = yield* resolvePromiseValue(self.runner, value, program)
|
||||
value = yield* resolvePromiseValue(self.runtime.runner, value, program)
|
||||
return value
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
||||
}
|
||||
@@ -270,16 +272,16 @@ export class Interpreter<R> {
|
||||
path: ReadonlyArray<string>,
|
||||
args: Array<unknown>,
|
||||
): Effect.Effect<Values.Promise, never, R> {
|
||||
return this.createPromise(Effect.suspend(() => this.executeTool(path, args)))
|
||||
return this.createPromise(Effect.suspend(() => this.runtime.executeTool(path, args)))
|
||||
}
|
||||
|
||||
private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<Values.Promise, never, R> {
|
||||
return this.promises.create(effect)
|
||||
return this.runtime.promises.create(effect)
|
||||
}
|
||||
|
||||
// Fiber exits make settlement idempotent; yielding prevents inline continuation.
|
||||
private settlePromise(promise: Values.Promise): Effect.Effect<unknown, unknown, never> {
|
||||
const promises = this.promises
|
||||
settlePromise(promise: Values.Promise): Effect.Effect<unknown, unknown, never> {
|
||||
const promises = this.runtime.promises
|
||||
return Effect.suspend(() => {
|
||||
promises.markObserved(promise)
|
||||
return Effect.flatMap(promises.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit))
|
||||
@@ -652,7 +654,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
private awaitValue(value: unknown, node: AstNode = promiseResolutionNode): Effect.Effect<unknown, unknown, R> {
|
||||
return Effect.flatMap(resolvePromise(this.runner, this.promises, value, node), (promise) =>
|
||||
return Effect.flatMap(resolvePromise(this.runtime.runner, this.runtime.promises, value, node), (promise) =>
|
||||
this.settlePromise(promise),
|
||||
)
|
||||
}
|
||||
@@ -674,7 +676,7 @@ export class Interpreter<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private syncIterator(value: unknown, node: AstNode) {
|
||||
syncIterator(value: unknown, node: AstNode) {
|
||||
const iterator = Array.isArray(value)
|
||||
? value[Symbol.iterator]()
|
||||
: typeof value === "string"
|
||||
@@ -824,7 +826,7 @@ export class Interpreter<R> {
|
||||
|
||||
private enumerableKeys(value: unknown): Array<string> | undefined {
|
||||
if (value instanceof ToolReference) {
|
||||
return [...this.toolKeys(value.path)]
|
||||
return [...this.runtime.toolKeys(value.path)]
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return Object.keys(value)
|
||||
@@ -1524,7 +1526,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
// The single dispatch for every invocation: call expressions and callbacks share it.
|
||||
private invokeCallable(
|
||||
invokeCallable(
|
||||
callable: unknown,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
@@ -1547,10 +1549,10 @@ export class Interpreter<R> {
|
||||
return callable.generator.asynchronous ? yield* self.createPromise(requested) : yield* requested
|
||||
}
|
||||
if (callable instanceof IntrinsicReference) {
|
||||
return yield* invokeIntrinsic(self.runner, callable, args, node)
|
||||
return yield* invokeIntrinsic(self.runtime.runner, callable, args, node)
|
||||
}
|
||||
if (callable instanceof PromiseInstanceMethodReference) {
|
||||
return yield* invokePromiseInstanceMethod(self.runner, self.promises, callable, args, node)
|
||||
return yield* invokePromiseInstanceMethod(self.runtime.runner, self.runtime.promises, callable, args, node)
|
||||
}
|
||||
if (callable instanceof HostFunction) return yield* (callable as HostFunction<R>).call(args, node)
|
||||
if (callable === undefined || callable === null) {
|
||||
@@ -1589,9 +1591,8 @@ export class Interpreter<R> {
|
||||
})
|
||||
}
|
||||
|
||||
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||
const invocation = new Interpreter(this.executeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs)
|
||||
invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
|
||||
invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||
const invocation = new Frame(this.runtime, new ScopeStack([...fn.capturedScopes, new Map()]))
|
||||
const run = Effect.gen(function* () {
|
||||
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
|
||||
const paramScope = invocation.scopes.current()
|
||||
@@ -1620,7 +1621,9 @@ export class Interpreter<R> {
|
||||
// The initial yield assigns the promise before the body can self-resolve.
|
||||
const box: { promise?: Values.Promise } = {}
|
||||
return Effect.map(
|
||||
this.createPromise(Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runner, value, fn.body, box))),
|
||||
this.createPromise(
|
||||
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, box)),
|
||||
),
|
||||
(promise) => {
|
||||
box.promise = promise
|
||||
return promise
|
||||
@@ -1629,7 +1632,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
private createGenerator(
|
||||
invocation: Interpreter<R>,
|
||||
invocation: Frame<R>,
|
||||
run: Effect.Effect<unknown, unknown, R>,
|
||||
asynchronous: boolean,
|
||||
): CodeModeGenerator {
|
||||
@@ -1648,7 +1651,7 @@ export class Interpreter<R> {
|
||||
if (state.draining) return Deferred.await(request.response)
|
||||
state.draining = true
|
||||
return Effect.andThen(
|
||||
this.promises.fork(
|
||||
this.runtime.promises.fork(
|
||||
invocation
|
||||
.completeGeneratorRequests(state, true)
|
||||
.pipe(Effect.ensuring(Effect.sync(() => (state.draining = false)))),
|
||||
@@ -1699,7 +1702,7 @@ export class Interpreter<R> {
|
||||
yield* invocation.completeGeneratorRequests(state, asynchronous)
|
||||
state.completed = true
|
||||
})
|
||||
return Effect.andThen(this.promises.fork(body), Deferred.await(request.response))
|
||||
return Effect.andThen(this.runtime.promises.fork(body), Deferred.await(request.response))
|
||||
}
|
||||
return Deferred.await(request.response)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user