blob: 6d68a9f7cb85c219e4ef74588a0558d9156b62ce [file]
import { parse } from 'acorn';
import {
createSourceFile,
DiagnosticCategory,
isFunctionDeclaration,
ModuleKind,
ScriptTarget,
flattenDiagnosticMessageText,
transpileModule,
} from 'typescript';
import {
DEFAULT_CODE_MODE_LIMITS,
type CodeModeDiagnosticKind,
type CodeModeExecutionResult,
type CodeModeLimits,
type CodeModeToolCall,
type ExecuteCodeCellInput,
serializedByteLength,
} from './index.js';
interface AstNode {
type: string;
loc?: {
start: { line: number; column: number };
} | null;
[key: string]: unknown;
}
class InterpreterError extends Error {
constructor(
readonly kind: CodeModeDiagnosticKind,
message: string,
readonly node?: AstNode,
) {
super(message);
this.name = 'InterpreterError';
}
}
class ReturnSignal {
constructor(readonly value: unknown) {}
}
class BreakSignal {}
class ContinueSignal {}
class Environment {
private readonly values = new Map<string, { value: unknown; constant: boolean }>();
constructor(private readonly parent?: Environment) {}
declare(name: string, value: unknown, constant: boolean): void {
if (this.values.has(name)) {
throw new InterpreterError('execution_error', `Identifier "${name}" is already declared`);
}
this.values.set(name, { value, constant });
}
get(name: string, node: AstNode): unknown {
if (this.values.has(name)) return this.values.get(name)?.value;
if (this.parent) return this.parent.get(name, node);
throw new InterpreterError('execution_error', `Unknown identifier "${name}"`, node);
}
set(name: string, value: unknown, node: AstNode): unknown {
const binding = this.values.get(name);
if (binding) {
if (binding.constant) {
throw new InterpreterError('execution_error', `Cannot assign to constant "${name}"`, node);
}
binding.value = value;
return value;
}
if (this.parent) return this.parent.set(name, value, node);
throw new InterpreterError('execution_error', `Unknown identifier "${name}"`, node);
}
}
class ToolReference {
constructor(readonly name: string) {}
}
class PromiseMethodReference {
constructor(readonly name: 'all' | 'allSettled' | 'resolve' | 'reject') {}
}
class CellPromise {
private observed = false;
constructor(readonly promise: Promise<unknown>) {
promise.catch(() => {});
}
observe(): Promise<unknown> {
this.observed = true;
return this.promise;
}
isObserved(): boolean {
return this.observed;
}
}
class CellFunction {
constructor(
readonly parameters: readonly AstNode[],
readonly body: AstNode,
readonly expressionBody: boolean,
readonly closure: Environment,
) {}
}
class ArrayMethodReference {
constructor(
readonly target: unknown[],
readonly name: 'map' | 'filter' | 'push' | 'sort' | 'join' | 'includes' | 'slice',
) {}
}
class StringMethodReference {
constructor(
readonly target: string,
readonly name:
| 'trim'
| 'toLowerCase'
| 'toUpperCase'
| 'includes'
| 'startsWith'
| 'endsWith'
| 'slice'
| 'substring'
| 'split',
) {}
}
class DataMethodReference {
constructor(
readonly root: 'JSON' | 'Object',
readonly name: string,
) {}
}
const TOOL_ROOT = Symbol('tools');
const PROMISE_ROOT = Symbol('Promise');
const JSON_ROOT = Symbol('JSON');
const OBJECT_ROOT = Symbol('Object');
const OPTIONAL_CHAIN_SHORT_CIRCUIT = Symbol('optional-chain-short-circuit');
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
const MEMBER_ROOT_BINDINGS = new Set(['tools', 'Promise', 'JSON', 'Object']);
const RESERVED_ROOT_BINDINGS = new Set([...MEMBER_ROOT_BINDINGS, 'undefined']);
const SUPPORTED_SYNTAX_NODES = new Set([
'Program',
'BlockStatement',
'VariableDeclaration',
'VariableDeclarator',
'ReturnStatement',
'ExpressionStatement',
'IfStatement',
'ForOfStatement',
'ForStatement',
'WhileStatement',
'DoWhileStatement',
'TryStatement',
'CatchClause',
'ThrowStatement',
'BreakStatement',
'ContinueStatement',
'EmptyStatement',
'Literal',
'Identifier',
'ArrowFunctionExpression',
'AwaitExpression',
'ChainExpression',
'ObjectExpression',
'ArrayExpression',
'MemberExpression',
'CallExpression',
'BinaryExpression',
'LogicalExpression',
'ConditionalExpression',
'UnaryExpression',
'AssignmentExpression',
'UpdateExpression',
'TemplateLiteral',
'TemplateElement',
'Property',
'SpreadElement',
'AssignmentPattern',
'ArrayPattern',
'ObjectPattern',
'RestElement',
]);
const SUPPORTED_BINARY_OPERATORS = new Set([
'===',
'!==',
'==',
'!=',
'<',
'<=',
'>',
'>=',
'+',
'-',
'*',
'/',
'%',
]);
const SUPPORTED_LOGICAL_OPERATORS = new Set(['&&', '||', '??']);
const SUPPORTED_UNARY_OPERATORS = new Set(['!', '+', '-', 'typeof', 'void']);
const SUPPORTED_ASSIGNMENT_OPERATORS = new Set(['=', '+=', '-=', '*=', '/=', '%=']);
const SUPPORTED_UPDATE_OPERATORS = new Set(['++', '--']);
const SUPPORTED_PROMISE_METHODS = new Set(['all', 'allSettled', 'resolve', 'reject']);
const SUPPORTED_DATA_METHODS = new Set([
'parse',
'stringify',
'keys',
'values',
'entries',
'fromEntries',
]);
const SUPPORTED_COLLECTION_METHODS = new Set([
'map',
'filter',
'push',
'sort',
'join',
'includes',
'slice',
'trim',
'toLowerCase',
'toUpperCase',
'startsWith',
'endsWith',
'split',
'substring',
]);
interface MaterializationOptions {
label: string;
maxDepth: number;
maxCollectionItems: number;
preserveUndefined?: boolean;
maxBytes?: number;
byteLimitLabel?: string;
}
const MAX_COPY_NODES = 100_000;
class Interpreter {
private readonly limits: CodeModeLimits;
private readonly startedAt = Date.now();
private readonly toolNames: Set<string>;
private readonly toolCalls: CodeModeToolCall[] = [];
private readonly cellAbortController = new AbortController();
private readonly ownedPromises: CellPromise[] = [];
private readonly hostToolOperations: Promise<unknown>[] = [];
private readonly wallTimeTimer: ReturnType<typeof setTimeout>;
private readonly forwardInputAbort: () => void;
private terminalFailure: { reason: unknown } | undefined;
private steps = 0;
private activeToolCalls = 0;
constructor(private readonly input: ExecuteCodeCellInput) {
this.limits = { ...DEFAULT_CODE_MODE_LIMITS, ...input.limits };
this.toolNames = new Set(input.tools.map((tool) => tool.name));
this.forwardInputAbort = () => {
if (!this.cellAbortController.signal.aborted) {
this.cellAbortController.abort(input.signal ? abortReason(input.signal) : undefined);
}
};
if (input.signal?.aborted) this.forwardInputAbort();
else input.signal?.addEventListener('abort', this.forwardInputAbort, { once: true });
this.wallTimeTimer = setTimeout(() => {
if (!this.cellAbortController.signal.aborted) {
this.cellAbortController.abort(
new InterpreterError('limit_exceeded', 'Cell wall-time limit exceeded'),
);
}
}, this.limits.maxWallTimeMs);
}
async execute(program: AstNode): Promise<CodeModeExecutionResult> {
const environment = new Environment();
environment.declare('tools', TOOL_ROOT, true);
environment.declare('Promise', PROMISE_ROOT, true);
environment.declare('JSON', JSON_ROOT, true);
environment.declare('Object', OBJECT_ROOT, true);
environment.declare('undefined', undefined, true);
try {
let value: unknown = null;
try {
await this.evaluateStatements(nodes(program, 'body'), environment);
} catch (signal) {
if (!(signal instanceof ReturnSignal)) throw signal;
value = await resolveCellValue(signal.value);
}
this.throwIfTerminalFailure();
await this.superviseUnobservedPromises();
this.throwIfTerminalFailure();
const copied = materializePlainData(value, {
label: 'execution result',
maxDepth: this.limits.maxDataDepth,
maxCollectionItems: this.limits.maxCollectionItems,
maxBytes: this.limits.maxOutputBytes,
byteLimitLabel: 'output',
});
return { ok: true, value: copied, toolCalls: [...this.toolCalls] };
} catch (error) {
if (!this.cellAbortController.signal.aborted) this.cellAbortController.abort(error);
const drained = await Promise.allSettled([
...this.ownedPromises.map((promise) => promise.promise),
...this.hostToolOperations,
]);
if (this.input.signal?.aborted) throw abortReason(this.input.signal);
if (this.input.isFatalToolError?.(error)) throw error;
const fatalDrain = drained.find(
(settled) => settled.status === 'rejected' && this.input.isFatalToolError?.(settled.reason),
);
if (fatalDrain?.status === 'rejected') throw fatalDrain.reason;
const normalized = normalizeError(error);
return { ok: false, error: normalized, toolCalls: [...this.toolCalls] };
} finally {
clearTimeout(this.wallTimeTimer);
this.input.signal?.removeEventListener('abort', this.forwardInputAbort);
}
}
private async superviseUnobservedPromises(): Promise<void> {
for (;;) {
const unobserved = this.ownedPromises.filter((promise) => !promise.isObserved());
if (unobserved.length === 0) return;
await Promise.all(unobserved.map((promise) => promise.observe()));
}
}
private ownPromise(promise: Promise<unknown>): CellPromise {
const owned = new CellPromise(promise);
this.ownedPromises.push(owned);
return owned;
}
private throwIfTerminalFailure(): void {
if (this.input.signal?.aborted) throw abortReason(this.input.signal);
if (this.cellAbortController.signal.aborted) {
throw abortReason(this.cellAbortController.signal);
}
if (this.terminalFailure) throw this.terminalFailure.reason;
}
private async evaluateStatements(
statements: readonly AstNode[],
env: Environment,
): Promise<void> {
for (const statement of statements) await this.evaluateStatement(statement, env);
}
private async evaluateStatement(node: AstNode, env: Environment): Promise<void> {
await this.tick(node);
switch (node.type) {
case 'VariableDeclaration': {
const constant = field(node, 'kind') === 'const';
for (const declaration of nodes(node, 'declarations')) {
const id = ast(declaration, 'id');
const init = optionalAst(declaration, 'init');
await this.declarePattern(
id,
init ? await resolveCellValue(await this.evaluateExpression(init, env)) : undefined,
env,
constant,
);
}
return;
}
case 'ReturnStatement': {
const argument = optionalAst(node, 'argument');
throw new ReturnSignal(argument ? await this.evaluateExpression(argument, env) : null);
}
case 'ExpressionStatement':
await this.evaluateExpression(ast(node, 'expression'), env);
return;
case 'BlockStatement':
await this.evaluateStatements(nodes(node, 'body'), new Environment(env));
return;
case 'IfStatement': {
const branch = (await this.evaluateExpression(ast(node, 'test'), env))
? ast(node, 'consequent')
: optionalAst(node, 'alternate');
if (branch) await this.evaluateStatement(branch, env);
return;
}
case 'ForOfStatement': {
if (field(node, 'await') === true) {
throw unsupported(node, 'for await is not supported');
}
const iterable = await resolveCellValue(
await this.evaluateExpression(ast(node, 'right'), env),
);
let values: readonly unknown[] | string | undefined;
if (Array.isArray(iterable)) {
this.assertCollectionSize(iterable.length, 'for...of', node);
values = iterable;
} else if (typeof iterable === 'string') {
let itemCount = 0;
for (const _character of iterable) {
itemCount += 1;
this.assertCollectionSize(itemCount, 'for...of', node);
}
values = iterable;
}
if (values === undefined) {
throw new InterpreterError(
'execution_error',
'for...of expects an array or string',
node,
);
}
const left = ast(node, 'left');
for (const value of values) {
await this.tick(node);
const iterationEnv = new Environment(env);
if (left.type === 'VariableDeclaration') {
const declarations = nodes(left, 'declarations');
if (declarations.length !== 1) {
throw unsupported(left, 'for...of requires one variable declaration');
}
await this.declarePattern(
ast(declarations[0] as AstNode, 'id'),
value,
iterationEnv,
field(left, 'kind') === 'const',
);
} else {
this.assignPattern(left, value, env);
}
try {
await this.evaluateStatement(ast(node, 'body'), iterationEnv);
} catch (signal) {
if (signal instanceof ContinueSignal) continue;
if (signal instanceof BreakSignal) break;
throw signal;
}
}
return;
}
case 'ForStatement': {
const loopEnv = new Environment(env);
const init = optionalAst(node, 'init');
if (init) {
if (init.type === 'VariableDeclaration') await this.evaluateStatement(init, loopEnv);
else await this.evaluateExpression(init, loopEnv);
}
for (;;) {
await this.tick(node);
const test = optionalAst(node, 'test');
if (test && !(await this.evaluateExpression(test, loopEnv))) break;
const action = await this.evaluateLoopBody(ast(node, 'body'), loopEnv);
if (action === 'break') break;
const update = optionalAst(node, 'update');
if (update) await this.evaluateExpression(update, loopEnv);
}
return;
}
case 'WhileStatement':
case 'DoWhileStatement': {
let first = true;
for (;;) {
await this.tick(node);
if (
(!first || node.type === 'WhileStatement') &&
!(await this.evaluateExpression(ast(node, 'test'), env))
) {
break;
}
first = false;
if ((await this.evaluateLoopBody(ast(node, 'body'), env)) === 'break') break;
}
return;
}
case 'TryStatement': {
try {
await this.evaluateStatement(ast(node, 'block'), env);
} catch (error) {
if (
error instanceof ReturnSignal ||
error instanceof BreakSignal ||
error instanceof ContinueSignal
) {
throw error;
}
const handler = optionalAst(node, 'handler');
if (!handler) throw error;
const catchEnv = new Environment(env);
const parameter = optionalAst(handler, 'param');
if (parameter) {
await this.declarePattern(parameter, caughtReason(error), catchEnv, false);
}
await this.evaluateStatement(ast(handler, 'body'), catchEnv);
} finally {
const finalizer = optionalAst(node, 'finalizer');
if (finalizer) await this.evaluateStatement(finalizer, env);
}
return;
}
case 'ThrowStatement':
throw await resolveCellValue(await this.evaluateExpression(ast(node, 'argument'), env));
case 'BreakStatement':
throw new BreakSignal();
case 'ContinueStatement':
throw new ContinueSignal();
case 'EmptyStatement':
return;
default:
throw unsupported(node, `Unsupported statement: ${node.type}`);
}
}
private async evaluateExpression(node: AstNode, env: Environment): Promise<unknown> {
await this.tick(node);
switch (node.type) {
case 'Literal':
return field(node, 'value');
case 'Identifier':
return env.get(stringField(node, 'name'), node);
case 'ArrowFunctionExpression':
return new CellFunction(
nodes(node, 'params'),
ast(node, 'body'),
field(node, 'expression') === true,
env,
);
case 'AwaitExpression':
return await resolveCellValue(await this.evaluateExpression(ast(node, 'argument'), env));
case 'ChainExpression': {
const value = await this.evaluateExpression(ast(node, 'expression'), env);
return value === OPTIONAL_CHAIN_SHORT_CIRCUIT ? undefined : value;
}
case 'ObjectExpression': {
const output: Record<string, unknown> = {};
let outputItems = 0;
for (const property of nodes(node, 'properties')) {
if (property.type === 'SpreadElement') {
const spread = await resolveCellValue(
await this.evaluateExpression(ast(property, 'argument'), env),
);
if (!isPlainRecord(spread)) {
throw new InterpreterError(
'invalid_data',
'Object spread expects plain data',
property,
);
}
const spreadEntries = Object.entries(spread);
let spreadItems = 0;
for (const [key] of spreadEntries) {
assertSafeKey(key, property);
if (!Object.hasOwn(output, key)) spreadItems += 1;
}
this.assertCollectionSize(outputItems + spreadItems, 'object spread', node);
for (const [key, value] of spreadEntries) {
output[key] = value;
}
outputItems += spreadItems;
continue;
}
if (property.type !== 'Property' || field(property, 'kind') !== 'init') {
throw unsupported(property, 'Only ordinary object properties are supported');
}
const key = propertyName(property, env, this);
assertSafeKey(key, property);
const isNewKey = !Object.hasOwn(output, key);
if (isNewKey) this.assertCollectionSize(outputItems + 1, 'object literal', node);
output[key] = await this.evaluateExpression(ast(property, 'value'), env);
if (isNewKey) outputItems += 1;
}
return output;
}
case 'ArrayExpression': {
const output: unknown[] = [];
for (const element of arrayField(node, 'elements')) {
if (element === null) {
this.assertCollectionSize(output.length + 1, 'array literal', node);
output.push(null);
} else {
const elementNode = asAst(element, node);
if (elementNode.type === 'SpreadElement') {
const spread = await resolveCellValue(
await this.evaluateExpression(ast(elementNode, 'argument'), env),
);
if (!Array.isArray(spread) && typeof spread !== 'string') {
throw new InterpreterError(
'invalid_data',
'Array spread expects an array or string',
elementNode,
);
}
if (Array.isArray(spread)) {
this.assertCollectionSize(output.length + spread.length, 'array spread', node);
} else {
let spreadItems = 0;
for (const _item of spread) {
spreadItems += 1;
this.assertCollectionSize(output.length + spreadItems, 'array spread', node);
}
}
output.push(...spread);
} else {
output.push(await this.evaluateExpression(elementNode, env));
this.assertCollectionSize(output.length, 'array literal', node);
}
}
}
return output;
}
case 'MemberExpression': {
const object = await resolveCellValue(
await this.evaluateExpression(ast(node, 'object'), env),
);
if (object === OPTIONAL_CHAIN_SHORT_CIRCUIT) return OPTIONAL_CHAIN_SHORT_CIRCUIT;
if (field(node, 'optional') === true && (object === null || object === undefined)) {
return OPTIONAL_CHAIN_SHORT_CIRCUIT;
}
const property = await this.memberName(node, env);
assertSafeKey(property, node);
if (object === TOOL_ROOT) {
if (!this.toolNames.has(property)) {
throw new InterpreterError(
'unknown_tool',
`Unknown or inactive tool "${property}"`,
node,
);
}
return new ToolReference(property);
}
if (object === PROMISE_ROOT) {
if (property === 'race') {
throw unsupported(
node,
'Promise.race is not supported because nested tool calls cannot escape a cell',
);
}
if (!['all', 'allSettled', 'resolve', 'reject'].includes(property)) {
throw new InterpreterError(
'execution_error',
`Unsupported Promise method "${property}"`,
node,
);
}
return new PromiseMethodReference(property as PromiseMethodReference['name']);
}
if (object === JSON_ROOT && ['parse', 'stringify'].includes(property)) {
return new DataMethodReference('JSON', property);
}
if (
object === OBJECT_ROOT &&
['keys', 'values', 'entries', 'fromEntries'].includes(property)
) {
return new DataMethodReference('Object', property);
}
if (
Array.isArray(object) &&
['map', 'filter', 'push', 'sort', 'join', 'includes', 'slice'].includes(property)
) {
return new ArrayMethodReference(object, property as ArrayMethodReference['name']);
}
if (typeof object === 'string') {
if (property === 'length') return object.length;
const index = stringIndex(property);
if (index !== undefined) return object[index];
if (
[
'trim',
'toLowerCase',
'toUpperCase',
'includes',
'startsWith',
'endsWith',
'slice',
'substring',
'split',
].includes(property)
) {
return new StringMethodReference(object, property as StringMethodReference['name']);
}
}
if ((Array.isArray(object) || isPlainRecord(object)) && Object.hasOwn(object, property)) {
return object[property as keyof typeof object];
}
if (object === null || object === undefined) {
throw new InterpreterError('execution_error', `Cannot read property "${property}"`, node);
}
return undefined;
}
case 'CallExpression': {
const callee = await this.evaluateExpression(ast(node, 'callee'), env);
if (callee === OPTIONAL_CHAIN_SHORT_CIRCUIT) return OPTIONAL_CHAIN_SHORT_CIRCUIT;
if (field(node, 'optional') === true && (callee === null || callee === undefined)) {
return OPTIONAL_CHAIN_SHORT_CIRCUIT;
}
if (callee instanceof DataMethodReference) {
return this.callDataMethod(callee, await this.evaluateCallArguments(node, env), node);
}
if (callee instanceof StringMethodReference) {
return this.callStringMethod(callee, await this.evaluateCallArguments(node, env), node);
}
if (callee instanceof PromiseMethodReference) {
const args = nodes(node, 'arguments');
if (args.length !== 1 || args[0]?.type === 'SpreadElement') {
throw new InterpreterError(
'execution_error',
`Promise.${callee.name} expects one argument`,
node,
);
}
const argument = await this.evaluateExpression(args[0], env);
return this.callPromiseMethod(callee.name, argument, node);
}
if (callee instanceof ArrayMethodReference) {
const args = nodes(node, 'arguments');
if (args.some((argument) => argument.type === 'SpreadElement')) {
throw new InterpreterError(
'execution_error',
`Array.${callee.name} does not accept spread arguments`,
node,
);
}
if (callee.name === 'push') {
const values = [];
for (const argument of args) {
values.push(await this.evaluateExpression(argument, env));
}
this.assertCollectionSize(callee.target.length + values.length, 'Array.push', node);
callee.target.push(...values);
return callee.target.length;
}
if (callee.name === 'sort') {
if (args.length !== 0) {
throw unsupported(node, 'Array.sort comparators are not supported');
}
this.sortUtf16Values(callee.target, node);
return callee.target;
}
if (callee.name === 'join') {
if (args.length > 1) {
throw new InterpreterError(
'execution_error',
'Array.join expects zero or one argument',
node,
);
}
const separatorValue = args[0]
? await resolveCellValue(await this.evaluateExpression(args[0], env))
: undefined;
const separator =
separatorValue === undefined
? ','
: this.coerceToIntermediateString(separatorValue, 'Array.join separator', node);
this.assertJoinedString(callee.target, separator, 'Array.join', node);
return callee.target.join(separator);
}
if (callee.name === 'includes') {
if (args.length !== 1) {
throw new InterpreterError(
'execution_error',
'Array.includes expects one argument',
node,
);
}
return callee.target.includes(
await resolveCellValue(await this.evaluateExpression(args[0] as AstNode, env)),
);
}
if (callee.name === 'slice') {
if (args.length > 2) {
throw new InterpreterError(
'execution_error',
'Array.slice expects at most two arguments',
node,
);
}
const values = await this.evaluateCallArguments(node, env);
const start = values[0] === undefined ? 0 : Number(values[0]);
const end = values[1] === undefined ? callee.target.length : Number(values[1]);
const normalizedStart = Math.max(
0,
Math.min(callee.target.length, start < 0 ? callee.target.length + start : start),
);
const normalizedEnd = Math.max(
normalizedStart,
Math.min(callee.target.length, end < 0 ? callee.target.length + end : end),
);
this.assertCollectionSize(normalizedEnd - normalizedStart, 'Array.slice', node);
return callee.target.slice(
values[0] === undefined ? undefined : start,
values[1] === undefined ? undefined : end,
);
}
if (args.length !== 1) {
throw new InterpreterError(
'execution_error',
`Array.${callee.name} expects one callback`,
node,
);
}
const callback = await this.evaluateExpression(args[0], env);
if (!(callback instanceof CellFunction)) {
throw new InterpreterError(
'execution_error',
`Array.${callee.name} expects a function`,
node,
);
}
return this.callArrayMethod(callee, callback, node);
}
if (!(callee instanceof ToolReference)) {
throw unsupported(node, 'Only calls to tools.* are supported in this expression');
}
const args = nodes(node, 'arguments');
if (args.length > 1 || args.some((argument) => argument.type === 'SpreadElement')) {
throw new InterpreterError(
'execution_error',
'Tool calls accept one argument object',
node,
);
}
const argument = args[0] ? await this.evaluateExpression(args[0], env) : {};
return this.ownPromise(this.callTool(callee.name, argument, node));
}
case 'BinaryExpression':
return this.evaluateBinary(
stringField(node, 'operator'),
await this.evaluateExpression(ast(node, 'left'), env),
await this.evaluateExpression(ast(node, 'right'), env),
node,
);
case 'LogicalExpression': {
const operator = stringField(node, 'operator');
const left = await this.evaluateExpression(ast(node, 'left'), env);
if (operator === '&&') {
return left ? this.evaluateExpression(ast(node, 'right'), env) : left;
}
if (operator === '||') {
return left ? left : this.evaluateExpression(ast(node, 'right'), env);
}
if (operator === '??') {
return left !== null && left !== undefined
? left
: this.evaluateExpression(ast(node, 'right'), env);
}
throw unsupported(node, `Unsupported logical operator: ${operator}`);
}
case 'ConditionalExpression':
return (await this.evaluateExpression(ast(node, 'test'), env))
? this.evaluateExpression(ast(node, 'consequent'), env)
: this.evaluateExpression(ast(node, 'alternate'), env);
case 'UnaryExpression':
return this.evaluateUnary(
stringField(node, 'operator'),
await this.evaluateExpression(ast(node, 'argument'), env),
node,
);
case 'AssignmentExpression': {
const operator = stringField(node, 'operator');
const left = ast(node, 'left');
const right = await this.evaluateExpression(ast(node, 'right'), env);
if (operator === '=') return this.assignPattern(left, right, env);
if (left.type !== 'Identifier') {
throw unsupported(left, 'Compound assignment requires an identifier');
}
const current = env.get(stringField(left, 'name'), left);
const next = this.evaluateBinary(operator.slice(0, -1), current, right, node);
return env.set(stringField(left, 'name'), next, left);
}
case 'UpdateExpression': {
const argument = ast(node, 'argument');
if (argument.type !== 'Identifier') {
throw unsupported(argument, 'Update expressions require an identifier');
}
const name = stringField(argument, 'name');
const current = env.get(name, argument);
if (typeof current !== 'number') {
throw new InterpreterError(
'execution_error',
'Update expressions require a number',
node,
);
}
const operator = stringField(node, 'operator');
const next = operator === '++' ? current + 1 : operator === '--' ? current - 1 : undefined;
if (next === undefined) throw unsupported(node, `Unsupported update operator: ${operator}`);
env.set(name, next, argument);
return field(node, 'prefix') === true ? next : current;
}
case 'TemplateLiteral': {
const quasis = nodes(node, 'quasis');
const expressions = nodes(node, 'expressions');
let output = '';
for (let index = 0; index < quasis.length; index += 1) {
const value = field(quasis[index] as AstNode, 'value');
output = this.concatenateStrings(
output,
isPlainRecord(value) && typeof value.cooked === 'string' ? value.cooked : '',
node,
);
if (expressions[index]) {
output = this.concatenateStrings(
output,
this.coerceToIntermediateString(
await resolveCellValue(await this.evaluateExpression(expressions[index], env)),
'Template interpolation',
node,
),
node,
);
}
}
return output;
}
default:
throw unsupported(node, `Unsupported expression: ${node.type}`);
}
}
private async declarePattern(
pattern: AstNode,
value: unknown,
env: Environment,
constant: boolean,
): Promise<void> {
if (pattern.type === 'Identifier') {
env.declare(stringField(pattern, 'name'), value, constant);
return;
}
if (pattern.type === 'AssignmentPattern') {
const selected =
value === undefined ? await this.evaluateExpression(ast(pattern, 'right'), env) : value;
await this.declarePattern(ast(pattern, 'left'), selected, env, constant);
return;
}
if (pattern.type === 'ArrayPattern') {
if (!Array.isArray(value)) {
throw new InterpreterError(
'execution_error',
'Array destructuring expects an array',
pattern,
);
}
let index = 0;
for (const item of arrayField(pattern, 'elements')) {
if (item === null) {
index += 1;
continue;
}
const child = asAst(item, pattern);
if (child.type === 'RestElement') {
await this.declarePattern(ast(child, 'argument'), value.slice(index), env, constant);
index = value.length;
continue;
}
await this.declarePattern(child, value[index], env, constant);
index += 1;
}
return;
}
if (pattern.type === 'ObjectPattern') {
if (!isPlainRecord(value)) {
throw new InterpreterError(
'execution_error',
'Object destructuring expects plain data',
pattern,
);
}
const consumed = new Set<string>();
for (const property of nodes(pattern, 'properties')) {
if (property.type === 'RestElement') {
const rest: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) {
if (!consumed.has(key)) rest[key] = item;
}
await this.declarePattern(ast(property, 'argument'), rest, env, constant);
continue;
}
if (property.type !== 'Property' || field(property, 'kind') !== 'init') {
throw unsupported(property, 'Unsupported object destructuring property');
}
const key = propertyName(property, env, this);
assertSafeKey(key, property);
consumed.add(key);
await this.declarePattern(ast(property, 'value'), value[key], env, constant);
}
return;
}
throw unsupported(pattern, `Unsupported binding pattern: ${pattern.type}`);
}
private async evaluateLoopBody(
body: AstNode,
env: Environment,
): Promise<'continue' | 'break' | 'complete'> {
try {
await this.evaluateStatement(body, env);
return 'complete';
} catch (signal) {
if (signal instanceof ContinueSignal) return 'continue';
if (signal instanceof BreakSignal) return 'break';
throw signal;
}
}
private assignPattern(pattern: AstNode, value: unknown, env: Environment): unknown {
if (pattern.type === 'Identifier') {
return env.set(stringField(pattern, 'name'), value, pattern);
}
throw unsupported(pattern, 'Only identifier assignment targets are supported');
}
private async memberName(node: AstNode, env: Environment): Promise<string> {
const property = ast(node, 'property');
if (field(node, 'computed') === true) {
const value = await this.evaluateExpression(property, env);
if (typeof value !== 'string' && typeof value !== 'number') {
throw new InterpreterError(
'execution_error',
'Computed property must be a string or number',
node,
);
}
return String(value);
}
if (property.type !== 'Identifier') throw unsupported(property, 'Invalid member property');
return stringField(property, 'name');
}
private async evaluateCallArguments(node: AstNode, env: Environment): Promise<unknown[]> {
const values: unknown[] = [];
for (const argument of nodes(node, 'arguments')) {
if (argument.type === 'SpreadElement') {
throw unsupported(argument, 'Spread call arguments are not supported');
}
values.push(await resolveCellValue(await this.evaluateExpression(argument, env)));
}
return values;
}
private callDataMethod(
method: DataMethodReference,
args: readonly unknown[],
node: AstNode,
): unknown {
if (method.root === 'JSON') {
if (method.name === 'parse') {
if (args.length !== 1 || typeof args[0] !== 'string') {
throw new InterpreterError('execution_error', 'JSON.parse expects one string', node);
}
try {
return materializePlainData(JSON.parse(args[0]), {
label: 'JSON.parse result',
maxDepth: this.limits.maxDataDepth,
maxCollectionItems: this.limits.maxCollectionItems,
});
} catch (error) {
if (error instanceof InterpreterError) throw error;
throw new InterpreterError(
'execution_error',
`JSON.parse failed: ${error instanceof Error ? error.message : String(error)}`,
node,
);
}
}
if (args.length !== 1) {
throw new InterpreterError('execution_error', 'JSON.stringify expects one value', node);
}
const copied = materializePlainData(args[0], {
label: 'JSON.stringify input',
maxDepth: this.limits.maxDataDepth,
maxCollectionItems: this.limits.maxCollectionItems,
preserveUndefined: true,
});
assertByteLimit(copied, this.limits.maxIntermediateBytes, 'JSON.stringify');
return JSON.stringify(copied);
}
if (method.name === 'fromEntries') {
if (args.length !== 1 || !Array.isArray(args[0])) {
throw new InterpreterError('execution_error', 'Object.fromEntries expects one array', node);
}
this.assertCollectionSize(args[0].length, 'Object.fromEntries', node);
const output: Record<string, unknown> = {};
let uniqueKeyCount = 0;
let processedEntryCount = 0;
for (const entry of args[0]) {
if (!Array.isArray(entry) || entry.length < 2) {
throw new InterpreterError(
'execution_error',
'Object.fromEntries expects key/value pairs',
node,
);
}
const key = this.coerceToIntermediateString(entry[0], 'Object.fromEntries key', node);
assertSafeKey(key, node);
if (!Object.hasOwn(output, key)) uniqueKeyCount += 1;
this.assertCollectionSize(uniqueKeyCount, 'Object.fromEntries', node);
processedEntryCount += 1;
if ((processedEntryCount & 0xff) === 0) this.checkWallTime(node);
output[key] = entry[1];
}
return output;
}
if (args.length !== 1 || (!Array.isArray(args[0]) && !isPlainRecord(args[0]))) {
throw new InterpreterError(
'execution_error',
`Object.${method.name} expects plain data`,
node,
);
}
const itemCount = Array.isArray(args[0]) ? args[0].length : Object.keys(args[0]).length;
this.assertCollectionSize(itemCount, `Object.${method.name}`, node);
if (method.name === 'keys') return Object.keys(args[0]);
if (method.name === 'values') return Object.values(args[0]);
return Object.entries(args[0]);
}
private callStringMethod(
method: StringMethodReference,
args: readonly unknown[],
node: AstNode,
): unknown {
const noArgs = (): void => {
if (args.length !== 0) {
throw new InterpreterError(
'execution_error',
`String.${method.name} expects no arguments`,
node,
);
}
};
switch (method.name) {
case 'trim':
noArgs();
return method.target.trim();
case 'toLowerCase':
noArgs();
return method.target.toLowerCase();
case 'toUpperCase':
noArgs();
return method.target.toUpperCase();
case 'includes':
case 'startsWith':
case 'endsWith': {
if (args.length < 1 || args.length > 2) {
throw new InterpreterError(
'execution_error',
`String.${method.name} expects one or two arguments`,
node,
);
}
const search = this.coerceToIntermediateString(
args[0],
`String.${method.name} search`,
node,
);
const position = args[1] === undefined ? undefined : Number(args[1]);
return method.target[method.name](search, position);
}
case 'slice':
case 'substring': {
if (args.length > 2) {
throw new InterpreterError(
'execution_error',
`String.${method.name} expects at most two arguments`,
node,
);
}
if (args.length === 0) return method.target[method.name](0);
return method.target[method.name](
Number(args[0]),
args[1] === undefined ? undefined : Number(args[1]),
);
}
case 'split':
if (args.length > 2) {
throw new InterpreterError(
'execution_error',
'String.split expects at most two arguments',
node,
);
}
if (args.length === 0) {
this.assertCollectionSize(1, 'String.split', node);
return [method.target];
}
if (args[0] instanceof RegExp) {
throw unsupported(node, 'String.split does not support regular expression separators');
}
const requestedLimit = args[1] === undefined ? undefined : Number(args[1]);
if (args[0] === undefined) {
const itemCount = normalizeStringSplitLimit(requestedLimit) === 0 ? 0 : 1;
this.assertCollectionSize(itemCount, 'String.split', node);
return itemCount === 0 ? [] : [method.target];
}
const separator = this.coerceToIntermediateString(args[0], 'String.split separator', node);
this.assertStringSplitCollectionSize(method.target, separator, requestedLimit, node);
return method.target.split(separator, requestedLimit);
}
}
private evaluateBinary(operator: string, left: unknown, right: unknown, node: AstNode): unknown {
switch (operator) {
case '===':
return left === right;
case '!==':
return left !== right;
case '==':
return left == right;
case '!=':
return left != right;
case '<':
return (left as never) < (right as never);
case '<=':
return (left as never) <= (right as never);
case '>':
return (left as never) > (right as never);
case '>=':
return (left as never) >= (right as never);
case '+':
if (typeof left === 'string' || typeof right === 'string') {
return this.concatenateStrings(
this.coerceToIntermediateString(left, 'String concatenation operand', node),
this.coerceToIntermediateString(right, 'String concatenation operand', node),
node,
);
}
return (left as never) + (right as never);
case '-':
return Number(left) - Number(right);
case '*':
return Number(left) * Number(right);
case '/':
return Number(left) / Number(right);
case '%':
return Number(left) % Number(right);
default:
throw unsupported(node, `Unsupported binary operator: ${operator}`);
}
}
private concatenateStrings(left: string, right: string, node: AstNode): string {
const budget = createRawByteBudget(this.limits.maxIntermediateBytes);
countRawString(left, budget);
countRawString(right, budget);
if (budget.bytes > budget.limit) {
throw new InterpreterError('limit_exceeded', 'Intermediate string byte limit exceeded', node);
}
return left + right;
}
private assertIntermediateString(value: string, label: string, node: AstNode): string {
if (
rawStringByteLengthAtMost(value, this.limits.maxIntermediateBytes) >
this.limits.maxIntermediateBytes
) {
throw new InterpreterError('limit_exceeded', `${label} byte limit exceeded`, node);
}
return value;
}
private coerceToIntermediateString(value: unknown, label: string, node: AstNode): string {
if (
stringCoercionByteLengthAtMost(value, this.limits.maxIntermediateBytes) >
this.limits.maxIntermediateBytes
) {
throw new InterpreterError('limit_exceeded', `${label} byte limit exceeded`, node);
}
return this.assertIntermediateString(String(value), label, node);
}
private assertJoinedString(
values: readonly unknown[],
separator: string,
label: string,
node: AstNode,
): void {
if (
joinedStringByteLengthAtMost(values, separator, this.limits.maxIntermediateBytes) >
this.limits.maxIntermediateBytes
) {
throw new InterpreterError('limit_exceeded', `${label} byte limit exceeded`, node);
}
}
private sortUtf16Values(target: unknown[], node: AstNode): void {
const budget = createRawByteBudget(this.limits.maxIntermediateBytes);
const decorated: Array<{ value: unknown; text: string | undefined; index: number }> = [];
for (let index = 0; index < target.length; index += 1) {
if ((index & 0xff) === 0) this.checkWallTime(node);
const value = target[index];
if (value !== undefined && !countStringCoercion(value, budget, new Set(), false)) {
throw new InterpreterError('limit_exceeded', 'Array.sort byte limit exceeded', node);
}
decorated.push({ value, text: value === undefined ? undefined : String(value), index });
}
let comparisons = 0;
decorated.sort((left, right) => {
if ((comparisons++ & 0xff) === 0) this.checkWallTime(node);
if (left.text === undefined) return right.text === undefined ? left.index - right.index : 1;
if (right.text === undefined) return -1;
if (left.text < right.text) return -1;
if (left.text > right.text) return 1;
return left.index - right.index;
});
this.checkWallTime(node);
for (let index = 0; index < decorated.length; index += 1) {
target[index] = decorated[index].value;
}
}
private assertCollectionSize(size: number, label: string, node: AstNode): void {
if (size > this.limits.maxCollectionItems) {
throw new InterpreterError('limit_exceeded', `${label} item limit exceeded`, node);
}
}
private checkWallTime(node: AstNode): void {
this.throwIfTerminalFailure();
if (Date.now() - this.startedAt > this.limits.maxWallTimeMs) {
throw new InterpreterError('limit_exceeded', 'Cell wall-time limit exceeded', node);
}
}
private assertStringSplitCollectionSize(
target: string,
separator: string,
requestedLimit: number | undefined,
node: AstNode,
): void {
const limit = normalizeStringSplitLimit(requestedLimit);
if (limit === 0) return;
const maxItems = this.limits.maxCollectionItems;
if (separator.length === 0) {
this.assertCollectionSize(
limit === undefined ? target.length : Math.min(target.length, limit),
'String.split',
node,
);
return;
}
if (limit !== undefined && limit <= maxItems && limit <= 1) return;
let items = 1;
let offset = 0;
for (;;) {
const separatorIndex = target.indexOf(separator, offset);
if (separatorIndex < 0) return;
items += 1;
this.assertCollectionSize(items, 'String.split', node);
if (limit !== undefined && items >= limit) return;
offset = separatorIndex + separator.length;
}
}
private evaluateUnary(operator: string, value: unknown, node: AstNode): unknown {
switch (operator) {
case '!':
return !value;
case '+':
return Number(value);
case '-':
return -Number(value);
case 'typeof':
return value === null ? 'object' : typeof value;
case 'void':
return undefined;
default:
throw unsupported(node, `Unsupported unary operator: ${operator}`);
}
}
private callPromiseMethod(
name: PromiseMethodReference['name'],
value: unknown,
node: AstNode,
): CellPromise {
if (name === 'resolve') return this.ownPromise(resolveCellValue(value));
if (name === 'reject') return this.ownPromise(Promise.reject(value));
if (value instanceof CellPromise) {
return this.ownPromise(
value.observe().then((resolved) => this.promiseCollection(name, resolved, node)),
);
}
return this.ownPromise(this.promiseCollection(name, value, node));
}
private promiseCollection(
name: 'all' | 'allSettled',
value: unknown,
node: AstNode,
): Promise<unknown> {
if (!Array.isArray(value)) {
throw new InterpreterError('execution_error', `Promise.${name} expects an array`, node);
}
this.assertCollectionSize(value.length, `Promise.${name}`, node);
const promises = value.map(resolveCellValue);
if (name === 'all') {
return Promise.all(promises).then((items) => {
assertByteLimit(items, this.limits.maxIntermediateBytes, 'Promise.all');
return items;
});
}
return Promise.allSettled(promises).then((items) => {
const settled = items.map((item) =>
item.status === 'fulfilled'
? { status: 'fulfilled', value: item.value }
: { status: 'rejected', reason: caughtReason(item.reason) },
);
assertByteLimit(settled, this.limits.maxIntermediateBytes, 'Promise.allSettled');
return settled;
});
}
private callArrayMethod(
method: ArrayMethodReference,
callback: CellFunction,
node: AstNode,
): CellPromise {
this.assertCollectionSize(method.target.length, `Array.${method.name}`, node);
return this.ownPromise(
(async () => {
const values = new Array<unknown>(method.target.length);
const included = new Array<boolean>(method.target.length).fill(false);
let retainedValues = 0;
let serializedBytes = 2;
await Promise.all(
method.target.map(async (targetValue, index) => {
const callbackValue = await this.invokeFunction(callback, [
targetValue,
index,
method.target,
]);
if (method.name === 'filter' && !callbackValue) return;
const value = method.name === 'filter' ? targetValue : callbackValue;
const separatorBytes = retainedValues === 0 ? 0 : 1;
const remaining = this.limits.maxIntermediateBytes - serializedBytes - separatorBytes;
const valueBytes = serializedByteLength(value, Math.max(0, remaining));
if (remaining < 0 || valueBytes > remaining) {
throw new InterpreterError(
'limit_exceeded',
`Array.${method.name} byte limit exceeded`,
node,
);
}
serializedBytes += separatorBytes + valueBytes;
retainedValues += 1;
values[index] = value;
included[index] = true;
}),
);
return method.name === 'filter'
? values.filter((_value, index) => included[index])
: values;
})(),
);
}
private async invokeFunction(fn: CellFunction, args: readonly unknown[]): Promise<unknown> {
const env = new Environment(fn.closure);
for (const [index, parameter] of fn.parameters.entries()) {
await this.declarePattern(parameter, args[index], env, false);
}
if (fn.expressionBody) {
return resolveCellValue(await this.evaluateExpression(fn.body, env));
}
try {
await this.evaluateStatement(fn.body, env);
return null;
} catch (signal) {
if (!(signal instanceof ReturnSignal)) throw signal;
return resolveCellValue(signal.value);
}
}
private async callTool(name: string, value: unknown, node: AstNode): Promise<unknown> {
this.throwIfTerminalFailure();
if (this.toolCalls.length >= this.limits.maxToolCalls) {
throw new InterpreterError('limit_exceeded', 'Tool-call limit exceeded', node);
}
if (this.activeToolCalls >= this.limits.maxConcurrency) {
throw new InterpreterError('limit_exceeded', 'Tool concurrency limit exceeded', node);
}
const input = materializePlainData(value, {
label: `arguments for ${name}`,
maxDepth: this.limits.maxDataDepth,
maxCollectionItems: this.limits.maxCollectionItems,
});
assertByteLimit(input, this.limits.maxIntermediateBytes, 'tool arguments');
const call = { index: this.toolCalls.length + 1, name };
this.toolCalls.push(call);
this.activeToolCalls += 1;
try {
const operation = this.input.callTool(name, input, this.cellAbortController.signal);
this.hostToolOperations.push(operation);
const output = await awaitWithAbort(operation, this.cellAbortController.signal);
const copied = materializePlainData(output, {
label: `result from ${name}`,
maxDepth: this.limits.maxDataDepth,
maxCollectionItems: this.limits.maxCollectionItems,
maxBytes: this.limits.maxResultBytes,
byteLimitLabel: `result from ${name}`,
});
return copied;
} catch (error) {
if (this.input.signal?.aborted) throw abortReason(this.input.signal);
if (this.cellAbortController.signal.aborted) {
throw abortReason(this.cellAbortController.signal);
}
if (this.input.isFatalToolError?.(error)) {
this.terminalFailure = { reason: error };
throw error;
}
if (error instanceof InterpreterError) throw error;
const message = error instanceof Error ? error.message : String(error);
throw new InterpreterError('tool_failure', `Tool ${name} failed: ${message}`, node);
} finally {
this.activeToolCalls -= 1;
}
}
private async tick(node: AstNode): Promise<void> {
if (this.input.signal?.aborted) throw abortReason(this.input.signal);
if (this.cellAbortController.signal.aborted) {
throw abortReason(this.cellAbortController.signal);
}
this.steps += 1;
if (this.steps > this.limits.maxSteps) {
throw new InterpreterError('limit_exceeded', 'Interpreter step limit exceeded', node);
}
if (Date.now() - this.startedAt > this.limits.maxWallTimeMs) {
throw new InterpreterError('limit_exceeded', 'Cell wall-time limit exceeded', node);
}
if (this.steps % 256 === 0) await new Promise<void>((resolve) => setImmediate(resolve));
}
}
export async function executeCodeCellImpl(
input: ExecuteCodeCellInput,
): Promise<CodeModeExecutionResult> {
const limits = { ...DEFAULT_CODE_MODE_LIMITS, ...input.limits };
validateLimits(limits);
if (new TextEncoder().encode(input.code).byteLength > limits.maxSourceBytes) {
return {
ok: false,
error: { kind: 'limit_exceeded', message: 'Cell source-size limit exceeded' },
toolCalls: [],
};
}
try {
const program = parseCell(input.code);
validateSyntax(program);
const rootBindings = new ValidationScope();
for (const name of RESERVED_ROOT_BINDINGS) rootBindings.declare(name);
validateReferences(program, new Set(input.tools.map((tool) => tool.name)), rootBindings);
return await new Interpreter(input).execute(program);
} catch (error) {
if (input.signal?.aborted) throw abortReason(input.signal);
if (input.isFatalToolError?.(error)) throw error;
return { ok: false, error: normalizeError(error), toolCalls: [] };
}
}
function validateSyntax(node: AstNode): void {
if (!SUPPORTED_SYNTAX_NODES.has(node.type)) {
throw unsupported(node, `Unsupported syntax: ${node.type}`);
}
switch (node.type) {
case 'VariableDeclaration': {
const kind = field(node, 'kind');
if (kind !== 'const' && kind !== 'let') {
throw unsupported(node, `Unsupported declaration kind: ${String(kind)}`);
}
break;
}
case 'VariableDeclarator':
validateBindingPattern(ast(node, 'id'));
break;
case 'ArrowFunctionExpression':
for (const parameter of nodes(node, 'params')) validateBindingPattern(parameter);
break;
case 'CatchClause': {
const parameter = optionalAst(node, 'param');
if (parameter) validateBindingPattern(parameter);
break;
}
case 'ForOfStatement': {
if (field(node, 'await') === true) {
throw unsupported(node, 'for await is not supported');
}
const left = ast(node, 'left');
if (left.type === 'VariableDeclaration') {
if (nodes(left, 'declarations').length !== 1) {
throw unsupported(left, 'for...of requires one variable declaration');
}
} else if (left.type !== 'Identifier') {
throw unsupported(left, 'Only identifier assignment targets are supported');
}
break;
}
case 'BreakStatement':
case 'ContinueStatement':
if (optionalAst(node, 'label')) {
throw unsupported(node, 'Labeled loop control is not supported');
}
break;
case 'Property': {
if (field(node, 'kind') !== 'init') {
throw unsupported(node, 'Only ordinary object properties are supported');
}
if (field(node, 'computed') === true) {
throw unsupported(ast(node, 'key'), 'Computed object keys are not supported yet');
}
const key = ast(node, 'key');
const value = field(key, 'value');
if (
key.type !== 'Identifier' &&
!(key.type === 'Literal' && (typeof value === 'string' || typeof value === 'number'))
) {
throw unsupported(key, 'Object property key must be a string');
}
assertSafeKey(key.type === 'Identifier' ? stringField(key, 'name') : String(value), key);
break;
}
case 'AssignmentPattern':
case 'RestElement':
validateBindingPattern(ast(node, node.type === 'AssignmentPattern' ? 'left' : 'argument'));
break;
case 'MemberExpression': {
const property = ast(node, 'property');
if (field(node, 'computed') === true && staticMemberName(node) === undefined) {
throw unsupported(node, 'Computed member access requires a static string or number');
}
if (field(node, 'computed') !== true && property.type !== 'Identifier') {
throw unsupported(property, 'Invalid member property');
}
const staticProperty = staticMemberName(node);
if (staticProperty !== undefined) assertSafeKey(staticProperty, node);
break;
}
case 'CallExpression': {
if (nodes(node, 'arguments').some((argument) => argument.type === 'SpreadElement')) {
throw unsupported(node, 'Spread call arguments are not supported');
}
const callee = ast(node, 'callee');
if (callee.type !== 'MemberExpression') {
throw unsupported(node, 'Only calls to supported member methods are allowed');
}
const member = staticMemberName(callee);
if (member === undefined) {
throw unsupported(callee, 'Computed calls require a static member name');
}
const memberObject = ast(callee, 'object');
const memberObjectName =
memberObject.type === 'Identifier' ? stringField(memberObject, 'name') : undefined;
if (member === 'race' && memberObjectName === 'Promise') {
throw unsupported(
callee,
'Promise.race is not supported because nested tool calls cannot escape a cell',
);
}
assertSafeKey(member, callee);
if (
memberObjectName !== 'tools' &&
!(memberObjectName === 'Promise' && SUPPORTED_PROMISE_METHODS.has(member)) &&
!(
(memberObjectName === 'JSON' || memberObjectName === 'Object') &&
SUPPORTED_DATA_METHODS.has(member)
) &&
!SUPPORTED_COLLECTION_METHODS.has(member)
) {
throw unsupported(callee, `Unsupported member method: ${member}`);
}
break;
}
case 'BinaryExpression': {
const operator = stringField(node, 'operator');
if (!SUPPORTED_BINARY_OPERATORS.has(operator)) {
throw unsupported(node, `Unsupported binary operator: ${operator}`);
}
break;
}
case 'LogicalExpression': {
const operator = stringField(node, 'operator');
if (!SUPPORTED_LOGICAL_OPERATORS.has(operator)) {
throw unsupported(node, `Unsupported logical operator: ${operator}`);
}
break;
}
case 'UnaryExpression': {
const operator = stringField(node, 'operator');
if (!SUPPORTED_UNARY_OPERATORS.has(operator)) {
throw unsupported(node, `Unsupported unary operator: ${operator}`);
}
break;
}
case 'AssignmentExpression': {
const operator = stringField(node, 'operator');
if (!SUPPORTED_ASSIGNMENT_OPERATORS.has(operator)) {
throw unsupported(node, `Unsupported assignment operator: ${operator}`);
}
const left = ast(node, 'left');
if (left.type !== 'Identifier') {
throw unsupported(left, 'Only identifier assignment targets are supported');
}
break;
}
case 'UpdateExpression': {
const operator = stringField(node, 'operator');
if (!SUPPORTED_UPDATE_OPERATORS.has(operator)) {
throw unsupported(node, `Unsupported update operator: ${operator}`);
}
const argument = ast(node, 'argument');
if (argument.type !== 'Identifier') {
throw unsupported(argument, 'Update expressions require an identifier');
}
break;
}
}
for (const value of Object.values(node)) {
if (isAstNode(value)) validateSyntax(value);
else if (Array.isArray(value)) {
for (const item of value) {
if (isAstNode(item)) validateSyntax(item);
}
}
}
}
class ValidationScope {
private readonly bindings = new Set<string>();
constructor(private readonly parent?: ValidationScope) {}
declare(name: string): void {
this.bindings.add(name);
}
has(name: string): boolean {
return this.bindings.has(name) || this.parent?.has(name) === true;
}
}
function validateReferences(
node: AstNode,
toolNames: ReadonlySet<string>,
scope: ValidationScope,
parent?: AstNode,
parentField?: string,
): void {
if (node.type === 'BlockStatement') {
const blockScope = new ValidationScope(scope);
const statements = nodes(node, 'body');
for (const statement of statements) {
if (statement.type === 'VariableDeclaration') {
declareVariableBindings(statement, blockScope);
}
}
for (const statement of statements)
validateReferences(statement, toolNames, blockScope, node, 'body');
return;
}
if (node.type === 'VariableDeclaration') {
for (const declaration of nodes(node, 'declarations')) {
validatePatternReferences(ast(declaration, 'id'), toolNames, scope);
const init = optionalAst(declaration, 'init');
if (init) validateReferences(init, toolNames, scope, declaration, 'init');
}
return;
}
if (node.type === 'ArrowFunctionExpression') {
const functionScope = new ValidationScope(scope);
for (const parameter of nodes(node, 'params')) declarePatternBindings(parameter, functionScope);
for (const parameter of nodes(node, 'params')) {
validatePatternReferences(parameter, toolNames, functionScope);
}
validateReferences(ast(node, 'body'), toolNames, functionScope, node, 'body');
return;
}
if (node.type === 'CatchClause') {
const catchScope = new ValidationScope(scope);
const parameter = optionalAst(node, 'param');
if (parameter) {
declarePatternBindings(parameter, catchScope);
validatePatternReferences(parameter, toolNames, catchScope);
}
validateReferences(ast(node, 'body'), toolNames, catchScope, node, 'body');
return;
}
if (node.type === 'ForOfStatement') {
validateReferences(ast(node, 'right'), toolNames, scope, node, 'right');
const left = ast(node, 'left');
if (left.type === 'VariableDeclaration') {
const loopScope = new ValidationScope(scope);
declareVariableBindings(left, loopScope);
for (const declaration of nodes(left, 'declarations')) {
validatePatternReferences(ast(declaration, 'id'), toolNames, loopScope);
}
validateReferences(ast(node, 'body'), toolNames, loopScope, node, 'body');
} else {
validateReferences(left, toolNames, scope, node, 'left');
validateReferences(ast(node, 'body'), toolNames, scope, node, 'body');
}
return;
}
if (node.type === 'ForStatement') {
const loopScope = new ValidationScope(scope);
const init = optionalAst(node, 'init');
if (init?.type === 'VariableDeclaration') declareVariableBindings(init, loopScope);
if (init) validateReferences(init, toolNames, loopScope, node, 'init');
const test = optionalAst(node, 'test');
if (test) validateReferences(test, toolNames, loopScope, node, 'test');
const update = optionalAst(node, 'update');
if (update) validateReferences(update, toolNames, loopScope, node, 'update');
validateReferences(ast(node, 'body'), toolNames, loopScope, node, 'body');
return;
}
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
const target = ast(node, node.type === 'AssignmentExpression' ? 'left' : 'argument');
if (target.type === 'Identifier' && stringField(target, 'name') === 'undefined') {
throw unsupported(target, 'Cannot assign to reserved interpreter root "undefined"');
}
}
if (node.type === 'Identifier') {
const name = stringField(node, 'name');
if (
MEMBER_ROOT_BINDINGS.has(name) &&
!isDirectRootMemberReceiver(parent, parentField) &&
!isStaticPropertyName(parent, parentField)
) {
throw unsupported(
node,
`Interpreter root "${name}" may only be used as a direct member receiver`,
);
}
if (!isStaticPropertyName(parent, parentField) && !scope.has(name)) {
throw new InterpreterError('execution_error', `Unknown identifier "${name}"`, node);
}
return;
}
if (node.type === 'MemberExpression') {
const object = ast(node, 'object');
if (object.type === 'Identifier' && stringField(object, 'name') === 'tools') {
const name = staticMemberName(node);
if (name !== undefined && !toolNames.has(name)) {
throw new InterpreterError('unknown_tool', `Unknown or inactive tool "${name}"`, node);
}
}
validateReferences(object, toolNames, scope, node, 'object');
return;
}
if (node.type === 'Property') {
validateReferences(ast(node, 'value'), toolNames, scope, node, 'value');
return;
}
for (const [key, value] of Object.entries(node)) {
if (isAstNode(value)) validateReferences(value, toolNames, scope, node, key);
else if (Array.isArray(value)) {
for (const item of value) {
if (isAstNode(item)) validateReferences(item, toolNames, scope, node, key);
}
}
}
}
function declareVariableBindings(declaration: AstNode, scope: ValidationScope): void {
for (const declarator of nodes(declaration, 'declarations')) {
declarePatternBindings(ast(declarator, 'id'), scope);
}
}
function declarePatternBindings(pattern: AstNode, scope: ValidationScope): void {
if (pattern.type === 'Identifier') {
scope.declare(stringField(pattern, 'name'));
return;
}
if (pattern.type === 'AssignmentPattern') {
declarePatternBindings(ast(pattern, 'left'), scope);
return;
}
if (pattern.type === 'RestElement') {
declarePatternBindings(ast(pattern, 'argument'), scope);
return;
}
if (pattern.type === 'ArrayPattern') {
for (const item of arrayField(pattern, 'elements')) {
if (item !== null) declarePatternBindings(asAst(item, pattern), scope);
}
return;
}
if (pattern.type === 'ObjectPattern') {
for (const property of nodes(pattern, 'properties')) {
declarePatternBindings(
property.type === 'RestElement' ? ast(property, 'argument') : ast(property, 'value'),
scope,
);
}
}
}
function validatePatternReferences(
pattern: AstNode,
toolNames: ReadonlySet<string>,
scope: ValidationScope,
): void {
if (pattern.type === 'AssignmentPattern') {
validatePatternReferences(ast(pattern, 'left'), toolNames, scope);
validateReferences(ast(pattern, 'right'), toolNames, scope, pattern, 'right');
return;
}
if (pattern.type === 'RestElement') {
validatePatternReferences(ast(pattern, 'argument'), toolNames, scope);
return;
}
if (pattern.type === 'ArrayPattern') {
for (const item of arrayField(pattern, 'elements')) {
if (item !== null) validatePatternReferences(asAst(item, pattern), toolNames, scope);
}
return;
}
if (pattern.type === 'ObjectPattern') {
for (const property of nodes(pattern, 'properties')) {
validatePatternReferences(
property.type === 'RestElement' ? ast(property, 'argument') : ast(property, 'value'),
toolNames,
scope,
);
}
}
}
function isDirectRootMemberReceiver(
parent: AstNode | undefined,
parentField: string | undefined,
): boolean {
return parent?.type === 'MemberExpression' && parentField === 'object';
}
function isStaticPropertyName(
parent: AstNode | undefined,
parentField: string | undefined,
): boolean {
return (
(parentField === 'property' &&
parent?.type === 'MemberExpression' &&
field(parent, 'computed') !== true) ||
(parentField === 'key' && parent?.type === 'Property' && field(parent, 'computed') !== true)
);
}
function validateBindingPattern(pattern: AstNode): void {
if (pattern.type === 'Identifier') {
const name = stringField(pattern, 'name');
if (RESERVED_ROOT_BINDINGS.has(name)) {
throw unsupported(pattern, `Cannot shadow reserved interpreter root "${name}"`);
}
return;
}
if (pattern.type === 'AssignmentPattern') {
validateBindingPattern(ast(pattern, 'left'));
return;
}
if (pattern.type === 'RestElement') {
validateBindingPattern(ast(pattern, 'argument'));
return;
}
if (pattern.type === 'ArrayPattern') {
for (const item of arrayField(pattern, 'elements')) {
if (item !== null) validateBindingPattern(asAst(item, pattern));
}
return;
}
if (pattern.type === 'ObjectPattern') {
for (const property of nodes(pattern, 'properties')) {
if (property.type === 'RestElement') {
validateBindingPattern(ast(property, 'argument'));
} else if (property.type === 'Property' && field(property, 'kind') === 'init') {
validateBindingPattern(ast(property, 'value'));
} else {
throw unsupported(property, 'Unsupported object binding property');
}
}
return;
}
throw unsupported(pattern, `Unsupported binding pattern: ${pattern.type}`);
}
function staticMemberName(node: AstNode): string | undefined {
const property = ast(node, 'property');
if (field(node, 'computed') !== true && property.type === 'Identifier') {
return stringField(property, 'name');
}
const value = field(property, 'value');
return property.type === 'Literal' && (typeof value === 'string' || typeof value === 'number')
? String(value)
: undefined;
}
function stringIndex(property: string): number | undefined {
if (!/^(?:0|[1-9]\d*)$/u.test(property)) return undefined;
const index = Number(property);
return Number.isSafeInteger(index) ? index : undefined;
}
function isAstNode(value: unknown): value is AstNode {
return (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
typeof (value as AstNode).type === 'string'
);
}
function parseCell(code: string): AstNode {
const wrappedSource = `async function __maka_cell__() {\n${code}\n}`;
const sourceFile = createSourceFile('cell.ts', wrappedSource, ScriptTarget.ESNext, true);
const sourceWrapper = sourceFile.statements[0];
if (
sourceFile.statements.length !== 1 ||
!sourceWrapper ||
!isFunctionDeclaration(sourceWrapper) ||
sourceWrapper.name?.text !== '__maka_cell__' ||
!sourceWrapper.body
) {
throw new InterpreterError('parse_error', 'Cell source escaped its wrapper');
}
const transpiled = transpileModule(wrappedSource, {
reportDiagnostics: true,
compilerOptions: { target: ScriptTarget.ESNext, module: ModuleKind.ESNext },
});
const diagnostic = transpiled.diagnostics?.find(
(item) => item.category === DiagnosticCategory.Error,
);
if (diagnostic) {
throw new InterpreterError(
'parse_error',
`Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`,
);
}
let parsed: unknown;
try {
parsed = parse(transpiled.outputText, {
ecmaVersion: 'latest',
sourceType: 'script',
locations: true,
});
} catch (error) {
throw new InterpreterError(
'parse_error',
error instanceof Error ? error.message : 'Failed to parse JavaScript',
);
}
const root = asAst(parsed);
const body = nodes(root, 'body');
const wrapper = body[0];
if (
body.length !== 1 ||
wrapper?.type !== 'FunctionDeclaration' ||
optionalAst(wrapper, 'id')?.name !== '__maka_cell__'
) {
throw new InterpreterError('parse_error', 'Cell source escaped its wrapper');
}
return ast(wrapper, 'body');
}
function materializePlainData(value: unknown, options: MaterializationOptions): unknown {
const seen = new Set<object>();
let nodes = 0;
let serializedBytes = 0;
const ensureBytes = (bytes: number): void => {
if (options.maxBytes === undefined) return;
if (bytes > options.maxBytes - serializedBytes) {
throw new InterpreterError(
'limit_exceeded',
`${options.byteLimitLabel ?? options.label} byte limit exceeded`,
);
}
};
const addBytes = (bytes: number): void => {
ensureBytes(bytes);
if (options.maxBytes === undefined) return;
serializedBytes += bytes;
};
const addSerializedBytes = (item: unknown, requiredAfter = 0): void => {
if (options.maxBytes === undefined) return;
ensureBytes(requiredAfter);
const available = options.maxBytes - serializedBytes - requiredAfter;
const bytes = serializedByteLength(item, Math.max(0, available));
ensureBytes(bytes + requiredAfter);
addBytes(bytes);
};
const copy = (current: unknown, depth: number, requiredAfter: number): unknown => {
if (depth > options.maxDepth) {
throw new InterpreterError('invalid_data', `${options.label} exceeds the data-depth limit`);
}
if (current === null || typeof current === 'string' || typeof current === 'boolean') {
addSerializedBytes(current, requiredAfter);
return current;
}
if (typeof current === 'number') {
if (!Number.isFinite(current)) {
throw new InterpreterError('invalid_data', `${options.label} contains a non-finite number`);
}
addSerializedBytes(current, requiredAfter);
return current;
}
if (current === undefined) {
const copied = options.preserveUndefined ? undefined : null;
addSerializedBytes(copied, requiredAfter);
return copied;
}
if (typeof current !== 'object') {
throw new InterpreterError('invalid_data', `${options.label} contains a non-data value`);
}
ensureBytes(2 + requiredAfter);
nodes += 1;
if (nodes > MAX_COPY_NODES) {
throw new InterpreterError('limit_exceeded', `${options.label} traversal limit exceeded`);
}
if (seen.has(current)) {
throw new InterpreterError('invalid_data', `${options.label} contains a cycle`);
}
seen.add(current);
try {
if (Array.isArray(current)) {
const length = current.length;
if (length > options.maxCollectionItems) {
throw new InterpreterError('limit_exceeded', `${options.label} item limit exceeded`);
}
ensureBytes((length === 0 ? 2 : length * 2 + 1) + requiredAfter);
addBytes(1);
const output = new Array<unknown>(length);
for (let index = 0; index < length; index += 1) {
if (index > 0) addBytes(1);
const requiredAfterItem = (length - index - 1) * 2 + 1 + requiredAfter;
if (index in current) {
ensureBytes(1 + requiredAfterItem);
output[index] = copy(current[index], depth + 1, requiredAfterItem);
} else addSerializedBytes(null, requiredAfterItem);
}
addBytes(1);
return output;
}
if (!isPlainRecord(current)) {
throw new InterpreterError('invalid_data', `${options.label} contains a host object`);
}
const keys = Object.keys(current);
if (keys.length > options.maxCollectionItems) {
throw new InterpreterError('limit_exceeded', `${options.label} item limit exceeded`);
}
for (const key of keys) assertSafeKey(key);
const output: Record<string, unknown> = {};
addBytes(1);
let emitted = 0;
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(current, key);
if (!descriptor?.enumerable) continue;
if (emitted > 0) addBytes(1);
addSerializedBytes(key);
addBytes(1);
const requiredAfterItem = 1 + requiredAfter;
ensureBytes(1 + requiredAfterItem);
output[key] = copy(current[key], depth + 1, requiredAfterItem);
emitted += 1;
}
addBytes(1);
return output;
} finally {
seen.delete(current);
}
};
return copy(value, 0, 0);
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function assertByteLimit(value: unknown, limit: number, label: string): void {
const bytes = serializedByteLength(value, limit);
if (bytes > limit) throw new InterpreterError('limit_exceeded', `${label} byte limit exceeded`);
}
function assertSafeKey(key: string, node?: AstNode): void {
if (DANGEROUS_KEYS.has(key)) {
throw new InterpreterError('invalid_data', `Property "${key}" is not allowed`, node);
}
}
interface RawByteBudget {
bytes: number;
limit: number;
}
function createRawByteBudget(maxBytes: number): RawByteBudget {
const limit =
Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : Number.POSITIVE_INFINITY;
return { bytes: 0, limit };
}
function rawStringByteLengthAtMost(value: string, maxBytes: number): number {
const budget = createRawByteBudget(maxBytes);
countRawString(value, budget);
return budget.bytes;
}
function countRawString(value: string, budget: RawByteBudget): boolean {
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
let bytes: number;
if (code <= 0x7f) {
bytes = 1;
} else if (code <= 0x7ff) {
bytes = 2;
} else if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes = 4;
index += 1;
} else {
bytes = 3;
}
} else {
bytes = 3;
}
if (!addRawBytes(budget, bytes)) return false;
}
return true;
}
function stringCoercionByteLengthAtMost(value: unknown, maxBytes: number): number {
const budget = createRawByteBudget(maxBytes);
countStringCoercion(value, budget, new Set(), false);
return budget.bytes;
}
function joinedStringByteLengthAtMost(
values: readonly unknown[],
separator: string,
maxBytes: number,
): number {
const budget = createRawByteBudget(maxBytes);
countJoinedString(values, separator, budget, new Set());
return budget.bytes;
}
function countJoinedString(
values: readonly unknown[],
separator: string,
budget: RawByteBudget,
arrays: Set<readonly unknown[]>,
): boolean {
if (arrays.has(values)) return true;
arrays.add(values);
try {
for (let index = 0; index < values.length; index += 1) {
if (index > 0 && !countRawString(separator, budget)) return false;
if (!countStringCoercion(values[index], budget, arrays, true)) return false;
}
return true;
} finally {
arrays.delete(values);
}
}
function countStringCoercion(
value: unknown,
budget: RawByteBudget,
arrays: Set<readonly unknown[]>,
arrayElement: boolean,
): boolean {
if (arrayElement && (value === null || value === undefined)) return true;
if (Array.isArray(value)) return countJoinedString(value, ',', budget, arrays);
return countRawString(String(value), budget);
}
function addRawBytes(budget: RawByteBudget, bytes: number): boolean {
if (budget.bytes > Number.MAX_SAFE_INTEGER - bytes) {
budget.bytes = Number.POSITIVE_INFINITY;
return false;
}
if (budget.bytes + bytes > budget.limit) {
budget.bytes =
budget.limit < Number.MAX_SAFE_INTEGER ? budget.limit + 1 : Number.POSITIVE_INFINITY;
return false;
}
budget.bytes += bytes;
return true;
}
function normalizeStringSplitLimit(value: number | undefined): number | undefined {
if (value === undefined) return undefined;
if (!Number.isFinite(value)) return 0;
const modulo = 2 ** 32;
const integer = Math.trunc(value) % modulo;
return integer < 0 ? integer + modulo : integer;
}
function validateLimits(limits: CodeModeLimits): void {
for (const [name, value] of Object.entries(limits)) {
if (!Number.isSafeInteger(value) || value <= 0)
throw new RangeError(`${name} must be a positive safe integer`);
}
}
function normalizeError(
error: unknown,
): CodeModeExecutionResult extends infer _Result
? { kind: CodeModeDiagnosticKind; message: string; location?: { line: number; column: number } }
: never {
if (error instanceof InterpreterError) {
const location = error.node?.loc?.start;
return {
kind: error.kind,
message: error.message,
...(location
? { location: { line: Math.max(1, location.line - 1), column: location.column + 1 } }
: {}),
};
}
return {
kind: 'execution_error',
message: error instanceof Error ? error.message : String(error),
};
}
function unsupported(node: AstNode, message: string): InterpreterError {
return new InterpreterError('unsupported_syntax', message, node);
}
function propertyName(property: AstNode, env: Environment, interpreter: Interpreter): string {
const key = ast(property, 'key');
if (field(property, 'computed') === true) {
throw unsupported(key, 'Computed object keys are not supported yet');
}
if (key.type === 'Identifier') return stringField(key, 'name');
const value = field(key, 'value');
if (typeof value === 'string' || typeof value === 'number') return String(value);
void env;
void interpreter;
throw unsupported(key, 'Object property key must be a string');
}
function asAst(value: unknown, owner?: AstNode): AstNode {
if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value) ||
typeof (value as AstNode).type !== 'string'
) {
throw new InterpreterError('execution_error', 'Malformed interpreter AST', owner);
}
return value as AstNode;
}
function ast(node: AstNode, key: string): AstNode {
return asAst(node[key], node);
}
function optionalAst(node: AstNode, key: string): AstNode | undefined {
const value = node[key];
return value === undefined || value === null ? undefined : asAst(value, node);
}
function nodes(node: AstNode, key: string): AstNode[] {
const value = node[key];
if (!Array.isArray(value))
throw new InterpreterError('execution_error', `Malformed AST field "${key}"`, node);
return value.map((item) => asAst(item, node));
}
function arrayField(node: AstNode, key: string): unknown[] {
const value = node[key];
if (!Array.isArray(value))
throw new InterpreterError('execution_error', `Malformed AST field "${key}"`, node);
return value;
}
function field(node: AstNode, key: string): unknown {
return node[key];
}
function stringField(node: AstNode, key: string): string {
const value = node[key];
if (typeof value !== 'string')
throw new InterpreterError('execution_error', `Malformed AST field "${key}"`, node);
return value;
}
function abortReason(signal: AbortSignal): unknown {
return (
signal.reason ?? Object.assign(new Error('Code Mode cell aborted'), { name: 'AbortError' })
);
}
async function resolveCellValue(value: unknown): Promise<unknown> {
return value instanceof CellPromise ? await value.observe() : value;
}
async function awaitWithAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
if (signal.aborted) throw abortReason(signal);
let onAbort: (() => void) | undefined;
const aborted = new Promise<never>((_resolve, reject) => {
onAbort = () => reject(abortReason(signal));
signal.addEventListener('abort', onAbort, { once: true });
});
try {
return await Promise.race([operation, aborted]);
} finally {
if (onAbort) signal.removeEventListener('abort', onAbort);
}
}
function caughtReason(error: unknown): { name: string; message: string } {
return error instanceof Error
? { name: error.name, message: error.message }
: { name: 'Error', message: String(error) };
}