2188 lines
86 KiB
TypeScript
2188 lines
86 KiB
TypeScript
import { Cause, Effect } from "effect"
|
|
import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
|
|
import {
|
|
type AstNode,
|
|
asNode,
|
|
type Binding,
|
|
CodeModeFunction,
|
|
CoercionFunction,
|
|
ComputedValue,
|
|
ErrorConstructorReference,
|
|
GlobalMethodReference,
|
|
GlobalNamespace,
|
|
type GlobalNamespaceName,
|
|
getArray,
|
|
getBoolean,
|
|
getNode,
|
|
getOptionalNode,
|
|
getString,
|
|
IntrinsicReference,
|
|
InterpreterRuntimeError,
|
|
isRecord,
|
|
type MemberReference,
|
|
OptionalShortCircuit,
|
|
PromiseCapabilityFunction,
|
|
PromiseInstanceMethodReference,
|
|
PromiseMethodReference,
|
|
type PromiseMethodName,
|
|
PromiseNamespace,
|
|
ProgramThrow,
|
|
type ProgramNode,
|
|
SearchFunction,
|
|
type StatementResult,
|
|
supportedSyntaxMessage,
|
|
unsupportedSyntax,
|
|
UriFunction,
|
|
} from "./model.js"
|
|
import { caughtErrorValue, constructErrorValue } from "./errors.js"
|
|
import { arrayStatics, type CallbackRunner, invokeArrayFrom, invokeGlobalMethod, invokeIntrinsic } from "./methods.js"
|
|
import {
|
|
constructPromise,
|
|
invokePromiseInstanceMethod,
|
|
invokePromiseMethod,
|
|
PromiseRuntime,
|
|
selfResolutionError,
|
|
} from "./promises.js"
|
|
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
|
|
import { ScopeStack } from "./scope.js"
|
|
import { arrayMethods, mapMethods, setMethods, spreadItems } from "../stdlib/collections.js"
|
|
import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js"
|
|
import { dateMethods, dateStatics } from "../stdlib/date.js"
|
|
import { jsonStatics } from "../stdlib/json.js"
|
|
import { mathConstants, mathMethods } from "../stdlib/math.js"
|
|
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
|
|
import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js"
|
|
import { promiseStatics } from "../stdlib/promise.js"
|
|
import {
|
|
escapeRegexHint,
|
|
regexpMethods,
|
|
regexpProperties,
|
|
regexpStatics,
|
|
regexFailureReason,
|
|
} from "../stdlib/regexp.js"
|
|
import { stringMethods, stringStatics } from "../stdlib/string.js"
|
|
import {
|
|
urlMethods,
|
|
urlProperties,
|
|
urlSearchParamsMethods,
|
|
urlStatics,
|
|
urlWritableProperties,
|
|
invokeUriFunction,
|
|
uriArgument,
|
|
urlArgument,
|
|
} from "../stdlib/url.js"
|
|
import {
|
|
boundedData,
|
|
coerceToNumber,
|
|
coerceToString,
|
|
compoundOperators,
|
|
errorBrandName,
|
|
errorConstructors,
|
|
invokeCoercion,
|
|
valueConstructors,
|
|
} from "../stdlib/value.js"
|
|
import {
|
|
isCodeModeValue,
|
|
CodeModeDate,
|
|
CodeModeMap,
|
|
CodeModePromise,
|
|
CodeModeRegExp,
|
|
CodeModeSet,
|
|
CodeModeURL,
|
|
CodeModeURLSearchParams,
|
|
} from "../values.js"
|
|
|
|
const globalStaticMembers: Partial<Record<GlobalNamespaceName, Set<string>>> = {
|
|
Object: objectStatics,
|
|
Math: mathMethods,
|
|
JSON: jsonStatics,
|
|
Array: arrayStatics,
|
|
console: consoleMethods,
|
|
Date: dateStatics,
|
|
RegExp: regexpStatics,
|
|
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 constructor CodeMode knows: 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",
|
|
}
|
|
}
|
|
|
|
export class Interpreter<R> {
|
|
private scopes: ScopeStack
|
|
private readonly invokeTool: (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 readonly runner: CallbackRunner<R> = {
|
|
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
|
|
invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node),
|
|
settlePromise: (promise) => this.settlePromise(promise),
|
|
}
|
|
|
|
constructor(
|
|
invokeTool: (path: ReadonlyArray<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.invokeTool = invokeTool
|
|
this.invokeSearch = invokeSearch
|
|
this.toolKeys = toolKeys
|
|
this.logs = logs
|
|
this.promises = promises
|
|
globalScope.set("tools", { mutable: false, value: new ToolReference([]) })
|
|
globalScope.set("search", { mutable: false, value: new SearchFunction() })
|
|
globalScope.set("Promise", { mutable: false, value: new PromiseNamespace() })
|
|
globalScope.set("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.
|
|
if (value instanceof CodeModePromise) value = yield* self.settlePromise(value)
|
|
return value
|
|
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
|
}
|
|
|
|
// Fork at the call site so admission and hooks occur when the call is made.
|
|
private createToolCallPromise(
|
|
path: ReadonlyArray<string>,
|
|
args: Array<unknown>,
|
|
): Effect.Effect<CodeModePromise, never, R> {
|
|
return this.createPromise(Effect.suspend(() => this.invokeTool(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 "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 {
|
|
if (node.generator === true) {
|
|
throw new InterpreterRuntimeError(
|
|
"Generator functions are not supported in CodeMode.",
|
|
node,
|
|
"UnsupportedSyntax",
|
|
[supportedSyntaxMessage],
|
|
)
|
|
}
|
|
return new CodeModeFunction(
|
|
getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
|
|
getNode(node, "body"),
|
|
this.scopes.capture(),
|
|
node.async === true,
|
|
)
|
|
}
|
|
|
|
private hoistFunctions(statements: Array<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 in CodeMode.",
|
|
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 in CodeMode.",
|
|
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") return { kind: "none" } satisfies StatementResult
|
|
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): 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") {
|
|
continue
|
|
}
|
|
|
|
if (result.kind === "break") {
|
|
return { kind: "none" } satisfies StatementResult
|
|
}
|
|
|
|
if (result.kind === "return") {
|
|
return result
|
|
}
|
|
}
|
|
|
|
return { kind: "none" } satisfies StatementResult
|
|
})
|
|
}
|
|
|
|
private evaluateDoWhileStatement(node: AstNode): 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") {
|
|
continue
|
|
}
|
|
|
|
if (result.kind === "break") {
|
|
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): 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") {
|
|
return { kind: "none" } satisfies StatementResult
|
|
}
|
|
|
|
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): Effect.Effect<StatementResult, unknown, R> {
|
|
if (getBoolean(node, "await")) {
|
|
throw new InterpreterRuntimeError("for await...of is not supported.", node)
|
|
}
|
|
|
|
const left = getNode(node, "left")
|
|
const declared = loopDeclaration(left, "for...of")
|
|
if (declared?.lexical) this.scopes.push()
|
|
|
|
const self = this
|
|
return Effect.gen(function* () {
|
|
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
|
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
|
const body = getNode(node, "body")
|
|
|
|
const iterable = spreadItems(right)
|
|
if (iterable === undefined) {
|
|
throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
|
|
}
|
|
|
|
let assignment: AstNode | undefined
|
|
|
|
if (
|
|
left.type !== "VariableDeclaration" &&
|
|
(left.type === "Identifier" ||
|
|
left.type === "MemberExpression" ||
|
|
left.type === "ArrayPattern" ||
|
|
left.type === "ObjectPattern")
|
|
) {
|
|
assignment = left
|
|
} else if (left.type !== "VariableDeclaration") {
|
|
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
|
}
|
|
|
|
for (const value of iterable) {
|
|
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, value, declared.mutable, left, declared.lexical)
|
|
} else if (assignment) {
|
|
yield* self.assignPattern(assignment, value, left)
|
|
}
|
|
return yield* self.evaluateStatement(body)
|
|
}).pipe(
|
|
Effect.ensuring(
|
|
Effect.sync(() => {
|
|
if (declared) self.scopes.pop()
|
|
}),
|
|
),
|
|
)
|
|
|
|
if (result.kind === "return") {
|
|
return result
|
|
}
|
|
|
|
if (result.kind === "break") {
|
|
return { kind: "none" } satisfies StatementResult
|
|
}
|
|
|
|
if (result.kind === "continue") {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return { kind: "none" } satisfies StatementResult
|
|
}).pipe(
|
|
Effect.ensuring(
|
|
Effect.sync(() => {
|
|
if (declared?.lexical) self.scopes.pop()
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
|
|
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): 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 in CodeMode. 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") {
|
|
return { kind: "none" } satisfies StatementResult
|
|
}
|
|
|
|
if (result.kind === "continue") {
|
|
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")
|
|
|
|
if (labelNode) {
|
|
throw new InterpreterRuntimeError("Labeled break is not supported in v1.", node)
|
|
}
|
|
|
|
return { kind: "break" }
|
|
}
|
|
|
|
private evaluateContinueStatement(node: AstNode): StatementResult {
|
|
const labelNode = getOptionalNode(node, "label")
|
|
|
|
if (labelNode) {
|
|
throw new InterpreterRuntimeError("Labeled continue is not supported in v1.", node)
|
|
}
|
|
|
|
return { kind: "continue" }
|
|
}
|
|
|
|
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) || !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<string>()
|
|
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
|
|
}
|
|
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 in CodeMode.`, property)
|
|
}
|
|
consumed.add(String(key))
|
|
yield* self.declarePattern(
|
|
getNode(property, "value"),
|
|
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
|
mutable,
|
|
property,
|
|
initialize,
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (pattern.type === "ArrayPattern") {
|
|
const items = spreadItems(value)
|
|
if (items === undefined) {
|
|
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
|
|
}
|
|
|
|
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
|
if (item === null) continue
|
|
const element = asNode(item, `elements[${index}]`)
|
|
if (element.type === "RestElement") {
|
|
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize)
|
|
break
|
|
}
|
|
yield* self.declarePattern(element, items[index], mutable, pattern, initialize)
|
|
}
|
|
return
|
|
}
|
|
|
|
throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
|
|
})
|
|
}
|
|
|
|
private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect<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<string>()
|
|
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
|
|
}
|
|
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 in CodeMode.`, property)
|
|
}
|
|
consumed.add(String(key))
|
|
yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (pattern.type === "ArrayPattern") {
|
|
const items = spreadItems(value)
|
|
if (items === undefined) {
|
|
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
|
|
}
|
|
for (const [index, item] of getArray(pattern, "elements").entries()) {
|
|
if (item === null) continue
|
|
const element = asNode(item, `elements[${index}]`)
|
|
if (element.type === "RestElement") {
|
|
yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element)
|
|
break
|
|
}
|
|
yield* self.assignPattern(element, items[index], pattern)
|
|
}
|
|
return
|
|
}
|
|
|
|
throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
|
|
})
|
|
}
|
|
|
|
private destructuringPropertyKey(property: AstNode): Effect.Effect<string | number, 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: string | number): unknown {
|
|
if (!Array.isArray(source)) return source[String(key)]
|
|
if (key === "length") return source.length
|
|
if (typeof key === "number") return source[key]
|
|
if (Object.hasOwn(source, key)) return (source as Record<string, unknown> & Array<unknown>)[key]
|
|
if (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.
|
|
const self = this
|
|
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
|
|
value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value),
|
|
)
|
|
}
|
|
case "NewExpression":
|
|
return this.evaluateNewExpression(node)
|
|
default:
|
|
throw unsupportedSyntax(node.type, node)
|
|
}
|
|
}
|
|
|
|
private evaluateNewExpression(node: AstNode): Effect.Effect<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.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node))
|
|
}
|
|
// Array and Object construct identically with or without new, like JS.
|
|
if (name === "Array") {
|
|
return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructArray(args, node))
|
|
}
|
|
if (name === "Object") {
|
|
return Effect.map(this.evaluateCallArguments(argNodes), (args) => self.constructObject(args, node))
|
|
}
|
|
if (valueConstructors.has(name)) {
|
|
return Effect.gen(function* () {
|
|
const args = yield* self.evaluateCallArguments(argNodes)
|
|
switch (name) {
|
|
case "Date":
|
|
return yield* self.constructDate(args, node)
|
|
case "RegExp":
|
|
return self.constructRegExp(args, node)
|
|
case "Map":
|
|
return self.constructMap(args[0], node)
|
|
case "Set":
|
|
return self.constructSet(args[0], node)
|
|
case "URL":
|
|
return self.constructURL(args, node)
|
|
default:
|
|
return self.constructURLSearchParams(args[0], node)
|
|
}
|
|
})
|
|
}
|
|
throw unsupportedSyntax("NewExpression", node)
|
|
}
|
|
|
|
private constructArray(args: Array<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 in CodeMode; 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): CodeModeMap {
|
|
const target = new CodeModeMap()
|
|
if (init === undefined || init === null) return target
|
|
const entries = Array.isArray(init)
|
|
? init
|
|
: init instanceof CodeModeMap
|
|
? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item])
|
|
: undefined
|
|
if (entries === undefined) {
|
|
throw new InterpreterRuntimeError(
|
|
"new Map(...) expects an array of [key, value] pairs, a Map, or no argument.",
|
|
node,
|
|
)
|
|
}
|
|
for (const pair of entries) {
|
|
if (!Array.isArray(pair)) {
|
|
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node)
|
|
}
|
|
target.map.set(pair[0], pair[1])
|
|
}
|
|
return target
|
|
}
|
|
|
|
private constructSet(init: unknown, node: AstNode): CodeModeSet {
|
|
const target = new CodeModeSet()
|
|
if (init === undefined || init === null) return target
|
|
const items = Array.isArray(init)
|
|
? init
|
|
: init instanceof CodeModeSet
|
|
? Array.from(init.set.values())
|
|
: typeof init === "string"
|
|
? Array.from(init)
|
|
: undefined
|
|
if (items === undefined) {
|
|
throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node)
|
|
}
|
|
for (const item of items) target.set.add(item)
|
|
return target
|
|
}
|
|
|
|
private constructURL(args: Array<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): CodeModeURLSearchParams {
|
|
if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams())
|
|
if (init instanceof CodeModeURLSearchParams) {
|
|
return new CodeModeURLSearchParams(new URLSearchParams(init.params))
|
|
}
|
|
if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init))
|
|
if (init === null || typeof init === "number" || typeof init === "boolean") {
|
|
return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init)))
|
|
}
|
|
if (init instanceof CodeModeMap) {
|
|
return this.constructURLSearchParams(
|
|
Array.from(init.map.entries(), ([key, value]) => [key, value]),
|
|
node,
|
|
)
|
|
}
|
|
if (Array.isArray(init)) {
|
|
const entries = init.map((pair) => {
|
|
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
throw new InterpreterRuntimeError(
|
|
"new URLSearchParams(...) expects an array of [name, value] pairs.",
|
|
node,
|
|
).as("TypeError")
|
|
}
|
|
return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [
|
|
string,
|
|
string,
|
|
]
|
|
})
|
|
return new CodeModeURLSearchParams(new URLSearchParams(entries))
|
|
}
|
|
if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams())
|
|
const data = boundedData(init, "new URLSearchParams input")
|
|
if (data === null || typeof data !== "object") {
|
|
throw new InterpreterRuntimeError(
|
|
"new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.",
|
|
node,
|
|
).as("TypeError")
|
|
}
|
|
return new CodeModeURLSearchParams(
|
|
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
|
|
)
|
|
}
|
|
|
|
private evaluateBinaryExpression(node: AstNode): Effect.Effect<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 (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
|
|
throw new InterpreterRuntimeError("Binary operators require data values in CodeMode.", node, "InvalidDataValue")
|
|
}
|
|
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
|
|
// Dates use string coercion for `+` and epoch time elsewhere.
|
|
const coerceOperand = (operand: unknown): unknown => {
|
|
if (operand instanceof CodeModeDate) return operator === "+" ? coerceToString(operand) : operand.time
|
|
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
|
|
}
|
|
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
|
|
const l = coerceOperand(lhs)
|
|
const r = coerceOperand(rhs)
|
|
switch (operator) {
|
|
case "+":
|
|
return (l as string) + (r as string)
|
|
case "-":
|
|
return (l as number) - (r as number)
|
|
case "*":
|
|
return (l as number) * (r as number)
|
|
case "/":
|
|
return (l as number) / (r as number)
|
|
case "%":
|
|
return (l as number) % (r as number)
|
|
case "**":
|
|
return (l as number) ** (r as number)
|
|
case "==":
|
|
return bothObjects ? lhs === rhs : l == r
|
|
case "===":
|
|
return lhs === rhs
|
|
case "!=":
|
|
return bothObjects ? lhs !== rhs : l != r
|
|
case "!==":
|
|
return lhs !== rhs
|
|
case "<":
|
|
return (l as string) < (r as string)
|
|
case "<=":
|
|
return (l as string) <= (r as string)
|
|
case ">":
|
|
return (l as string) > (r as string)
|
|
case ">=":
|
|
return (l as string) >= (r as string)
|
|
case "&":
|
|
return (l as number) & (r as number)
|
|
case "|":
|
|
return (l as number) | (r as number)
|
|
case "^":
|
|
return (l as number) ^ (r as number)
|
|
case "<<":
|
|
return (l as number) << (r as number)
|
|
case ">>":
|
|
return (l as number) >> (r as number)
|
|
case ">>>":
|
|
return (l as number) >>> (r as number)
|
|
case "in":
|
|
if (rhs === null || typeof rhs !== "object") {
|
|
throw new InterpreterRuntimeError("The 'in' operator requires a data object on the right-hand side.", node)
|
|
}
|
|
// Never expose properties inherited from host prototypes.
|
|
return Object.hasOwn(rhs as object, coerceOperand(lhs) as PropertyKey)
|
|
default:
|
|
throw new InterpreterRuntimeError(`Unsupported binary operator '${operator}'.`, node)
|
|
}
|
|
}
|
|
|
|
private evaluateLogicalExpression(node: AstNode): Effect.Effect<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 in CodeMode.", 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 in CodeMode.`,
|
|
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 IntrinsicReference) {
|
|
return yield* invokeIntrinsic(self.runner, callable, args, node)
|
|
}
|
|
if (callable instanceof GlobalMethodReference) {
|
|
if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
|
|
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
|
|
return self.invokeObjectMethodOnTools(callable.name, args[0], node)
|
|
}
|
|
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
|
return invokeGlobalMethod(callable, args, node)
|
|
}
|
|
if (callable.namespace === "Array" && callable.name === "from") {
|
|
return yield* invokeArrayFrom(self.runner, args, node)
|
|
}
|
|
if (callable.namespace === "Array" && callable.name === "of") {
|
|
return invokeGlobalMethod(callable, args, node)
|
|
}
|
|
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
|
}
|
|
if (callable instanceof CoercionFunction) {
|
|
return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`)
|
|
}
|
|
if (callable instanceof UriFunction) {
|
|
return invokeUriFunction(callable, args, node)
|
|
}
|
|
if (callable instanceof SearchFunction) {
|
|
return yield* self.invokeSearch(args)
|
|
}
|
|
if (callable instanceof ErrorConstructorReference) {
|
|
return constructErrorValue(callable.name, args, node)
|
|
}
|
|
if (callable instanceof GlobalNamespace) {
|
|
// Real JS permits calling Array, Object, Date, and RegExp without new.
|
|
if (callable.name === "Array") return self.constructArray(args, node)
|
|
if (callable.name === "Object") return self.constructObject(args, node)
|
|
// ISO instead of the host's locale string: CodeMode date strings are
|
|
// deterministic and must not leak the host timezone.
|
|
if (callable.name === "Date") return new Date().toISOString()
|
|
if (callable.name === "RegExp") return self.constructRegExp(args, node)
|
|
if (typeofValue(callable) === "function") {
|
|
throw new InterpreterRuntimeError(`Constructor ${callable.name} requires 'new'.`, node).as("TypeError")
|
|
}
|
|
throw new InterpreterRuntimeError(`${callable.name} is not a function.`, node).as("TypeError")
|
|
}
|
|
if (callable instanceof PromiseNamespace) {
|
|
throw new InterpreterRuntimeError("Constructor Promise requires 'new'.", node).as("TypeError")
|
|
}
|
|
if (callable instanceof 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 in CodeMode.", 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 in CodeMode.`, 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 items = spreadItems(spread)
|
|
if (items === undefined)
|
|
throw new InterpreterRuntimeError(
|
|
"Spread arguments require an array, string, Map, or Set in CodeMode.",
|
|
argNode,
|
|
)
|
|
args.push(...items)
|
|
} 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.invokeTool, this.invokeSearch, this.toolKeys, this.promises, this.logs)
|
|
invocation.scopes = new ScopeStack([...fn.capturedScopes, new Map()])
|
|
const run = Effect.gen(function* () {
|
|
// Seed all parameters first so defaults cannot fall through to same-named outer bindings.
|
|
const paramScope = invocation.scopes.current()
|
|
for (const parameter of fn.parameters) {
|
|
for (const name of collectPatternNames(parameter)) {
|
|
paramScope.set(name, { mutable: true, value: undefined, initialized: false })
|
|
}
|
|
}
|
|
for (const [index, parameter] of fn.parameters.entries()) {
|
|
if (parameter.type === "RestElement") {
|
|
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true)
|
|
break
|
|
}
|
|
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
|
|
}
|
|
|
|
if (fn.body.type === "BlockStatement") {
|
|
const result = yield* invocation.evaluateStatement(fn.body)
|
|
return result.kind === "return" ? result.value : undefined
|
|
}
|
|
|
|
return yield* invocation.evaluateExpression(fn.body)
|
|
})
|
|
if (!fn.async) return run
|
|
// The initial yield assigns `box.own` before the body can self-resolve.
|
|
const box: { own?: CodeModePromise } = {}
|
|
return Effect.map(
|
|
this.createPromise(
|
|
Effect.flatMap(run, (value) => {
|
|
if (!(value instanceof CodeModePromise)) return Effect.succeed(value)
|
|
if (value === box.own) return Effect.fail(selfResolutionError())
|
|
return invocation.settlePromise(value)
|
|
}),
|
|
),
|
|
(promise) => {
|
|
box.own = promise
|
|
return promise
|
|
},
|
|
)
|
|
}
|
|
|
|
private evaluateObjectExpression(node: AstNode): Effect.Effect<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 in CodeMode.",
|
|
property,
|
|
"InvalidDataValue",
|
|
)
|
|
}
|
|
for (const [key, value] of Object.entries(spread)) {
|
|
if (isBlockedMember(key))
|
|
throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, property)
|
|
objectValue[key] = value
|
|
}
|
|
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 in CodeMode.`, keyNode)
|
|
}
|
|
objectValue[String(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 items = spreadItems(spread)
|
|
if (items === undefined)
|
|
throw new InterpreterRuntimeError(
|
|
"Array spread requires an array, string, Map, or Set in CodeMode.",
|
|
element,
|
|
)
|
|
values.push(...items)
|
|
} 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
|
|
| 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 in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`,
|
|
propertyNode,
|
|
)
|
|
}
|
|
|
|
if (objectValue instanceof GlobalNamespace) {
|
|
if (typeof key === "string" && isBlockedMember(key)) {
|
|
throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available in CodeMode.`, 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 (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 = 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 in CodeMode.`, 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 (isRuntimeReference(objectValue)) {
|
|
throw new InterpreterRuntimeError(
|
|
"CodeMode 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 in CodeMode.`, propertyNode)
|
|
}
|
|
|
|
if (Array.isArray(objectValue)) {
|
|
if (operation === "delete") return { target: objectValue, key }
|
|
const index = 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 ||
|
|
reference instanceof ToolReference ||
|
|
reference instanceof PromiseMethodReference ||
|
|
reference instanceof PromiseInstanceMethodReference ||
|
|
reference instanceof IntrinsicReference ||
|
|
reference instanceof GlobalMethodReference
|
|
)
|
|
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 reference.target[reference.key]
|
|
}
|
|
if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex
|
|
if (reference.target instanceof CodeModeURL) {
|
|
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
|
|
}
|
|
return reference.target[String(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 in CodeMode.", argument)
|
|
}
|
|
return Effect.map(this.getMemberReference(target, "delete"), (reference) => {
|
|
if (reference === OptionalShortCircuit) return true
|
|
if (
|
|
reference instanceof ComputedValue ||
|
|
reference === undefined ||
|
|
reference instanceof ToolReference ||
|
|
reference instanceof PromiseMethodReference ||
|
|
reference instanceof PromiseInstanceMethodReference ||
|
|
reference instanceof IntrinsicReference ||
|
|
reference instanceof GlobalMethodReference ||
|
|
reference.target instanceof CodeModeURL
|
|
) {
|
|
throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", 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 ||
|
|
reference instanceof ToolReference ||
|
|
reference instanceof PromiseMethodReference ||
|
|
reference instanceof PromiseInstanceMethodReference ||
|
|
reference instanceof IntrinsicReference ||
|
|
reference instanceof GlobalMethodReference
|
|
) {
|
|
throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node)
|
|
}
|
|
if (Array.isArray(reference.target)) {
|
|
if (reference.key === "length")
|
|
throw new InterpreterRuntimeError("Array length cannot be assigned in CodeMode.", node)
|
|
if (typeof reference.key === "string" && arrayMethods.has(reference.key)) {
|
|
throw new InterpreterRuntimeError("Array methods cannot be assigned in CodeMode.", node)
|
|
}
|
|
}
|
|
const key = Array.isArray(reference.target) ? reference.key : String(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: number | string): unknown {
|
|
if (reference.target instanceof CodeModeURL) {
|
|
return (reference.target.url as unknown as Record<string, unknown>)[key]
|
|
}
|
|
if (reference.target instanceof CodeModeRegExp) return reference.target.lastIndex
|
|
return (reference.target as Record<PropertyKey, unknown>)[key]
|
|
}
|
|
|
|
private assignToReference(reference: MemberReference, key: number | string, 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
|
|
const objectKey = key as string
|
|
rejectCircularInsertion(target, next, "Object assignment result", node)
|
|
target[objectKey] = next
|
|
}
|
|
|
|
private toPropertyKey(value: unknown, node: AstNode): string | number {
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
return value
|
|
}
|
|
|
|
throw new InterpreterRuntimeError("Property key must be a string or number.", node)
|
|
}
|
|
}
|