import { Cause, Deferred, Effect, Exit } from "effect"
import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
import {
  type AstNode,
  AsyncIteratorSymbol,
  asNode,
  type Binding,
  CodeModeFunction,
  CodeModeGenerator,
  CoercionFunction,
  ComputedValue,
  ErrorConstructorReference,
  GlobalMethodReference,
  GlobalNamespace,
  GeneratorMethodReference,
  GeneratorReturn,
  type GeneratorRequestKind,
  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,
  unsupportedSyntax,
  UriFunction,
} from "./model.js"
import { caughtErrorValue, constructAggregateErrorValue, constructErrorValue } from "./errors.js"
import {
  arrayStatics,
  type CallbackRunner,
  invokeArrayFrom,
  invokeGlobalMethod,
  invokeGroupBy,
  invokeIntrinsic,
} from "./methods.js"
import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js"
import {
  constructPromise,
  invokePromiseInstanceMethod,
  invokePromiseMethod,
  PromiseRuntime,
  resolvePromise,
  resolvePromiseValue,
} from "./promises.js"
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
import { ScopeStack } from "./scope.js"
import { arrayMethods, mapMethods, mapStatics, setMethods } 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 { invokeMathSumPrecise, mathConstants, mathMethods } from "../stdlib/math.js"
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
import { invokeObjectFromEntries, 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<Record<GlobalNamespaceName, Set<string>>> = {
  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<string> = []): Array<string> => {
  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 | CodeModeGenerator
  next: unknown
  asynchronous: boolean
}

type OpaqueMemberReference =
  | ToolReference
  | PromiseMethodReference
  | PromiseInstanceMethodReference
  | IntrinsicReference
  | GlobalMethodReference
  | JsonMethodReference
  | GeneratorMethodReference

const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference =>
  value instanceof ToolReference ||
  value instanceof PromiseMethodReference ||
  value instanceof PromiseInstanceMethodReference ||
  value instanceof IntrinsicReference ||
  value instanceof GlobalMethodReference ||
  value instanceof JsonMethodReference ||
  value instanceof GeneratorMethodReference

const copyIteratorSymbols = (source: object, target: object, consumed?: ReadonlySet<PropertyKey>): void => {
  for (const symbol of IteratorSymbols) {
    if (!consumed?.has(symbol) && Object.hasOwn(source, symbol))
      Reflect.set(target, symbol, Reflect.get(source, symbol))
  }
}

type GeneratorRequest = {
  kind: GeneratorRequestKind
  value: unknown
  response: Deferred.Deferred<unknown, unknown>
}

type GeneratorState = {
  started: boolean
  completed: boolean
  draining: boolean
  active?: GeneratorRequest
  pending: Array<GeneratorRequest>
  pendingIndex: number
  available?: Deferred.Deferred<void>
}

const promiseResolutionNode: AstNode = { type: "PromiseResolution" }

export class Interpreter<R> {
  private scopes: ScopeStack
  private readonly executeTool: (
    path: ReadonlyArray<string>,
    args: Array<unknown>,
  ) => Effect.Effect<unknown, unknown, R>
  private readonly invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
  private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
  private readonly logs: Array<string>
  private readonly promises: PromiseRuntime<R>
  private generatorState?: GeneratorState
  private generatorAsync = false
  private readonly runner: CallbackRunner<R> & SyncIteratorRunner<R> = {
    invokeFunction: (fn, args) => this.invokeFunction(fn, args),
    invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node),
    settlePromise: (promise) => this.settlePromise(promise),
    syncIterator: (value, node) => this.syncIterator(value, node),
  }

  constructor(
    executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
    invokeSearch: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>,
    toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>,
    promises: PromiseRuntime<R>,
    logs: Array<string> = [],
  ) {
    const globalScope = new Map<string, Binding>()
    this.scopes = new ScopeStack([globalScope])
    this.executeTool = executeTool
    this.invokeSearch = invokeSearch
    this.toolKeys = toolKeys
    this.logs = logs
    this.promises = promises
    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<unknown, unknown, R> {
    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.
      value = yield* resolvePromiseValue(self.runner, value, program)
      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<string>,
    args: Array<unknown>,
  ): Effect.Effect<CodeModePromise, never, R> {
    return this.createPromise(Effect.suspend(() => this.executeTool(path, args)))
  }

  private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<CodeModePromise, never, R> {
    return this.promises.create(effect)
  }

  // Fiber exits make settlement idempotent; yielding prevents inline continuation.
  private settlePromise(promise: CodeModePromise): Effect.Effect<unknown, unknown, never> {
    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<StatementResult, unknown, R> {
    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<StatementResult, unknown, R> {
    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 {
    return new CodeModeFunction(
      getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
      getNode(node, "body"),
      this.scopes.capture(),
      node.async === true,
      node.generator === true,
    )
  }

  private hoistFunctions(statements: Array<unknown>): 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<unknown>): 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<StatementResult, unknown, R> {
    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<StatementResult, unknown, R> {
    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<string>,
  ): Effect.Effect<StatementResult, unknown, R> {
    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<string>,
  ): Effect.Effect<StatementResult, unknown, R> {
    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<string>,
  ): Effect.Effect<StatementResult, unknown, R> {
    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<string>,
  ): Effect.Effect<StatementResult, unknown, R> {
    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 iterator = yield* self.customIterator(right, node, awaiting)
      const cursor = iterator === undefined ? yield* self.syncIterator(right, node) : undefined
      if (iterator === undefined && cursor === undefined) {
        throw new InterpreterRuntimeError(
          `${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`,
          node,
        ).as("TypeError")
      }
      const close = () =>
        iterator
          ? self.closeIterator(iterator, node, awaiting)
          : awaiting
            ? Effect.andThen(cursor?.close ?? Effect.void, Effect.yieldNow)
            : (cursor?.close ?? Effect.void)

      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()
            }),
          ),
        )

      while (true) {
        const current = iterator
          ? yield* self.nextIteratorResult(iterator, node, awaiting)
          : yield* cursor?.next ?? Effect.fail(new InterpreterRuntimeError("Iterator is unavailable.", node))
        const step = cursor && awaiting ? { done: current.done, value: yield* self.awaitValue(current.value) } : current
        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(close())
          }
          return yield* Effect.failCause(bodyExit.cause)
        }
        const result = bodyExit.value

        if (result.kind === "return") {
          yield* close()
          return result
        }

        if (result.kind === "break") {
          yield* close()
          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* close()
          return result
        }
      }
    }).pipe(
      Effect.ensuring(
        Effect.sync(() => {
          if (declared?.lexical) self.scopes.pop()
        }),
      ),
    )
  }

  private awaitValue(value: unknown, node: AstNode = promiseResolutionNode): Effect.Effect<unknown, unknown, R> {
    return Effect.flatMap(resolvePromise(this.runner, this.promises, value, node), (promise) =>
      this.settlePromise(promise),
    )
  }

  private awaitAsyncFromSyncValue(
    iterator: CustomIterator,
    value: unknown,
    node: AstNode,
    closeOnRejection: boolean,
  ): Effect.Effect<unknown, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      const settled = yield* Effect.exit(self.awaitValue(value))
      if (Exit.isSuccess(settled)) return settled.value
      if (closeOnRejection && !Cause.hasInterruptsOnly(settled.cause)) {
        yield* Effect.exit(self.closeIterator(iterator, node, false))
      }
      return yield* Effect.failCause(settled.cause)
    })
  }

  private syncIterator(value: unknown, node: AstNode) {
    const iterator = Array.isArray(value)
      ? value[Symbol.iterator]()
      : typeof value === "string"
        ? value[Symbol.iterator]()
        : value instanceof CodeModeMap
          ? value.map.entries()
          : value instanceof CodeModeSet
            ? value.set.values()
            : value instanceof CodeModeURLSearchParams
              ? value.params.entries()
              : undefined
    if (iterator !== undefined) {
      return Effect.succeed({
        next: Effect.sync(() => {
          const step = iterator.next()
          return { done: Boolean(step.done), value: step.value }
        }),
        close: Effect.void,
      })
    }
    const self = this
    return Effect.map(this.customIterator(value, node, false), (iterator) =>
      iterator === undefined
        ? undefined
        : {
            next: self.nextIteratorResult(iterator, node, false),
            close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
          },
    )
  }

  private customIterator(value: unknown, node: AstNode, allowAsync = true) {
    if (value instanceof CodeModeGenerator) {
      if (value.asynchronous && !allowAsync) return Effect.succeed(undefined)
      return Effect.succeed({
        iterator: value,
        next: new GeneratorMethodReference(value, "next"),
        asynchronous: value.asynchronous,
      })
    }
    if (!isRecord(value) || isRuntimeReference(value)) return Effect.succeed(undefined)
    const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined
    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.requireIterator(iterator, node)
        return {
          iterator: object,
          next:
            object instanceof CodeModeGenerator
              ? new GeneratorMethodReference(object, "next")
              : self.requireIteratorMethod(object.next, "Iterator next", node),
          asynchronous: asyncMethod !== undefined && asyncMethod !== null,
        }
      },
    )
  }

  private nextIteratorResult(iterator: CustomIterator, node: AstNode, awaiting: boolean) {
    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)) {
        if (awaiting) 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)) {
        if (awaiting) yield* Effect.yieldNow
        return yield* Effect.failCause(captured.cause)
      }
      return {
        done: captured.value.done,
        value: awaiting
          ? yield* self.awaitAsyncFromSyncValue(iterator, captured.value.value, node, !captured.value.done)
          : captured.value.value,
      }
    })
  }

  private closeIterator(iterator: CustomIterator, node: AstNode, awaiting = true): Effect.Effect<void, unknown, R> {
    const close =
      iterator.iterator instanceof CodeModeGenerator
        ? new GeneratorMethodReference(iterator.iterator, "return")
        : iterator.iterator.return
    if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? 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)) {
        if (awaiting) 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)) {
        if (awaiting) yield* Effect.yieldNow
        return yield* Effect.failCause(captured.cause)
      }
      if (awaiting) 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 requireIterator(value: unknown, node: AstNode): SafeObject | CodeModeGenerator {
    return value instanceof CodeModeGenerator
      ? value
      : this.requireIteratorObject(value, "Iterator method result", node)
  }

  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<string> | 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<string>,
  ): Effect.Effect<StatementResult, unknown, R> {
    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<StatementResult, unknown, R> {
    const labels = new Set<string>()
    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<StatementResult, unknown, R> {
    const argument = getNode(node, "argument")
    return Effect.flatMap(this.evaluateExpression(argument), (value) => Effect.fail(new ProgramThrow(value)))
  }

  private evaluateTryStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
    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) || Cause.squash(cause) instanceof GeneratorReturn || !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<void, unknown, R> {
    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<void, unknown, R> {
    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<PropertyKey>()
        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
            }
            copyIteratorSymbols(value, rest, consumed)
            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<unknown>, key),
            mutable,
            property,
            initialize,
          )
        }
        return
      }

      if (pattern.type === "ArrayPattern") {
        return yield* self.destructureArrayPattern(pattern, value, (target, item, context) =>
          self.declarePattern(target, item, mutable, context, initialize),
        )
      }

      throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
    })
  }

  private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect<void, unknown, R> {
    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<unknown>
        const consumed = new Set<PropertyKey>()
        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
            }
            copyIteratorSymbols(source, rest, consumed)
            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") {
        return yield* self.destructureArrayPattern(pattern, value, (target, item, context) =>
          self.assignPattern(target, item, context),
        )
      }

      throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
    })
  }

  private destructureArrayPattern(
    pattern: AstNode,
    value: unknown,
    consume: (target: AstNode, value: unknown, context: AstNode) => Effect.Effect<void, unknown, R>,
  ): Effect.Effect<void, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      const cursor = yield* self.syncIterator(value, pattern)
      if (cursor === undefined) {
        throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern).as(
          "TypeError",
        )
      }
      let done = false
      for (const [index, item] of getArray(pattern, "elements").entries()) {
        if (done) {
          if (item === null) continue
          const element = asNode(item, `elements[${index}]`)
          yield* consume(
            element.type === "RestElement" ? getNode(element, "argument") : element,
            element.type === "RestElement" ? [] : undefined,
            element,
          )
          if (element.type === "RestElement") return
          continue
        }
        const step = yield* cursor.next
        done = step.done
        if (item === null) continue
        const element = asNode(item, `elements[${index}]`)
        if (element.type === "RestElement") {
          const rest: Array<unknown> = []
          if (!step.done) rest.push(step.value)
          while (!done) {
            const next = yield* cursor.next
            done = next.done
            if (!done) rest.push(next.value)
          }
          yield* consume(getNode(element, "argument"), rest, element)
          return
        }
        const consumed = consume(element, step.done ? undefined : step.value, pattern)
        yield* step.done ? consumed : preserveConsumerError(cursor, consumed)
      }
      if (!done) yield* cursor.close
    })
  }

  private destructuringPropertyKey(property: AstNode): Effect.Effect<PropertyKey, unknown, R> {
    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<unknown>, 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<unknown, unknown, R> {
    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.
        return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
          this.awaitValue(value, node),
        )
      }
      case "YieldExpression":
        return this.evaluateYieldExpression(node)
      case "NewExpression":
        return this.evaluateNewExpression(node)
      default:
        throw unsupportedSyntax(node.type, node)
    }
  }

  private evaluateNewExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
    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.flatMap(this.evaluateCallArguments(argNodes), (args) =>
        name === "AggregateError"
          ? constructAggregateErrorValue(self.runner, args, node)
          : Effect.succeed(constructErrorValue(name, args)),
      )
    }
    // 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 yield* self.constructMap(args[0], node)
          case "Set":
            return yield* self.constructSet(args[0], node)
          case "URL":
            return self.constructURL(args, node)
          default:
            return yield* self.constructURLSearchParams(args[0], node)
        }
      })
    }
    throw unsupportedSyntax("NewExpression", node)
  }

  private constructArray(args: Array<unknown>, node: AstNode): Array<unknown> {
    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<unknown>, 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<unknown>, node: AstNode): Effect.Effect<CodeModeDate, unknown, R> {
    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<unknown, unknown, R> {
    if (value === null || (typeof value !== "object" && typeof value !== "function")) return Effect.succeed(value)
    const object = value as Record<string, unknown>
    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<unknown>, 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): Effect.Effect<CodeModeMap, unknown, R> {
    const target = new CodeModeMap()
    if (init === undefined || init === null) return Effect.succeed(target)
    const self = this
    return Effect.gen(function* () {
      const cursor = yield* self.syncIterator(init, node)
      if (cursor === undefined) {
        throw new InterpreterRuntimeError(
          "new Map(...) expects an iterable of [key, value] pairs or no argument.",
          node,
        ).as("TypeError")
      }
      while (true) {
        const step = yield* cursor.next
        if (step.done) return target
        yield* preserveConsumerError(
          cursor,
          Effect.sync(() => {
            if (!isRecord(step.value) || isRuntimeReference(step.value)) {
              throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs as entry objects.", node).as(
                "TypeError",
              )
            }
            target.map.set(step.value[0], step.value[1])
          }),
        )
      }
    })
  }

  private constructSet(init: unknown, node: AstNode): Effect.Effect<CodeModeSet, unknown, R> {
    const target = new CodeModeSet()
    if (init === undefined || init === null) return Effect.succeed(target)
    const self = this
    return Effect.gen(function* () {
      const cursor = yield* self.syncIterator(init, node)
      if (cursor === undefined) {
        throw new InterpreterRuntimeError("new Set(...) expects a synchronous iterable or no argument.", node).as(
          "TypeError",
        )
      }
      while (true) {
        const step = yield* cursor.next
        if (step.done) return target
        target.set.add(step.value)
      }
    })
  }

  private constructURL(args: Array<unknown>, 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): Effect.Effect<CodeModeURLSearchParams, unknown, R> {
    if (init === undefined) return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams()))
    if (init instanceof CodeModeURLSearchParams) {
      return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init.params)))
    }
    if (typeof init === "string") return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init)))
    if (init === null || typeof init === "number" || typeof init === "boolean") {
      return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init))))
    }
    const self = this
    return Effect.gen(function* () {
      const cursor = yield* self.syncIterator(init, node)
      if (cursor !== undefined) {
        const entries: Array<Array<string>> = []
        while (true) {
          const step = yield* cursor.next
          if (step.done) {
            if (entries.some((entry) => entry.length !== 2)) {
              throw new InterpreterRuntimeError(
                "new URLSearchParams(...) expects iterable [name, value] pairs.",
                node,
              ).as("TypeError")
            }
            return new CodeModeURLSearchParams(
              new URLSearchParams(entries.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])),
            )
          }
          entries.push(yield* preserveConsumerError(cursor, self.readURLSearchParamsPair(step.value, node)))
        }
      }
      if (isRuntimeReference(init)) {
        throw new InterpreterRuntimeError(
          "new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.",
          node,
        ).as("TypeError")
      }
      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, iterable pairs, or URLSearchParams.",
          node,
        ).as("TypeError")
      }
      return new CodeModeURLSearchParams(
        new URLSearchParams(
          Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)])),
        ),
      )
    })
  }

  private readURLSearchParamsPair(value: unknown, node: AstNode): Effect.Effect<Array<string>, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      const cursor = yield* self.syncIterator(value, node)
      if (cursor === undefined) {
        throw new InterpreterRuntimeError("new URLSearchParams(...) expects iterable [name, value] pairs.", node).as(
          "TypeError",
        )
      }
      const items: Array<string> = []
      while (true) {
        const step = yield* cursor.next
        if (step.done) return items
        items.push(
          yield* preserveConsumerError(
            cursor,
            Effect.sync(() => uriArgument(step.value, "URLSearchParams pair value")),
          ),
        )
      }
    })
  }

  private evaluateBinaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
    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 (operator === "===") return lhs === rhs
    if (operator === "!==") return lhs !== rhs
    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 bothObjects ? lhs !== rhs : l != r
      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<unknown, unknown, R> {
    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<unknown, unknown, R> {
    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<unknown, unknown, R> {
    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<unknown, unknown, R> {
    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<unknown, unknown, R> {
    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<unknown, unknown, R> {
    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<unknown>,
    node: AstNode,
    callee: AstNode = node,
  ): Effect.Effect<unknown, unknown, R> {
    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 GeneratorMethodReference) {
        if (callable.kind === "iterator") return callable.generator
        const requested = callable.generator.request(callable.kind, args[0], node) as Effect.Effect<unknown, unknown, R>
        return callable.generator.asynchronous ? yield* self.createPromise(requested) : yield* requested
      }
      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)) {
          if (callable.name === "fromEntries") return yield* invokeObjectFromEntries(self.runner, args[0], node)
          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 === "Math" && callable.name === "sumPrecise") {
          return yield* invokeMathSumPrecise(self.runner, args[0], 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) {
        if (callable.name === "AggregateError") return yield* constructAggregateErrorValue(self.runner, args, node)
        return constructErrorValue(callable.name, args)
      }
      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<unknown>, 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<unknown>): Effect.Effect<Array<unknown>, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      const args: Array<unknown> = []
      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 cursor = yield* self.syncIterator(spread, argNode)
          if (cursor === undefined)
            throw new InterpreterRuntimeError("Spread arguments require a synchronous iterable.", argNode).as(
              "TypeError",
            )
          while (true) {
            const step = yield* cursor.next
            if (step.done) break
            args.push(step.value)
          }
        } else {
          args.push(yield* self.evaluateExpression(argNode))
        }
      }
      return args
    })
  }

  private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
    const invocation = new Interpreter(this.executeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs)
    invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
    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.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
    if (!fn.async) return run
    // The initial yield assigns the promise before the body can self-resolve.
    const box: { promise?: CodeModePromise } = {}
    return Effect.map(
      this.createPromise(Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runner, value, fn.body, box))),
      (promise) => {
        box.promise = promise
        return promise
      },
    )
  }

  private createGenerator(
    invocation: Interpreter<R>,
    run: Effect.Effect<unknown, unknown, R>,
    asynchronous: boolean,
  ): CodeModeGenerator {
    const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
    invocation.generatorState = state
    invocation.generatorAsync = asynchronous
    const generator = new CodeModeGenerator(asynchronous, (kind, value, node) => {
      const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
      if (!asynchronous && state.active) {
        return Effect.fail(new InterpreterRuntimeError("Generator is already running.", node).as("TypeError"))
      }
      if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
        state.started = true
        state.completed = true
        state.pending.push(request)
        if (state.draining) return Deferred.await(request.response)
        state.draining = true
        return Effect.andThen(
          this.promises.fork(
            invocation
              .completeGeneratorRequests(state, true)
              .pipe(Effect.ensuring(Effect.sync(() => (state.draining = false)))),
          ),
          Deferred.await(request.response),
        )
      }
      if (state.completed) {
        if (kind === "throw") return Effect.fail(new ProgramThrow(value))
        return Effect.succeed({ value: kind === "return" ? value : undefined, done: true })
      }
      if (!state.started && kind !== "next") {
        state.completed = true
        if (kind === "throw") return Effect.fail(new ProgramThrow(value))
        return Effect.succeed({ value, done: true })
      }

      state.pending.push(request)
      if (state.available) {
        const available = state.available
        state.available = undefined
        Deferred.doneUnsafe(available, Exit.succeed(undefined))
      }
      if (!state.started) {
        state.started = true
        const body = Effect.gen(function* () {
          state.active = yield* invocation.takeGeneratorRequest(state)
          const exit = yield* Effect.exit(
            run.pipe(
              Effect.flatMap((result) => (asynchronous ? invocation.awaitValue(result) : Effect.succeed(result))),
              Effect.catch((error) =>
                error instanceof GeneratorReturn
                  ? asynchronous
                    ? invocation.awaitValue(error.value)
                    : Effect.succeed(error.value)
                  : Effect.fail(error),
              ),
            ),
          )
          const active = state.active
          state.active = undefined
          if (active) {
            Deferred.doneUnsafe(
              active.response,
              Exit.isSuccess(exit) ? Exit.succeed({ value: exit.value, done: true }) : exit,
            )
          }
          yield* invocation.completeGeneratorRequests(state, asynchronous)
          state.completed = true
        })
        return Effect.andThen(this.promises.fork(body), Deferred.await(request.response))
      }
      return Deferred.await(request.response)
    })
    return generator
  }

  private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
    const self = this
    return Effect.gen(function* () {
      while (true) {
        const pending = self.dequeueGeneratorRequest(state)
        if (!pending) return
        if (pending.kind === "throw") {
          Deferred.doneUnsafe(pending.response, Exit.fail(new ProgramThrow(pending.value)))
          continue
        }
        if (asynchronous && pending.kind === "return") {
          const resolved = yield* Effect.exit(self.awaitValue(pending.value))
          Deferred.doneUnsafe(
            pending.response,
            Exit.isSuccess(resolved) ? Exit.succeed({ value: resolved.value, done: true }) : resolved,
          )
          continue
        }
        Deferred.doneUnsafe(
          pending.response,
          Exit.succeed({ value: pending.kind === "return" ? pending.value : undefined, done: true }),
        )
      }
    })
  }

  private takeGeneratorRequest(state: GeneratorState): Effect.Effect<GeneratorRequest> {
    const next = this.dequeueGeneratorRequest(state)
    if (next) return Effect.succeed(next)
    state.available = Deferred.makeUnsafe<void>()
    return Effect.andThen(
      Deferred.await(state.available),
      Effect.sync(() => this.dequeueGeneratorRequest(state)!),
    )
  }

  private dequeueGeneratorRequest(state: GeneratorState): GeneratorRequest | undefined {
    const request = state.pending[state.pendingIndex]
    if (!request) return undefined
    state.pendingIndex += 1
    if (state.pendingIndex === state.pending.length) {
      state.pending = []
      state.pendingIndex = 0
    }
    return request
  }

  private evaluateYieldExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
    const argument = getOptionalNode(node, "argument")
    const self = this
    return Effect.gen(function* () {
      if (!self.generatorState) throw new InterpreterRuntimeError("yield is only valid inside a generator.", node)
      if (node.delegate === true) {
        const value = argument ? yield* self.evaluateExpression(argument) : undefined
        return yield* self.delegateYield(value, node)
      }
      const value = argument ? yield* self.evaluateExpression(argument) : undefined
      const yielded = self.generatorAsync ? yield* self.awaitValue(value) : value
      return yield* self.suspendGenerator(yielded, node)
    })
  }

  private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
    const state = this.generatorState
    if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node)
    Deferred.doneUnsafe(state.active.response, Exit.succeed({ value, done: false }))
    state.active = undefined
    return Effect.flatMap(this.takeGeneratorRequest(state), (request) => {
      state.active = request
      if (request.kind === "next") return Effect.succeed(request.value)
      if (request.kind === "throw") return Effect.fail(new ProgramThrow(request.value))
      return this.generatorAsync
        ? Effect.flatMap(this.awaitValue(request.value), (value) => Effect.fail(new GeneratorReturn(value)))
        : Effect.fail(new GeneratorReturn(request.value))
    })
  }

  private delegateYield(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      if (
        Array.isArray(value) ||
        typeof value === "string" ||
        value instanceof CodeModeMap ||
        value instanceof CodeModeSet ||
        value instanceof CodeModeURLSearchParams
      ) {
        const cursor = yield* self.syncIterator(value, node)
        if (!cursor) throw new InterpreterRuntimeError("Built-in iterator is unavailable.", node)
        while (true) {
          const step = yield* cursor.next
          if (step.done) return undefined
          const resumed = yield* Effect.exit(
            self.suspendGenerator(self.generatorAsync ? yield* self.awaitValue(step.value) : step.value, node),
          )
          if (Exit.isSuccess(resumed)) continue
          const error = Cause.squash(resumed.cause)
          if (error instanceof GeneratorReturn) {
            yield* cursor.close
            return yield* Effect.fail(error)
          }
          if (error instanceof ProgramThrow) {
            yield* cursor.close
            throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as(
              "TypeError",
            )
          }
          return yield* Effect.failCause(resumed.cause)
        }
      }

      const iterator = yield* self.customIterator(value, node, self.generatorAsync)
      if (!iterator)
        throw new InterpreterRuntimeError("yield* requires a compatible iterable value.", node).as("TypeError")
      let kind: GeneratorRequestKind = "next"
      let input: unknown = undefined
      while (true) {
        const method =
          kind === "next"
            ? iterator.next
            : iterator.iterator instanceof CodeModeGenerator
              ? new GeneratorMethodReference(iterator.iterator, kind)
              : iterator.iterator[kind]
        if (method === undefined || method === null) {
          if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input))
          yield* self.closeIterator(iterator, node, self.generatorAsync)
          throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as(
            "TypeError",
          )
        }
        const called = yield* self.invokeCallable(
          self.requireIteratorMethod(method, `Iterator ${kind}`, node),
          [input],
          node,
        )
        const result = self.requireIteratorObject(
          iterator.asynchronous ? yield* self.awaitValue(called) : called,
          `Iterator ${kind}() result`,
          node,
        )
        const done = Boolean(result.done)
        const resultValue: unknown =
          self.generatorAsync && !iterator.asynchronous
            ? yield* self.awaitAsyncFromSyncValue(iterator, result.value, node, kind !== "return" && !done)
            : result.value
        if (done) {
          if (kind === "return") return yield* Effect.fail(new GeneratorReturn(resultValue))
          return resultValue
        }

        const resumed: Exit.Exit<unknown, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
        if (Exit.isSuccess(resumed)) {
          kind = "next"
          input = resumed.value
          continue
        }
        const error: unknown = Cause.squash(resumed.cause)
        if (!(error instanceof GeneratorReturn) && !(error instanceof ProgramThrow)) {
          return yield* Effect.failCause(resumed.cause)
        }
        kind = error instanceof GeneratorReturn ? "return" : "throw"
        input = error.value
      }
    })
  }

  private evaluateObjectExpression(node: AstNode): Effect.Effect<Record<string, unknown>, unknown, R> {
    const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
    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
          }
          copyIteratorSymbols(spread, objectValue)
          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<Array<unknown>, unknown, R> {
    const elements = getArray(node, "elements")
    const values: Array<unknown> = []

    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 cursor = yield* self.syncIterator(spread, element)
          if (cursor === undefined)
            throw new InterpreterRuntimeError("Array spread requires a synchronous iterable.", element).as("TypeError")
          while (true) {
            const step = yield* cursor.next
            if (step.done) break
            values.push(step.value)
          }
        } else {
          values.push(yield* self.evaluateExpression(element))
        }
      }
      return values
    })
  }

  private evaluateTemplateLiteral(node: AstNode): Effect.Effect<string, unknown, R> {
    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<unknown, unknown, R> {
    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
    | GeneratorMethodReference
    | 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<string, number>)[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<string, number>)[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<string, unknown>)[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 (objectValue instanceof CodeModeGenerator) {
        if (key === "next" || key === "return" || key === "throw") {
          return new GeneratorMethodReference(objectValue, key)
        }
        if (
          (key === IteratorSymbol && !objectValue.asynchronous) ||
          (key === AsyncIteratorSymbol && objectValue.asynchronous)
        ) {
          return new GeneratorMethodReference(objectValue, "iterator")
        }
        return new ComputedValue(undefined)
      }

      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<string, unknown> & Array<unknown>)[key])
          }
          return new ComputedValue(undefined)
        }
        return { target: objectValue, key: index ?? key }
      }

      return { target: objectValue as SafeObject, key }
    })
  }

  private readMember(node: AstNode): Effect.Effect<unknown, unknown, R> {
    return Effect.map(this.getMemberReference(node), (reference) => {
      if (reference === OptionalShortCircuit) return OptionalShortCircuit
      if (reference instanceof ComputedValue) return reference.value
      if (reference === undefined || isOpaqueMemberReference(reference)) 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<unknown, unknown, R> {
    return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
  }

  private evaluateDeleteExpression(argument: AstNode): Effect.Effect<boolean, unknown, R> {
    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 ||
        isOpaqueMemberReference(reference) ||
        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<unknown, unknown, R> {
    const self = this
    return Effect.gen(function* () {
      const reference = yield* self.getMemberReference(node)
      if (
        reference === OptionalShortCircuit ||
        reference instanceof ComputedValue ||
        reference === undefined ||
        isOpaqueMemberReference(reference)
      ) {
        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<string, string>
        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,
    )
  }
}
