From c0aa963c13d14bb774624dc6c7de27d81569e71e Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:43:48 -0500 Subject: [PATCH] refactor(codemode): split the interpreter into a per-execution Runtime and per-call Frame (#48082) --- packages/codemode/src/interpreter/execute.ts | 5 +- packages/codemode/src/interpreter/runtime.ts | 101 ++++++++++--------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/packages/codemode/src/interpreter/execute.ts b/packages/codemode/src/interpreter/execute.ts index 773052f157f..c626ecb85c4 100644 --- a/packages/codemode/src/interpreter/execute.ts +++ b/packages/codemode/src/interpreter/execute.ts @@ -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 = ( code: string, @@ -39,8 +39,7 @@ export const executeProgram = ( Effect.gen(function* () { const program = parseProgram(code) const promises = new PromiseRuntime(scope) - const interpreter = new Interpreter(tools.execute, tools.search, tools.keys, promises, logs) - const value = yield* interpreter.run(program) + const value = yield* new Runtime(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() diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 5c0c206f743..aad0b255c53 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -197,43 +197,45 @@ type GeneratorState = { const promiseResolutionNode: AstNode = { type: "PromiseResolution", start: 0, end: 0 } -export class Interpreter { - private scopes: ScopeStack - private readonly executeTool: ( - path: ReadonlyArray, - args: Array, - ) => Effect.Effect - private readonly invokeSearch: (args: Array) => Effect.Effect - private readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray - private readonly logs: Array - private readonly promises: PromiseRuntime - private generatorState?: GeneratorState - private generatorAsync = false - private readonly runner: Runner = { - 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 { + readonly runner: Runner + private readonly root: Frame constructor( - executeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, - invokeSearch: (args: Array) => Effect.Effect, - toolKeys: (path: ReadonlyArray) => ReadonlyArray, - promises: PromiseRuntime, - logs: Array = [], + readonly executeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, + readonly search: (args: Array) => Effect.Effect, + readonly toolKeys: (path: ReadonlyArray) => ReadonlyArray, + readonly promises: PromiseRuntime, + readonly logs: Array = [], ) { const globalScope = new Map() - 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 { + return this.root.run(program) + } +} + +/** One activation: the top-level program or a single function call, evaluating against its own scope chain. */ +class Frame { + private generatorState?: GeneratorState + private generatorAsync = false + + constructor( + private readonly runtime: Runtime, + private scopes: ScopeStack, + ) {} + run(program: Program): Effect.Effect { const self = this // Keep top-level declarations separate so they can shadow builtins. @@ -260,7 +262,7 @@ export class Interpreter { } // 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 { path: ReadonlyArray, args: Array, ): Effect.Effect { - return this.createPromise(Effect.suspend(() => this.executeTool(path, args))) + return this.createPromise(Effect.suspend(() => this.runtime.executeTool(path, args))) } private createPromise(effect: Effect.Effect): Effect.Effect { - 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 { - const promises = this.promises + settlePromise(promise: Values.Promise): Effect.Effect { + 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 { } private awaitValue(value: unknown, node: AstNode = promiseResolutionNode): Effect.Effect { - 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 { }) } - 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 { private enumerableKeys(value: unknown): Array | 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 { } // The single dispatch for every invocation: call expressions and callbacks share it. - private invokeCallable( + invokeCallable( callable: unknown, args: Array, node: AstNode, @@ -1547,10 +1549,10 @@ export class Interpreter { 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).call(args, node) if (callable === undefined || callable === null) { @@ -1589,9 +1591,8 @@ export class Interpreter { }) } - private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { - 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): Effect.Effect { + 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 { // 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 { } private createGenerator( - invocation: Interpreter, + invocation: Frame, run: Effect.Effect, asynchronous: boolean, ): CodeModeGenerator { @@ -1648,7 +1651,7 @@ export class Interpreter { 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 { 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) })