import { Cause, Effect, Exit } from "effect" import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" import { type AstNode, AsyncIteratorSymbol, asNode, type Binding, CodeModeFunction, CoercionFunction, ComputedValue, ErrorConstructorReference, GlobalMethodReference, GlobalNamespace, type GlobalNamespaceName, getArray, getBoolean, getNode, getOptionalNode, getString, IntrinsicReference, InterpreterRuntimeError, isRecord, IteratorSymbol, IteratorSymbols, JsonMethodReference, type MemberReference, OptionalShortCircuit, PromiseCapabilityFunction, PromiseInstanceMethodReference, PromiseMethodReference, type PromiseMethodName, PromiseNamespace, ProgramThrow, type ProgramNode, SearchFunction, SymbolNamespace, type StatementResult, supportedSyntaxMessage, unsupportedSyntax, UriFunction, } from "./model.js" import { caughtErrorValue, constructErrorValue } from "./errors.js" import { arrayStatics, type CallbackRunner, invokeArrayFrom, invokeGlobalMethod, invokeGroupBy, invokeIntrinsic, } from "./methods.js" import { constructPromise, invokePromiseInstanceMethod, invokePromiseMethod, PromiseRuntime, selfResolutionError, } from "./promises.js" import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js" import { ScopeStack } from "./scope.js" import { arrayMethods, mapMethods, mapStatics, setMethods, spreadItems } from "../stdlib/collections.js" import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js" import { dateMethods, dateStatics } from "../stdlib/date.js" import { invokeJsonMethod, jsonStatics, type JsonMethodName } from "../stdlib/json.js" import { mathConstants, mathMethods } from "../stdlib/math.js" import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js" import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js" import { promiseStatics } from "../stdlib/promise.js" import { escapeRegexHint, regexpMethods, regexpProperties, regexpStatics, regexFailureReason, } from "../stdlib/regexp.js" import { stringMethods, stringStatics } from "../stdlib/string.js" import { urlMethods, urlProperties, urlSearchParamsMethods, urlStatics, urlWritableProperties, invokeUriFunction, uriArgument, urlArgument, } from "../stdlib/url.js" import { boundedData, coerceToNumber, coerceToString, compoundOperators, errorBrandName, errorConstructors, invokeCoercion, valueConstructors, } from "../stdlib/value.js" import { isCodeModeValue, CodeModeDate, CodeModeMap, CodeModePromise, CodeModeRegExp, CodeModeSet, CodeModeURL, CodeModeURLSearchParams, } from "../values.js" const globalStaticMembers: Partial>> = { Object: objectStatics, Math: mathMethods, Array: arrayStatics, console: consoleMethods, Date: dateStatics, RegExp: regexpStatics, Map: mapStatics, URL: urlStatics, } const MAX_ARRAY_LENGTH = 4_294_967_295 const parseArrayIndex = (key: string | number): number | undefined => { const property = String(key) if (!/^(0|[1-9]\d*)$/.test(property)) return undefined const index = Number(property) return index < MAX_ARRAY_LENGTH ? index : undefined } const calleeDescription = (callee: AstNode): string => { if (callee.type === "Identifier") return getString(callee, "name") if (callee.type === "MemberExpression") { const object = getNode(callee, "object") const property = getNode(callee, "property") const key = callee.computed !== true && property.type === "Identifier" ? getString(property, "name") : property.type === "Literal" && typeof property.value === "string" ? property.value : undefined if (object.type === "Identifier" && key !== undefined) return `${getString(object, "name")}.${key}` } return "The called value" } const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { if (rhs instanceof ErrorConstructorReference) { const brand = errorBrandName(lhs) return brand !== undefined && (rhs.name === "Error" || brand === rhs.name) } if (rhs instanceof GlobalNamespace) { switch (rhs.name) { case "Date": return lhs instanceof CodeModeDate case "RegExp": return lhs instanceof CodeModeRegExp case "Map": return lhs instanceof CodeModeMap case "Set": return lhs instanceof CodeModeSet case "URL": return lhs instanceof CodeModeURL case "URLSearchParams": return lhs instanceof CodeModeURLSearchParams case "Array": return Array.isArray(lhs) case "Object": return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function") } } if (rhs instanceof PromiseNamespace) return lhs instanceof CodeModePromise if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { return false } throw new InterpreterRuntimeError( "The right-hand side of 'instanceof' must be a supported constructor: Error (or a specific error type like TypeError), Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, or Promise.", node, ) } const collectPatternNames = (pattern: AstNode, out: Array = []): Array => { switch (pattern.type) { case "Identifier": out.push(getString(pattern, "name")) break case "AssignmentPattern": collectPatternNames(getNode(pattern, "left"), out) break case "RestElement": collectPatternNames(getNode(pattern, "argument"), out) break case "ArrayPattern": for (const element of getArray(pattern, "elements")) { if (element !== null) collectPatternNames(asNode(element, "elements"), out) } break case "ObjectPattern": for (const property of getArray(pattern, "properties")) { const prop = asNode(property, "properties") collectPatternNames(prop.type === "RestElement" ? getNode(prop, "argument") : getNode(prop, "value"), out) } break } return out } const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => { if (left.type !== "VariableDeclaration") return undefined const declarations = getArray(left, "declarations") if (declarations.length !== 1) { throw new InterpreterRuntimeError(`${statement} supports one declared binding.`, left) } const kind = getString(left, "kind") return { pattern: getNode(asNode(declarations[0], "declarations[0]"), "id"), mutable: kind !== "const", lexical: kind !== "var", } } type CustomIterator = { iterator: SafeObject next: unknown asynchronous: boolean } export class Interpreter { private scopes: ScopeStack private readonly invokeTool: (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 readonly runner: CallbackRunner = { invokeFunction: (fn, args) => this.invokeFunction(fn, args), invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node), settlePromise: (promise) => this.settlePromise(promise), } constructor( invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect, invokeSearch: (args: Array) => Effect.Effect, toolKeys: (path: ReadonlyArray) => ReadonlyArray, promises: PromiseRuntime, logs: Array = [], ) { const globalScope = new Map() this.scopes = new ScopeStack([globalScope]) this.invokeTool = invokeTool this.invokeSearch = invokeSearch this.toolKeys = toolKeys this.logs = logs this.promises = promises globalScope.set("tools", { mutable: false, value: new ToolReference([]) }) globalScope.set("search", { mutable: false, value: new SearchFunction() }) globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() }) globalScope.set("Symbol", { mutable: false, value: new SymbolNamespace() }) globalScope.set("undefined", { mutable: false, value: undefined }) globalScope.set("Object", { mutable: false, value: new GlobalNamespace("Object") }) globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") }) globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") }) globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") }) globalScope.set("String", { mutable: false, value: new CoercionFunction("String") }) globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") }) globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") }) globalScope.set("console", { mutable: false, value: new GlobalNamespace("console") }) globalScope.set("parseInt", { mutable: false, value: new CoercionFunction("parseInt") }) globalScope.set("parseFloat", { mutable: false, value: new CoercionFunction("parseFloat") }) globalScope.set("isFinite", { mutable: false, value: new CoercionFunction("isFinite") }) globalScope.set("isNaN", { mutable: false, value: new CoercionFunction("isNaN") }) globalScope.set("Date", { mutable: false, value: new GlobalNamespace("Date") }) globalScope.set("RegExp", { mutable: false, value: new GlobalNamespace("RegExp") }) globalScope.set("Map", { mutable: false, value: new GlobalNamespace("Map") }) globalScope.set("Set", { mutable: false, value: new GlobalNamespace("Set") }) globalScope.set("URL", { mutable: false, value: new GlobalNamespace("URL") }) globalScope.set("URLSearchParams", { mutable: false, value: new GlobalNamespace("URLSearchParams") }) globalScope.set("encodeURI", { mutable: false, value: new UriFunction("encodeURI") }) globalScope.set("encodeURIComponent", { mutable: false, value: new UriFunction("encodeURIComponent") }) globalScope.set("decodeURI", { mutable: false, value: new UriFunction("decodeURI") }) globalScope.set("decodeURIComponent", { mutable: false, value: new UriFunction("decodeURIComponent") }) for (const name of errorConstructors) { globalScope.set(name, { mutable: false, value: new ErrorConstructorReference(name) }) } globalScope.set("NaN", { mutable: false, value: NaN }) globalScope.set("Infinity", { mutable: false, value: Infinity }) } run(program: ProgramNode): Effect.Effect { const self = this // Keep top-level declarations separate so they can shadow builtins. this.scopes.push() return Effect.gen(function* () { self.predeclareLexical(program.body) self.hoistFunctions(program.body) let value: unknown = undefined for (const [index, statement] of program.body.entries()) { if (index === program.body.length - 1 && statement.type === "ExpressionStatement") { value = yield* self.evaluateExpression(getNode(statement, "expression")) break } const result = yield* self.evaluateStatement(statement) if (result.kind === "return") { value = result.value break } if (result.kind === "break" || result.kind === "continue") { throw new InterpreterRuntimeError(`Unexpected '${result.kind}' outside of a loop.`, statement) } } // The implicit async body adopts returned promises before copy-out. if (value instanceof CodeModePromise) value = yield* self.settlePromise(value) return value }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } // Fork at the call site so admission and hooks occur when the call is made. private createToolCallPromise( path: ReadonlyArray, args: Array, ): Effect.Effect { return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) } private createPromise(effect: Effect.Effect): Effect.Effect { return this.promises.create(effect) } // Fiber exits make settlement idempotent; yielding prevents inline continuation. private settlePromise(promise: CodeModePromise): Effect.Effect { const promises = this.promises return Effect.suspend(() => { promises.markObserved(promise) return Effect.flatMap(promises.await(promise), (exit) => Effect.andThen(Effect.yieldNow, exit)) }) } private evaluateStatement(node: AstNode): Effect.Effect { switch (node.type) { case "ExpressionStatement": return Effect.as(this.evaluateExpression(getNode(node, "expression")), { kind: "none" }) case "VariableDeclaration": return Effect.map(this.evaluateVariableDeclaration(node), () => ({ kind: "none" })) case "ReturnStatement": { const argumentNode = getOptionalNode(node, "argument") return argumentNode ? Effect.map(this.evaluateExpression(argumentNode), (value) => ({ kind: "return", value })) : Effect.succeed({ kind: "return", value: undefined }) } case "BlockStatement": return this.evaluateBlock(node) case "IfStatement": return this.evaluateIfStatement(node) case "SwitchStatement": return this.evaluateSwitchStatement(node) case "LabeledStatement": return this.evaluateLabeledStatement(node) case "WhileStatement": return this.evaluateWhileStatement(node) case "DoWhileStatement": return this.evaluateDoWhileStatement(node) case "ForStatement": return this.evaluateForStatement(node) case "ForOfStatement": return this.evaluateForOfStatement(node) case "ForInStatement": return this.evaluateForInStatement(node) case "BreakStatement": return Effect.succeed(this.evaluateBreakStatement(node)) case "ContinueStatement": return Effect.succeed(this.evaluateContinueStatement(node)) case "ThrowStatement": return this.evaluateThrowStatement(node) case "TryStatement": return this.evaluateTryStatement(node) case "EmptyStatement": return Effect.succeed({ kind: "none" }) case "FunctionDeclaration": return Effect.succeed({ kind: "none" }) default: throw unsupportedSyntax(node.type, node) } } private evaluateBlock(node: AstNode): Effect.Effect { this.scopes.push() const self = this return Effect.gen(function* () { const body = getArray(node, "body") self.predeclareLexical(body) self.hoistFunctions(body) for (const statementValue of body) { const statement = asNode(statementValue, "body") const result = yield* self.evaluateStatement(statement) if (result.kind !== "none") { return result } } return { kind: "none" } satisfies StatementResult }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } private createFunction(node: AstNode): CodeModeFunction { if (node.generator === true) { throw new InterpreterRuntimeError("Generator functions are not supported.", node, "UnsupportedSyntax", [ supportedSyntaxMessage, ]) } return new CodeModeFunction( getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)), getNode(node, "body"), this.scopes.capture(), node.async === true, ) } private hoistFunctions(statements: Array): void { for (const statementValue of statements) { if (!isRecord(statementValue) || statementValue.type !== "FunctionDeclaration") continue const node = statementValue as AstNode this.scopes.declare(getString(getNode(node, "id"), "name"), this.createFunction(node), true, node) } } private predeclareLexical(statements: Array): void { for (const statementValue of statements) { if (!isRecord(statementValue) || statementValue.type !== "VariableDeclaration") continue const statement = statementValue as AstNode const kind = getString(statement, "kind") if (kind === "var") continue for (const declarationValue of getArray(statement, "declarations")) { const declaration = asNode(declarationValue, "declarations") for (const name of collectPatternNames(getNode(declaration, "id"))) { this.scopes.reserve(name, kind !== "const", declaration) } } } } private predeclarePattern(pattern: AstNode, mutable: boolean, node: AstNode): void { for (const name of collectPatternNames(pattern)) this.scopes.reserve(name, mutable, node) } private evaluateIfStatement(node: AstNode): Effect.Effect { const testNode = getNode(node, "test") const consequentNode = getNode(node, "consequent") const alternateNode = getOptionalNode(node, "alternate") return Effect.flatMap(this.evaluateExpression(testNode), (test) => test ? this.evaluateStatement(consequentNode) : alternateNode ? this.evaluateStatement(alternateNode) : Effect.succeed({ kind: "none" }), ) } private evaluateSwitchStatement(node: AstNode): Effect.Effect { const self = this return Effect.gen(function* () { const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant")) if (containsOpaqueReference(discriminant)) { throw new InterpreterRuntimeError("Switch discriminants must be data values.", node, "InvalidDataValue") } self.scopes.push() return yield* Effect.gen(function* () { const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) self.predeclareLexical(cases.flatMap((branch) => getArray(branch, "consequent"))) let defaultIndex: number | undefined let selected: number | undefined for (const [index, branch] of cases.entries()) { const test = getOptionalNode(branch, "test") if (!test) { defaultIndex = index continue } const candidate = yield* self.evaluateExpression(test) if (containsOpaqueReference(candidate)) { throw new InterpreterRuntimeError("Switch case values must be data values.", test, "InvalidDataValue") } if (candidate === discriminant) { selected = index break } } const start = selected ?? defaultIndex if (start === undefined) return { kind: "none" } satisfies StatementResult for (let index = start; index < cases.length; index += 1) { for (const statementValue of getArray(cases[index]!, "consequent")) { const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) if (result.kind === "break") { if (result.label === undefined) return { kind: "none" } satisfies StatementResult return result } if (result.kind === "return" || result.kind === "continue") return result } } return { kind: "none" } satisfies StatementResult }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) }) } private evaluateWhileStatement( node: AstNode, labels?: ReadonlySet, ): Effect.Effect { const testNode = getNode(node, "test") const bodyNode = getNode(node, "body") const self = this return Effect.gen(function* () { while (yield* self.evaluateExpression(testNode)) { const result = yield* self.evaluateStatement(bodyNode) if (result.kind === "continue") { if (result.label !== undefined && !labels?.has(result.label)) return result continue } if (result.kind === "break") { if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "return") { return result } } return { kind: "none" } satisfies StatementResult }) } private evaluateDoWhileStatement( node: AstNode, labels?: ReadonlySet, ): Effect.Effect { const bodyNode = getNode(node, "body") const testNode = getNode(node, "test") const self = this return Effect.gen(function* () { do { const result = yield* self.evaluateStatement(bodyNode) if (result.kind === "continue") { if (result.label !== undefined && !labels?.has(result.label)) return result continue } if (result.kind === "break") { if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "return") { return result } } while (yield* self.evaluateExpression(testNode)) return { kind: "none" } satisfies StatementResult }) } private evaluateForStatement( node: AstNode, labels?: ReadonlySet, ): Effect.Effect { this.scopes.push() const self = this return Effect.gen(function* () { const initNode = getOptionalNode(node, "init") const testNode = getOptionalNode(node, "test") const updateNode = getOptionalNode(node, "update") const bodyNode = getNode(node, "body") if (initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var") { self.predeclareLexical([initNode]) } if (initNode) { if (initNode.type === "VariableDeclaration") { yield* self.evaluateVariableDeclaration(initNode) } else { yield* self.evaluateExpression(initNode) } } const perIterationBindings = initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var" ? Array.from(self.scopes.current().keys()) : [] const nextIteration = () => { if (perIterationBindings.length === 0) return const current = self.scopes.current() self.scopes.pop() self.scopes.push( new Map(perIterationBindings.map((name): [string, Binding] => [name, { ...current.get(name)! }])), ) } nextIteration() while (testNode ? yield* self.evaluateExpression(testNode) : true) { const result = yield* self.evaluateStatement(bodyNode) if (result.kind === "return") { return result } if (result.kind === "break") { if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result nextIteration() if (updateNode) { yield* self.evaluateExpression(updateNode) } if (result.kind === "continue") { continue } } return { kind: "none" } satisfies StatementResult }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } private evaluateForOfStatement( node: AstNode, labels?: ReadonlySet, ): Effect.Effect { const awaiting = getBoolean(node, "await") const left = getNode(node, "left") const declared = loopDeclaration(left, "for...of") if (declared?.lexical) this.scopes.push() const self = this return Effect.gen(function* () { if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") const iterable = spreadItems(right) const iterator = iterable === undefined && awaiting ? yield* self.customIterator(right, node) : undefined if (iterable === undefined && iterator === undefined) { throw new InterpreterRuntimeError( `${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams${awaiting ? ", or custom iterator" : ""} value.`, node, ) } let assignment: AstNode | undefined if ( left.type !== "VariableDeclaration" && (left.type === "Identifier" || left.type === "MemberExpression" || left.type === "ArrayPattern" || left.type === "ObjectPattern") ) { assignment = left } else if (left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...of binding.", left) } const evaluateBody = (value: unknown) => Effect.gen(function* () { if (declared) { self.scopes.push() if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical) } else if (assignment) { yield* self.assignPattern(assignment, value, left) } return yield* self.evaluateStatement(body) }).pipe( Effect.ensuring( Effect.sync(() => { if (declared) self.scopes.pop() }), ), ) if (iterable !== undefined) { for (const value of iterable) { const result = yield* evaluateBody(awaiting ? yield* self.awaitValue(value) : value) if (result.kind === "return") return result if (result.kind === "break") { if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result } return { kind: "none" } satisfies StatementResult } if (iterator === undefined) throw new InterpreterRuntimeError("Custom iterator is unavailable.", node) while (true) { const step = yield* self.nextIteratorResult(iterator, node) if (step.done) return { kind: "none" } satisfies StatementResult const bodyExit = yield* Effect.exit(evaluateBody(step.value)) if (!Exit.isSuccess(bodyExit)) { // Process interruption must remain prompt; user cleanup cannot extend a timeout. if (!Cause.hasInterruptsOnly(bodyExit.cause)) yield* Effect.exit(self.closeIterator(iterator, node)) return yield* Effect.failCause(bodyExit.cause) } const result = bodyExit.value if (result.kind === "return") { yield* self.closeIterator(iterator, node) return result } if (result.kind === "break") { yield* self.closeIterator(iterator, node) if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) { yield* self.closeIterator(iterator, node) return result } } }).pipe( Effect.ensuring( Effect.sync(() => { if (declared?.lexical) self.scopes.pop() }), ), ) } private awaitValue(value: unknown): Effect.Effect { return value instanceof CodeModePromise ? this.settlePromise(value) : Effect.as(Effect.yieldNow, value) } private customIterator(value: unknown, node: AstNode) { if (!isRecord(value) || isRuntimeReference(value)) return Effect.succeed(undefined) const asyncMethod = Reflect.get(value, AsyncIteratorSymbol) const method = asyncMethod ?? Reflect.get(value, IteratorSymbol) if (method === undefined || method === null) return Effect.succeed(undefined) const self = this return Effect.map( this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), [], node), (iterator) => { const object = self.requireIteratorObject(iterator, "Iterator method result", node) return { iterator: object, next: self.requireIteratorMethod(object.next, "Iterator next", node), asynchronous: asyncMethod !== undefined && asyncMethod !== null, } }, ) } private nextIteratorResult(iterator: CustomIterator, node: AstNode) { const self = this return Effect.gen(function* () { if (iterator.asynchronous) { const object = self.requireIteratorObject( yield* self.awaitValue(yield* self.invokeCallable(iterator.next, [], node)), "Iterator next() result", node, ) return { done: Boolean(object.done), value: object.value } } const called = yield* Effect.exit(self.invokeCallable(iterator.next, [], node)) if (!Exit.isSuccess(called)) { yield* Effect.yieldNow return yield* Effect.failCause(called.cause) } const captured = yield* Effect.exit( Effect.sync(() => { const object = self.requireIteratorObject(called.value, "Iterator next() result", node) return { done: Boolean(object.done), value: object.value } }), ) if (!Exit.isSuccess(captured)) { yield* Effect.yieldNow return yield* Effect.failCause(captured.cause) } return { done: captured.value.done, value: yield* self.awaitValue(captured.value.value) } }) } private closeIterator(iterator: CustomIterator, node: AstNode): Effect.Effect { const close = iterator.iterator.return if (close === undefined || close === null) return iterator.asynchronous ? Effect.void : Effect.yieldNow const self = this return Effect.gen(function* () { const method = self.requireIteratorMethod(close, "Iterator return", node) if (iterator.asynchronous) { self.requireIteratorObject( yield* self.awaitValue(yield* self.invokeCallable(method, [], node)), "Iterator return() result", node, ) return } const called = yield* Effect.exit(self.invokeCallable(method, [], node)) if (!Exit.isSuccess(called)) { yield* Effect.yieldNow return yield* Effect.failCause(called.cause) } const captured = yield* Effect.exit( Effect.sync(() => self.requireIteratorObject(called.value, "Iterator return() result", node).value), ) if (!Exit.isSuccess(captured)) { yield* Effect.yieldNow return yield* Effect.failCause(captured.cause) } yield* self.awaitValue(captured.value) }) } private requireIteratorObject(value: unknown, context: string, node: AstNode): SafeObject { if (isRecord(value) && !isRuntimeReference(value)) return value throw new InterpreterRuntimeError(`${context} must be an object.`, node).as("TypeError") } private requireIteratorMethod(value: unknown, context: string, node: AstNode): unknown { if (typeofValue(value) === "function") return value throw new InterpreterRuntimeError(`${context} must be a function.`, node).as("TypeError") } private enumerableKeys(value: unknown): Array | undefined { if (value instanceof ToolReference) { return [...this.toolKeys(value.path)] } if (Array.isArray(value)) { return Object.keys(value) } if (value !== null && typeof value === "object" && !isRuntimeReference(value)) { return Object.keys(value) } return undefined } private evaluateForInStatement( node: AstNode, labels?: ReadonlySet, ): Effect.Effect { const left = getNode(node, "left") const declared = loopDeclaration(left, "for...in") if (declared?.lexical) this.scopes.push() const self = this return Effect.gen(function* () { if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") const keys = self.enumerableKeys(right) if (keys === undefined) { throw new InterpreterRuntimeError( "for...in requires a plain object, array, or tools reference. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.", node, ) } let assignmentName: string | undefined if (left.type === "Identifier") { assignmentName = getString(left, "name") } else if (left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...in binding.", left) } for (const key of keys) { const result = yield* Effect.gen(function* () { if (declared) { self.scopes.push() if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical) } else if (assignmentName) { self.scopes.set(assignmentName, key, left) } return yield* self.evaluateStatement(body) }).pipe( Effect.ensuring( Effect.sync(() => { if (declared) self.scopes.pop() }), ), ) if (result.kind === "return") { return result } if (result.kind === "break") { if (result.label !== undefined && !labels?.has(result.label)) return result return { kind: "none" } satisfies StatementResult } if (result.kind === "continue") { if (result.label !== undefined && !labels?.has(result.label)) return result continue } } return { kind: "none" } satisfies StatementResult }).pipe( Effect.ensuring( Effect.sync(() => { if (declared?.lexical) self.scopes.pop() }), ), ) } private evaluateBreakStatement(node: AstNode): StatementResult { const labelNode = getOptionalNode(node, "label") return labelNode ? { kind: "break", label: getString(labelNode, "name") } : { kind: "break" } } private evaluateContinueStatement(node: AstNode): StatementResult { const labelNode = getOptionalNode(node, "label") return labelNode ? { kind: "continue", label: getString(labelNode, "name") } : { kind: "continue" } } private evaluateLabeledStatement(node: AstNode): Effect.Effect { const labels = new Set() let body = node while (body.type === "LabeledStatement") { labels.add(getString(getNode(body, "label"), "name")) body = getNode(body, "body") } const evaluated = (() => { if (body.type === "WhileStatement") return this.evaluateWhileStatement(body, labels) if (body.type === "DoWhileStatement") return this.evaluateDoWhileStatement(body, labels) if (body.type === "ForStatement") return this.evaluateForStatement(body, labels) if (body.type === "ForOfStatement") return this.evaluateForOfStatement(body, labels) if (body.type === "ForInStatement") return this.evaluateForInStatement(body, labels) return this.evaluateStatement(body) })() return Effect.map(evaluated, (result) => result.kind === "break" && result.label !== undefined && labels.has(result.label) ? ({ kind: "none" } satisfies StatementResult) : result, ) } private evaluateThrowStatement(node: AstNode): Effect.Effect { const argument = getNode(node, "argument") return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value))) } private evaluateTryStatement(node: AstNode): Effect.Effect { const body = getNode(node, "block") const handler = getOptionalNode(node, "handler") const finalizer = getOptionalNode(node, "finalizer") const self = this const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), { onFailure: (cause) => { if (cause.reasons.some(Cause.isInterruptReason) || !handler) { return Effect.failCause(cause) } const caught = caughtErrorValue(Cause.squash(cause)) const parameter = getOptionalNode(handler, "param") self.scopes.push() return Effect.gen(function* () { if (parameter) yield* self.declarePattern(parameter, caught, true, handler) return yield* self.evaluateStatement(getNode(handler, "body")) }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) }, onSuccess: Effect.succeed, }) if (!finalizer) return attempted const isAbrupt = (result: StatementResult): boolean => result.kind === "return" || result.kind === "break" || result.kind === "continue" return Effect.matchCauseEffect(attempted, { onFailure: (cause) => cause.reasons.some(Cause.isInterruptReason) ? Effect.failCause(cause) : Effect.flatMap(this.evaluateStatement(finalizer), (final) => isAbrupt(final) ? Effect.succeed(final) : Effect.failCause(cause), ), onSuccess: (result) => Effect.flatMap(this.evaluateStatement(finalizer), (final) => isAbrupt(final) ? Effect.succeed(final) : Effect.succeed(result), ), }) } private evaluateVariableDeclaration(node: AstNode): Effect.Effect { const kind = getString(node, "kind") const declarations = getArray(node, "declarations") const self = this return Effect.gen(function* () { for (const declarationValue of declarations) { const declaration = asNode(declarationValue, "declarations") if (declaration.type !== "VariableDeclarator") { throw new InterpreterRuntimeError("Unsupported variable declaration shape.", declaration) } const init = getOptionalNode(declaration, "init") const value = init ? yield* self.evaluateExpression(init) : undefined yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration, kind !== "var") } }) } private declarePattern( pattern: AstNode, value: unknown, mutable: boolean, node: AstNode, initialize = false, ): Effect.Effect { const self = this return Effect.gen(function* () { if (pattern.type === "Identifier") { const name = getString(pattern, "name") if (initialize) self.scopes.initialize(name, value, node) else self.scopes.declare(name, value, mutable, node) return } if (pattern.type === "AssignmentPattern") { const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node, initialize) return } if (pattern.type === "ObjectPattern") { if (value === null || typeof value !== "object" || isRuntimeReference(value)) { throw new InterpreterRuntimeError( "Object destructuring requires a data object or array value.", pattern, "InvalidDataValue", ) } const consumed = new Set() for (const propertyValue of getArray(pattern, "properties")) { const property = asNode(propertyValue, "properties") if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(value as SafeObject)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } for (const symbol of IteratorSymbols) { if (!consumed.has(symbol) && Object.hasOwn(value, symbol)) Reflect.set(rest, symbol, Reflect.get(value, symbol)) } yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize) continue } const key = yield* self.destructuringPropertyKey(property) if (isBlockedMember(String(key))) { throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, property) } consumed.add(typeof key === "symbol" ? key : String(key)) yield* self.declarePattern( getNode(property, "value"), self.destructuringPropertyValue(value as SafeObject | Array, key), mutable, property, initialize, ) } return } if (pattern.type === "ArrayPattern") { const items = spreadItems(value) if (items === undefined) { throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) } for (const [index, item] of getArray(pattern, "elements").entries()) { if (item === null) continue const element = asNode(item, `elements[${index}]`) if (element.type === "RestElement") { yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize) break } yield* self.declarePattern(element, items[index], mutable, pattern, initialize) } return } throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern) }) } private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect { const self = this return Effect.gen(function* () { if (pattern.type === "Identifier") { self.scopes.set(getString(pattern, "name"), value, pattern) return } if (pattern.type === "MemberExpression") { yield* self.writeMember(pattern, value) return } if (pattern.type === "AssignmentPattern") { const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value yield* self.assignPattern(getNode(pattern, "left"), resolved, node) return } if (pattern.type === "ObjectPattern") { if (value === null || typeof value !== "object" || isRuntimeReference(value)) { throw new InterpreterRuntimeError( "Object destructuring requires a data object or array value.", pattern, "InvalidDataValue", ) } const source = value as SafeObject | Array const consumed = new Set() for (const propertyValue of getArray(pattern, "properties")) { const property = asNode(propertyValue, "properties") if (property.type === "RestElement") { const rest: SafeObject = Object.create(null) as SafeObject for (const [key, item] of Object.entries(source)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } for (const symbol of IteratorSymbols) { if (!consumed.has(symbol) && Object.hasOwn(source, symbol)) Reflect.set(rest, symbol, Reflect.get(source, symbol)) } yield* self.assignPattern(getNode(property, "argument"), rest, property) continue } const key = yield* self.destructuringPropertyKey(property) if (isBlockedMember(String(key))) { throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, property) } consumed.add(typeof key === "symbol" ? key : String(key)) yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property) } return } if (pattern.type === "ArrayPattern") { const items = spreadItems(value) if (items === undefined) { throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern) } for (const [index, item] of getArray(pattern, "elements").entries()) { if (item === null) continue const element = asNode(item, `elements[${index}]`) if (element.type === "RestElement") { yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element) break } yield* self.assignPattern(element, items[index], pattern) } return } throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) }) } private destructuringPropertyKey(property: AstNode): Effect.Effect { if (property.type !== "Property" || getString(property, "kind") !== "init") { throw new InterpreterRuntimeError("Unsupported object destructuring property.", property) } const keyNode = getNode(property, "key") if (getBoolean(property, "computed")) { return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode)) } return Effect.succeed(keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)) } private destructuringPropertyValue(source: SafeObject | Array, key: PropertyKey): unknown { if (!Array.isArray(source)) return Reflect.get(source, key) if (key === "length") return source.length if (typeof key === "number") return source[key] if (Object.hasOwn(source, key)) return Reflect.get(source, key) if (typeof key === "string" && arrayMethods.has(key)) return new IntrinsicReference(source, key) return undefined } private evaluateExpression(node: AstNode): Effect.Effect { switch (node.type) { case "Literal": { const regex = node.regex if (isRecord(regex) && typeof regex.pattern === "string") { return Effect.sync(() => this.constructRegExp([regex.pattern, typeof regex.flags === "string" ? regex.flags : ""], node), ) } return Effect.sync(() => boundedData(node.value, "Literal")) } case "Identifier": return Effect.sync(() => this.scopes.get(getString(node, "name"), node)) case "BinaryExpression": return this.evaluateBinaryExpression(node) case "LogicalExpression": return this.evaluateLogicalExpression(node) case "UnaryExpression": return this.evaluateUnaryExpression(node) case "AssignmentExpression": return this.evaluateAssignmentExpression(node) case "SequenceExpression": { const self = this return Effect.gen(function* () { let result: unknown for (const expression of getArray(node, "expressions")) { result = yield* self.evaluateExpression(asNode(expression, "expressions")) } return result }) } case "CallExpression": return this.evaluateCallExpression(node) case "ArrowFunctionExpression": case "FunctionExpression": return Effect.sync(() => this.createFunction(node)) case "MemberExpression": return this.readMember(node) case "ChainExpression": return Effect.map(this.evaluateExpression(getNode(node, "expression")), (value) => value === OptionalShortCircuit ? undefined : value, ) case "ObjectExpression": return this.evaluateObjectExpression(node) case "ArrayExpression": return this.evaluateArrayExpression(node) case "TemplateLiteral": return this.evaluateTemplateLiteral(node) case "ConditionalExpression": return this.evaluateConditionalExpression(node) case "UpdateExpression": return this.evaluateUpdateExpression(node) case "AwaitExpression": { // Await always suspends, including for plain values. const self = this return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), ) } case "NewExpression": return this.evaluateNewExpression(node) default: throw unsupportedSyntax(node.type, node) } } private evaluateNewExpression(node: AstNode): Effect.Effect { const callee = getNode(node, "callee") if (callee.type !== "Identifier") { throw unsupportedSyntax("NewExpression", node) } const name = getString(callee, "name") const argNodes = getArray(node, "arguments") const self = this if (name === "Promise") { return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => constructPromise(self.runner, self.promises, args[0], node), ) } if (errorConstructors.has(name)) { return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node)) } // Array and Object construct identically with or without new, like JS. if (name === "Array") { return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructArray(args, node)) } if (name === "Object") { return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructObject(args, node)) } if (valueConstructors.has(name)) { return Effect.gen(function* () { const args = yield* self.evaluateCallArguments(argNodes) switch (name) { case "Date": return yield* self.constructDate(args, node) case "RegExp": return self.constructRegExp(args, node) case "Map": return self.constructMap(args[0], node) case "Set": return self.constructSet(args[0], node) case "URL": return self.constructURL(args, node) default: return self.constructURLSearchParams(args[0], node) } }) } throw unsupportedSyntax("NewExpression", node) } private constructArray(args: Array, node: AstNode): Array { if (args.length !== 1) return [...args] const first = args[0] if (typeof first !== "number") return [first] if (!Number.isInteger(first) || first < 0 || first > 4294967295) { throw new InterpreterRuntimeError("Invalid array length.", node).as("RangeError") } // Sparse like JS: Array(3) has holes, and combinator loops already skip them. return new Array(first) } private constructObject(args: Array, node: AstNode): unknown { const first = args[0] if (first === null || first === undefined) return {} if (typeof first === "object") return first throw new InterpreterRuntimeError( `Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`, node, ) } private constructDate(args: Array, node: AstNode): Effect.Effect { if (args.length === 0) return Effect.succeed(new CodeModeDate(Date.now())) if (args.length === 1) { const arg = args[0] if (arg instanceof CodeModeDate) return Effect.succeed(new CodeModeDate(arg.time)) return Effect.map(this.toDatePrimitive(arg, node), (value) => typeof value === "string" ? new CodeModeDate(Date.parse(value)) : new CodeModeDate(new Date(coerceToNumber(value)).getTime()), ) } const parts = args.map((arg) => coerceToNumber(arg)) return Effect.succeed(new CodeModeDate(new Date(...(parts as [number, number])).getTime())) } private toDatePrimitive(value: unknown, node: AstNode): Effect.Effect { if (value === null || (typeof value !== "object" && typeof value !== "function")) return Effect.succeed(value) const object = value as Record const self = this return Effect.gen(function* () { if (Object.hasOwn(object, "valueOf") && typeofValue(object.valueOf) === "function") { const result = yield* self.runner.invokeCallable(object.valueOf, [], node) if (result === null || (typeof result !== "object" && typeof result !== "function")) return result } if (!Object.hasOwn(object, "toString")) return coerceToString(value) if (typeofValue(object.toString) === "function") { const result = yield* self.runner.invokeCallable(object.toString, [], node) if (result === null || (typeof result !== "object" && typeof result !== "function")) return result } throw new InterpreterRuntimeError("Cannot convert object to primitive value.", node).as("TypeError") }) } private constructRegExp(args: Array, node: AstNode): CodeModeRegExp { const first = args[0] const pattern = first instanceof CodeModeRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) const flagsArg = args[1] if (flagsArg !== undefined && typeof flagsArg !== "string") { throw new InterpreterRuntimeError( `RegExp flags must be a string of flag characters (e.g. "g", "gi"), not ${flagsArg === null ? "null" : typeof flagsArg}.`, node, ).as("SyntaxError") } const flags = flagsArg ?? (first instanceof CodeModeRegExp ? first.regex.flags : "") try { return new CodeModeRegExp(pattern, flags) } catch (error) { const reason = regexFailureReason(error) throw new InterpreterRuntimeError( /flag/i.test(reason) ? `new RegExp(...) received invalid flags ${JSON.stringify(flags)} (${reason}). Valid flags are d, g, i, m, s, u, v, and y.` : `new RegExp(...) received ${JSON.stringify(pattern)}, which is not a valid regular expression pattern (${reason}). ${escapeRegexHint}`, node, ).as("SyntaxError") } } private constructMap(init: unknown, node: AstNode): CodeModeMap { const target = new CodeModeMap() if (init === undefined || init === null) return target const entries = Array.isArray(init) ? init : init instanceof CodeModeMap ? Array.from(init.map.entries(), ([key, item]): Array => [key, item]) : undefined if (entries === undefined) { throw new InterpreterRuntimeError( "new Map(...) expects an array of [key, value] pairs, a Map, or no argument.", node, ) } for (const pair of entries) { if (!Array.isArray(pair)) { throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node) } target.map.set(pair[0], pair[1]) } return target } private constructSet(init: unknown, node: AstNode): CodeModeSet { const target = new CodeModeSet() if (init === undefined || init === null) return target const items = Array.isArray(init) ? init : init instanceof CodeModeSet ? Array.from(init.set.values()) : typeof init === "string" ? Array.from(init) : undefined if (items === undefined) { throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node) } for (const item of items) target.set.add(item) return target } private constructURL(args: Array, node: AstNode): CodeModeURL { if (args.length === 0) { throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( "TypeError", ) } const input = urlArgument(args[0], "new URL input") const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") try { return new CodeModeURL(new URL(input, base)) } catch { throw new InterpreterRuntimeError( `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, node, ).as("TypeError") } } private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams { if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams()) if (init instanceof CodeModeURLSearchParams) { return new CodeModeURLSearchParams(new URLSearchParams(init.params)) } if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init)) if (init === null || typeof init === "number" || typeof init === "boolean") { return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init))) } if (init instanceof CodeModeMap) { return this.constructURLSearchParams( Array.from(init.map.entries(), ([key, value]) => [key, value]), node, ) } if (Array.isArray(init)) { const entries = init.map((pair) => { if (!Array.isArray(pair) || pair.length !== 2) { throw new InterpreterRuntimeError( "new URLSearchParams(...) expects an array of [name, value] pairs.", node, ).as("TypeError") } return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [ string, string, ] }) return new CodeModeURLSearchParams(new URLSearchParams(entries)) } if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams()) const data = boundedData(init, "new URLSearchParams input") if (data === null || typeof data !== "object") { throw new InterpreterRuntimeError( "new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.", node, ).as("TypeError") } return new CodeModeURLSearchParams( new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), ) } private evaluateBinaryExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") const self = this return Effect.gen(function* () { const lhs = yield* self.evaluateExpression(getNode(node, "left")) const rhs = yield* self.evaluateExpression(getNode(node, "right")) if (operator === "instanceof") return instanceofValue(lhs, rhs, node) return boundedData(self.applyBinaryOperator(operator, lhs, rhs, node), "Binary expression result") }) } private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown { if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) { throw new InterpreterRuntimeError("Binary operators require data values.", node, "InvalidDataValue") } // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. // Dates use their default string hint for addition and loose equality, and epoch time elsewhere. const coerceOperand = (operand: unknown): unknown => { if (operand instanceof CodeModeDate) { return operator === "+" || operator === "==" || operator === "!=" ? coerceToString(operand) : operand.time } return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand } const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" const l = coerceOperand(lhs) const r = coerceOperand(rhs) switch (operator) { case "+": return (l as string) + (r as string) case "-": return (l as number) - (r as number) case "*": return (l as number) * (r as number) case "/": return (l as number) / (r as number) case "%": return (l as number) % (r as number) case "**": return (l as number) ** (r as number) case "==": return bothObjects ? lhs === rhs : l == r case "===": return lhs === rhs case "!=": return bothObjects ? lhs !== rhs : l != r case "!==": return lhs !== rhs case "<": return (l as string) < (r as string) case "<=": return (l as string) <= (r as string) case ">": return (l as string) > (r as string) case ">=": return (l as string) >= (r as string) case "&": return (l as number) & (r as number) case "|": return (l as number) | (r as number) case "^": return (l as number) ^ (r as number) case "<<": return (l as number) << (r as number) case ">>": return (l as number) >> (r as number) case ">>>": return (l as number) >>> (r as number) case "in": if (rhs === null || typeof rhs !== "object") { throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node) } // Never expose properties inherited from host prototypes. return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey) default: throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node) } } private evaluateLogicalExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") return Effect.flatMap(this.evaluateExpression(getNode(node, "left")), (left) => { if (operator === "&&") return left ? this.evaluateExpression(getNode(node, "right")) : Effect.succeed(left) if (operator === "||") return left ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right")) if (operator === "??") return left !== null && left !== undefined ? Effect.succeed(left) : this.evaluateExpression(getNode(node, "right")) throw new InterpreterRuntimeError(`Unsupported logical operator '${operator}'.`, node) }) } private evaluateUnaryExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") const argument = getNode(node, "argument") if (operator === "delete") return this.evaluateDeleteExpression(argument) // Undeclared names short-circuit, but declared TDZ bindings must still throw. if (operator === "typeof" && argument.type === "Identifier" && !this.scopes.resolve(getString(argument, "name"))) { return Effect.succeed("undefined") } return Effect.map(this.evaluateExpression(argument), (value) => { if (operator === "typeof") return typeofValue(value) if (operator === "!") return !value if (operator === "void") return undefined if (containsOpaqueReference(value)) { throw new InterpreterRuntimeError("Unary operators require data values.", node, "InvalidDataValue") } const operand = value instanceof CodeModeDate ? value.time : value !== null && typeof value === "object" ? coerceToString(value) : value let result: unknown switch (operator) { case "+": result = +(operand as number) break case "-": result = -(operand as number) break case "~": result = ~(operand as number) break default: throw new InterpreterRuntimeError(`Unsupported unary operator '${operator}'.`, node) } return boundedData(result, "Unary expression result") }) } private evaluateAssignmentExpression(node: AstNode): Effect.Effect { const left = getNode(node, "left") const operator = getString(node, "operator") const self = this return Effect.gen(function* () { if (operator === "??=" || operator === "||=" || operator === "&&=") { return yield* self.evaluateLogicalAssignment(node, left, operator) } if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) { const rightValue = yield* self.evaluateExpression(getNode(node, "right")) yield* self.assignPattern(left, rightValue, node) return rightValue } if (left.type === "Identifier") { const name = getString(left, "name") if (operator !== "=") { const current = self.scopes.get(name, left) const rightValue = yield* self.evaluateExpression(getNode(node, "right")) const next = boundedData( self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result", ) return self.scopes.set(name, next, left) } const rightValue = yield* self.evaluateExpression(getNode(node, "right")) return self.scopes.set(name, rightValue, left) } if (left.type === "MemberExpression") { return yield* self.modifyMember(left, (current) => Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => { if (operator === "=") return { write: true, next: rightValue, result: rightValue } const next = boundedData( self.applyCompoundAssignment(operator, current, rightValue, node), "Assignment result", ) return { write: true, next, result: next } }), ) } throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) }) } private evaluateLogicalAssignment( node: AstNode, left: AstNode, operator: string, ): Effect.Effect { const self = this const shouldAssign = (current: unknown): boolean => operator === "??=" ? current === null || current === undefined : operator === "||=" ? !current : Boolean(current) if (left.type === "Identifier") { const name = getString(left, "name") return Effect.gen(function* () { const current = self.scopes.get(name, left) if (!shouldAssign(current)) return current const rightValue = yield* self.evaluateExpression(getNode(node, "right")) return self.scopes.set(name, rightValue, left) }) } if (left.type === "MemberExpression") { return self.modifyMember(left, (current) => shouldAssign(current) ? Effect.map(self.evaluateExpression(getNode(node, "right")), (rightValue) => ({ write: true, next: rightValue, result: rightValue, })) : Effect.succeed({ write: false, next: current, result: current }), ) } throw new InterpreterRuntimeError("Assignment target must be an Identifier or MemberExpression.", left) } private evaluateUpdateExpression(node: AstNode): Effect.Effect { const operator = getString(node, "operator") const argument = getNode(node, "argument") const prefix = getBoolean(node, "prefix") const increment = operator === "++" ? 1 : operator === "--" ? -1 : undefined if (increment === undefined) { throw new InterpreterRuntimeError(`Unsupported update operator '${operator}'.`, node) } // CodeMode numeric coercion, not host Number(): null-prototype data objects would make // the host throw during ToPrimitive, and opaque runtime references must reject clearly. const operand = (current: unknown): number => { if (containsOpaqueReference(current)) { throw new InterpreterRuntimeError(`'${operator}' requires a data value.`, argument, "InvalidDataValue") } return coerceToNumber(current) } if (argument.type === "Identifier") { return Effect.sync(() => { const name = getString(argument, "name") const current = operand(this.scopes.get(name, argument)) const next = current + increment this.scopes.set(name, next, argument) return prefix ? next : current }) } if (argument.type === "MemberExpression") { return this.modifyMember(argument, (current) => { const value = operand(current) const next = value + increment return Effect.succeed({ write: true, next, result: prefix ? next : value }) }) } throw new InterpreterRuntimeError("Update target must be an Identifier or MemberExpression.", argument) } private evaluateCallExpression(node: AstNode): Effect.Effect { const callee = getNode(node, "callee") const argNodes = getArray(node, "arguments") const self = this return Effect.gen(function* () { const callable = yield* self.evaluateExpression(callee) if (callable === OptionalShortCircuit) return OptionalShortCircuit if ((callable === null || callable === undefined) && node.optional === true) return OptionalShortCircuit const args = yield* self.evaluateCallArguments(argNodes) return yield* self.invokeCallable(callable, args, node, callee) }) } // The single dispatch for every invocation: call expressions and callbacks share it. private invokeCallable( callable: unknown, args: Array, node: AstNode, callee: AstNode = node, ): Effect.Effect { const self = this return Effect.gen(function* () { if (callable instanceof ToolReference) { if (callable.path.length === 0) throw new InterpreterRuntimeError("The tools root is not callable.", callee) return yield* self.createToolCallPromise(callable.path, args) } if (callable instanceof PromiseMethodReference) { return yield* invokePromiseMethod(self.runner, self.promises, callable, args, node) } if (callable instanceof PromiseInstanceMethodReference) { return yield* invokePromiseInstanceMethod(self.runner, self.promises, callable, args, node) } if (callable instanceof CodeModeFunction) { return yield* self.invokeFunction(callable, args) } if (callable instanceof IntrinsicReference) { return yield* invokeIntrinsic(self.runner, callable, args, node) } if (callable instanceof GlobalMethodReference) { if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node) if (callable.namespace === "Object" && args[0] instanceof ToolReference) { return self.invokeObjectMethodOnTools(callable.name, args[0], node) } if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) { return invokeGlobalMethod(callable, args, node) } if (callable.namespace === "Array" && callable.name === "from") { return yield* invokeArrayFrom(self.runner, args, node) } if ((callable.namespace === "Object" || callable.namespace === "Map") && callable.name === "groupBy") { return yield* invokeGroupBy(self.runner, callable.namespace, args, node) } if (callable.namespace === "Array" && callable.name === "of") { return invokeGlobalMethod(callable, args, node) } return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`) } if (callable instanceof JsonMethodReference) { return yield* invokeJsonMethod(self.runner, callable.name, args, node) } if (callable instanceof CoercionFunction) { return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`) } if (callable instanceof UriFunction) { return invokeUriFunction(callable, args, node) } if (callable instanceof SearchFunction) { return yield* self.invokeSearch(args) } if (callable instanceof ErrorConstructorReference) { return constructErrorValue(callable.name, args, node) } if (callable instanceof GlobalNamespace) { // Real JS permits calling Array, Object, Date, and RegExp without new. if (callable.name === "Array") return self.constructArray(args, node) if (callable.name === "Object") return self.constructObject(args, node) // ISO instead of the host's locale string: CodeMode date strings are // deterministic and must not leak the host timezone. if (callable.name === "Date") return new Date().toISOString() if (callable.name === "RegExp") return self.constructRegExp(args, node) if (typeofValue(callable) === "function") { throw new InterpreterRuntimeError(`Constructor ${callable.name} requires 'new'.`, node).as("TypeError") } throw new InterpreterRuntimeError(`${callable.name} is not a function.`, node).as("TypeError") } if (callable instanceof PromiseNamespace) { throw new InterpreterRuntimeError("Constructor Promise requires 'new'.", node).as("TypeError") } if (callable instanceof SymbolNamespace) { throw new InterpreterRuntimeError( "Symbol is not callable; only Symbol.asyncIterator and Symbol.iterator are available.", node, ).as("TypeError") } if (callable instanceof PromiseCapabilityFunction) { callable.settle(args[0]) return undefined } if (callable === undefined || callable === null) { throw new InterpreterRuntimeError(`${calleeDescription(callee)} is not a function.`, callee).as("TypeError") } throw new InterpreterRuntimeError("Only tools are callable here.", callee) }) } private invokeObjectMethodOnTools(name: string, ref: ToolReference, node: AstNode): unknown { if (name === "keys") { return boundedData(this.enumerableKeys(ref)!, "Object.keys result") } throw new InterpreterRuntimeError( `Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`, node, "InvalidDataValue", ) } private invokeConsole(name: string, args: Array, node: AstNode): undefined { if (!consoleMethods.has(name)) throw new InterpreterRuntimeError(`console.${name} is not available.`, node) this.logs.push(formatConsoleMessage(name, args)) return undefined } private evaluateCallArguments(argNodes: Array): Effect.Effect, unknown, R> { const self = this return Effect.gen(function* () { const args: Array = [] for (const [index, arg] of argNodes.entries()) { const argNode = asNode(arg, `arguments[${index}]`) if (argNode.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(argNode, "argument")) const items = spreadItems(spread) if (items === undefined) throw new InterpreterRuntimeError("Spread arguments require an array, string, Map, or Set.", argNode) args.push(...items) } else { args.push(yield* self.evaluateExpression(argNode)) } } return args }) } private invokeFunction(fn: CodeModeFunction, args: Array): Effect.Effect { const invocation = new Interpreter(this.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs) invocation.scopes = 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() for (const parameter of fn.parameters) { for (const name of collectPatternNames(parameter)) { paramScope.set(name, { mutable: true, value: undefined, initialized: false }) } } for (const [index, parameter] of fn.parameters.entries()) { if (parameter.type === "RestElement") { yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true) break } yield* invocation.declarePattern(parameter, args[index], true, parameter, true) } if (fn.body.type === "BlockStatement") { const result = yield* invocation.evaluateStatement(fn.body) return result.kind === "return" ? result.value : undefined } return yield* invocation.evaluateExpression(fn.body) }) if (!fn.async) return run // The initial yield assigns `box.own` before the body can self-resolve. const box: { own?: CodeModePromise } = {} return Effect.map( this.createPromise( Effect.flatMap(run, (value) => { if (!(value instanceof CodeModePromise)) return Effect.succeed(value) if (value === box.own) return Effect.fail(selfResolutionError()) return invocation.settlePromise(value) }), ), (promise) => { box.own = promise return promise }, ) } private evaluateObjectExpression(node: AstNode): Effect.Effect, unknown, R> { const objectValue: Record = Object.create(null) as Record const properties = getArray(node, "properties") const self = this return Effect.gen(function* () { for (const propertyValue of properties) { const property = asNode(propertyValue, "properties") if (property.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(property, "argument")) if (spread === null || spread === undefined || isCodeModeValue(spread)) continue if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { throw new InterpreterRuntimeError("Object spread requires a data object.", property, "InvalidDataValue") } for (const [key, value] of Object.entries(spread)) { if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property) objectValue[key] = value } for (const symbol of IteratorSymbols) { if (Object.hasOwn(spread, symbol)) Reflect.set(objectValue, symbol, Reflect.get(spread, symbol)) } continue } if (property.type !== "Property") { throw new InterpreterRuntimeError("Only standard object properties are supported.", property) } if (getString(property, "kind") !== "init") { throw new InterpreterRuntimeError("Only init object properties are supported.", property) } const keyNode = getNode(property, "key") const valueNode = getNode(property, "value") const computed = getBoolean(property, "computed") let key: PropertyKey if (computed) { key = self.toPropertyKey(yield* self.evaluateExpression(keyNode), keyNode) } else if (keyNode.type === "Identifier") { key = getString(keyNode, "name") } else if (keyNode.type === "Literal") { key = self.toPropertyKey(keyNode.value, keyNode) } else { throw new InterpreterRuntimeError("Unsupported object property key shape.", keyNode) } if (isBlockedMember(String(key))) { throw new InterpreterRuntimeError(`Property '${String(key)}' is not available.`, keyNode) } Reflect.set(objectValue, key, yield* self.evaluateExpression(valueNode)) } return objectValue }) } private evaluateArrayExpression(node: AstNode): Effect.Effect, unknown, R> { const elements = getArray(node, "elements") const values: Array = [] const self = this return Effect.gen(function* () { for (const elementValue of elements) { if (elementValue === null) { // A literal elision is a real hole, like JS: extend length without an own index. values.length += 1 continue } const element = asNode(elementValue, "elements") if (element.type === "SpreadElement") { const spread = yield* self.evaluateExpression(getNode(element, "argument")) const items = spreadItems(spread) if (items === undefined) throw new InterpreterRuntimeError("Array spread requires an array, string, Map, or Set.", element) values.push(...items) } else { values.push(yield* self.evaluateExpression(element)) } } return values }) } private evaluateTemplateLiteral(node: AstNode): Effect.Effect { const quasis = getArray(node, "quasis") const expressions = getArray(node, "expressions") let output = "" const self = this return Effect.gen(function* () { for (let index = 0; index < quasis.length; index += 1) { const quasi = asNode(quasis[index], "quasis") const rawValue = quasi.value if (!isRecord(rawValue) || typeof rawValue.cooked !== "string") { throw new InterpreterRuntimeError("Invalid template literal quasi.", quasi) } output += rawValue.cooked if (index < expressions.length) { const raw = yield* self.evaluateExpression(asNode(expressions[index], "expressions")) output += coerceToString(boundedData(raw, "Template interpolation")) } } return output }) } private evaluateConditionalExpression(node: AstNode): Effect.Effect { return Effect.flatMap(this.evaluateExpression(getNode(node, "test")), (test) => this.evaluateExpression(getNode(node, test ? "consequent" : "alternate")), ) } private applyCompoundAssignment(operator: string, current: unknown, incoming: unknown, node: AstNode): unknown { if (!compoundOperators.has(operator)) { throw new InterpreterRuntimeError(`Unsupported assignment operator '${operator}'.`, node) } return this.applyBinaryOperator(operator.slice(0, -1), current, incoming, node) } private getMemberReference( node: AstNode, operation: "read" | "delete" = "read", ): Effect.Effect< | MemberReference | ToolReference | PromiseMethodReference | PromiseInstanceMethodReference | IntrinsicReference | GlobalMethodReference | JsonMethodReference | ComputedValue | typeof OptionalShortCircuit | undefined, unknown, R > { const objectNode = getNode(node, "object") const propertyNode = getNode(node, "property") const computed = getBoolean(node, "computed") const optional = node.optional === true const self = this return Effect.gen(function* () { const objectValue = yield* self.evaluateExpression(objectNode) if (objectValue === OptionalShortCircuit) return OptionalShortCircuit if ((objectValue === null || objectValue === undefined) && optional) return OptionalShortCircuit const key = computed ? self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) : propertyNode.type === "Identifier" ? getString(propertyNode, "name") : self.toPropertyKey(yield* self.evaluateExpression(propertyNode), propertyNode) if (objectValue instanceof ToolReference) { if (typeof key !== "string") { throw new InterpreterRuntimeError("Tool paths must use string property names.", propertyNode) } return new ToolReference([...objectValue.path, key]) } if (objectValue instanceof PromiseNamespace) { if (typeof key === "string" && promiseStatics.has(key as PromiseMethodName)) { return new PromiseMethodReference(key as PromiseMethodName) } throw new InterpreterRuntimeError( `Promise.${String(key)} is not available. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`, propertyNode, ) } if (objectValue instanceof SymbolNamespace) { if (key === "asyncIterator") return new ComputedValue(AsyncIteratorSymbol) if (key === "iterator") return new ComputedValue(IteratorSymbol) return new ComputedValue(undefined) } if (objectValue instanceof GlobalNamespace) { if (typeof key === "string" && isBlockedMember(key)) { throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available.`, propertyNode) } if (typeof key !== "string") return new ComputedValue(undefined) if (objectValue.name === "Math" && mathConstants.has(key)) { return new ComputedValue((Math as unknown as Record)[key]) } if (objectValue.name === "JSON") { if (jsonStatics.has(key)) return new JsonMethodReference(key as JsonMethodName) return new ComputedValue(undefined) } if (globalStaticMembers[objectValue.name]?.has(key)) { return new GlobalMethodReference(objectValue.name, key) } // Unknown static members read as undefined so feature detection works like native JS. return new ComputedValue(undefined) } if (typeof objectValue === "string") { if (key === "length") return new ComputedValue(objectValue.length) const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) if (index !== undefined) return new ComputedValue(objectValue[index]) if (typeof key === "string" && stringMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (typeof objectValue === "number") { if (typeof key === "string" && numberMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (objectValue instanceof CoercionFunction) { if (typeof key === "string" && isBlockedMember(key)) { throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available.`, propertyNode) } if (typeof key !== "string") return new ComputedValue(undefined) if (objectValue.name === "Number" && numberConstants.has(key)) { return new ComputedValue((Number as unknown as Record)[key]) } if (objectValue.name === "Number" && numberStatics.has(key)) { return new GlobalMethodReference("Number", key) } if (objectValue.name === "String" && stringStatics.has(key)) { return new GlobalMethodReference("String", key) } return new ComputedValue(undefined) } if (objectValue instanceof CodeModeDate) { if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (objectValue instanceof CodeModeRegExp) { if (key === "lastIndex") return { target: objectValue, key } if (typeof key === "string" && regexpProperties.has(key)) { return new ComputedValue((objectValue.regex as unknown as Record)[key]) } if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (objectValue instanceof CodeModeMap) { if (key === "size") return new ComputedValue(objectValue.map.size) if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (objectValue instanceof CodeModeSet) { if (key === "size") return new ComputedValue(objectValue.set.size) if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } if (objectValue instanceof CodeModeURL) { if (key === "searchParams") { return new ComputedValue(objectValue.searchParams) } if (typeof key === "string" && urlMethods.has(key)) return new IntrinsicReference(objectValue, key) if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } return new ComputedValue(undefined) } if (objectValue instanceof CodeModeURLSearchParams) { if (key === "size") return new ComputedValue(objectValue.params.size) if (typeof key === "string" && urlSearchParamsMethods.has(key)) { return new IntrinsicReference(objectValue, key) } return new ComputedValue(undefined) } // Reject unknown promise properties so a missing await cannot hide. if (objectValue instanceof CodeModePromise) { if (key === "then" || key === "catch" || key === "finally") { return new PromiseInstanceMethodReference(objectValue, key) } throw new InterpreterRuntimeError( "This value is an un-awaited Promise; await it first - e.g. `const result = await tools.ns.tool(...)`.", objectNode, "InvalidDataValue", ) } if (isRuntimeReference(objectValue)) { throw new InterpreterRuntimeError( "Runtime references are opaque and do not expose properties.", objectNode, "InvalidDataValue", ) } if (typeof objectValue !== "object" || objectValue === null) { throw new InterpreterRuntimeError("Cannot access a property on a non-object value.", objectNode) } if (typeof key === "string" && isBlockedMember(key)) { throw new InterpreterRuntimeError(`Property '${key}' is not available.`, propertyNode) } if (Array.isArray(objectValue)) { if (operation === "delete") return { target: objectValue, key } const index = typeof key === "symbol" ? undefined : parseArrayIndex(key) if (key !== "length" && !(typeof key === "string" && arrayMethods.has(key)) && index === undefined) { if (typeof key === "string" && Object.hasOwn(objectValue, key)) { return new ComputedValue((objectValue as Record & Array)[key]) } return new ComputedValue(undefined) } return { target: objectValue, key: index ?? key } } return { target: objectValue as SafeObject, key } }) } private readMember(node: AstNode): Effect.Effect { return Effect.map(this.getMemberReference(node), (reference) => { if (reference === OptionalShortCircuit) return OptionalShortCircuit if (reference instanceof ComputedValue) return reference.value if ( reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference || reference instanceof JsonMethodReference ) return reference if (Array.isArray(reference.target)) { if (reference.key === "length") return reference.target.length if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key) return Reflect.get(reference.target, reference.key) } if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex if (reference.target instanceof CodeModeURL) { return Reflect.get(reference.target.url, reference.key) } return Reflect.get(reference.target, reference.key) }) } private writeMember(node: AstNode, value: unknown): Effect.Effect { return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value })) } private evaluateDeleteExpression(argument: AstNode): Effect.Effect { const target = argument.type === "ChainExpression" ? getNode(argument, "expression") : argument if (target.type !== "MemberExpression") { throw new InterpreterRuntimeError("Only data fields may be deleted.", argument) } return Effect.map(this.getMemberReference(target, "delete"), (reference) => { if (reference === OptionalShortCircuit) return true if ( reference instanceof ComputedValue || reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference || reference instanceof JsonMethodReference || reference.target instanceof CodeModeURL ) { throw new InterpreterRuntimeError("Only data fields may be deleted.", target, "InvalidDataValue") } if (reference.target instanceof CodeModeRegExp) { return Reflect.deleteProperty(reference.target.regex, reference.key) } return Reflect.deleteProperty(reference.target, reference.key) }) } // Resolve side-effecting object and key expressions exactly once. private modifyMember( node: AstNode, compute: (current: unknown) => Effect.Effect<{ write: boolean; next: unknown; result: unknown }, unknown, R>, ): Effect.Effect { const self = this return Effect.gen(function* () { const reference = yield* self.getMemberReference(node) if ( reference === OptionalShortCircuit || reference instanceof ComputedValue || reference === undefined || reference instanceof ToolReference || reference instanceof PromiseMethodReference || reference instanceof PromiseInstanceMethodReference || reference instanceof IntrinsicReference || reference instanceof GlobalMethodReference || reference instanceof JsonMethodReference ) { throw new InterpreterRuntimeError("Only data fields may be assigned.", node) } if (Array.isArray(reference.target)) { if (reference.key === "length") throw new InterpreterRuntimeError("Array length cannot be assigned.", node) if (typeof reference.key === "string" && arrayMethods.has(reference.key)) { throw new InterpreterRuntimeError("Array methods cannot be assigned.", node) } } const key = reference.key const { write, next, result } = yield* compute(self.readReferenceValue(reference, key)) if (write) self.assignToReference(reference, key, next, node) return result }) } private readReferenceValue(reference: MemberReference, key: PropertyKey): unknown { if (reference.target instanceof CodeModeURL) { return Reflect.get(reference.target.url, key) } if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex return Reflect.get(reference.target, key) } private assignToReference(reference: MemberReference, key: PropertyKey, next: unknown, node: AstNode): void { if (Array.isArray(reference.target)) { const target = reference.target if (typeof key !== "number" || parseArrayIndex(key) === undefined) { throw new InterpreterRuntimeError( "Array assignment index must be a valid array index.", node, "InvalidDataValue", ) } rejectCircularInsertion(target, next, "Array assignment result", node) target[key] = next return } if (reference.target instanceof CodeModeURL) { const property = key as string if (!urlWritableProperties.has(property)) { throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") } try { const url = reference.target.url as unknown as Record url[property] = uriArgument(next, `URL.${property} value`) return } catch (error) { if (error instanceof InterpreterRuntimeError || error instanceof ToolRuntimeError) throw error throw new InterpreterRuntimeError(`URL.${property} received an invalid value.`, node).as("TypeError") } } if (reference.target instanceof CodeModeRegExp) { reference.target.lastIndex = next return } const target = reference.target as SafeObject rejectCircularInsertion(target, next, "Object assignment result", node) Reflect.set(target, key, next) } private toPropertyKey(value: unknown, node: AstNode): PropertyKey { if (typeof value === "string" || typeof value === "number") { return value } if (value === AsyncIteratorSymbol || value === IteratorSymbol) return value throw new InterpreterRuntimeError( "Property key must be a string or number, or Symbol.asyncIterator/Symbol.iterator.", node, ) } }