feat(codemode): unify callback acceptance and support built-in references (#36771)
This commit is contained in:
parent
0d4f9797f4
commit
ea89a2f619
11 changed files with 530 additions and 124 deletions
|
|
@ -3,14 +3,16 @@ import {
|
|||
type AstNode,
|
||||
CodeModeFunction,
|
||||
CoercionFunction,
|
||||
ErrorConstructorReference,
|
||||
GlobalMethodReference,
|
||||
GlobalNamespace,
|
||||
IntrinsicReference,
|
||||
InterpreterRuntimeError,
|
||||
PromiseCapabilityFunction,
|
||||
supportedSyntaxMessage,
|
||||
PromiseNamespace,
|
||||
UriFunction,
|
||||
} from "./model.js"
|
||||
import { rejectCircularInsertion } from "./references.js"
|
||||
import { rejectCircularInsertion, typeofValue } from "./references.js"
|
||||
import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
|
||||
import {
|
||||
CodeModeDate,
|
||||
|
|
@ -28,14 +30,47 @@ import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js"
|
|||
import { invokeObjectMethod } from "../stdlib/object.js"
|
||||
import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js"
|
||||
import { invokeStringStatic } from "../stdlib/string.js"
|
||||
import { invokeUriFunction, invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
|
||||
import { boundedData, coerceToNumber, coerceToString, invokeCoercion } from "../stdlib/value.js"
|
||||
import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
|
||||
import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js"
|
||||
|
||||
export type CallbackRunner<R> = {
|
||||
readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
readonly invokeCallable: (
|
||||
callable: unknown,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
) => Effect.Effect<unknown, unknown, R>
|
||||
readonly settlePromise: (promise: CodeModePromise) => Effect.Effect<unknown, unknown, never>
|
||||
}
|
||||
|
||||
// The single acceptance list for callbacks: collections, sort, string replacers,
|
||||
// Array.from mappers, and promise reactions all admit exactly these callables.
|
||||
// Admission means dispatchable, not necessarily invocable: new-requiring
|
||||
// constructors pass the gate and throw a TypeError on call, like JS.
|
||||
export type SupportedCallback =
|
||||
| CodeModeFunction
|
||||
| CoercionFunction
|
||||
| UriFunction
|
||||
| PromiseCapabilityFunction
|
||||
| GlobalMethodReference
|
||||
| IntrinsicReference
|
||||
| ErrorConstructorReference
|
||||
| GlobalNamespace
|
||||
| PromiseNamespace
|
||||
|
||||
export const isSupportedCallback = (value: unknown): value is SupportedCallback =>
|
||||
value instanceof CodeModeFunction ||
|
||||
value instanceof CoercionFunction ||
|
||||
value instanceof UriFunction ||
|
||||
value instanceof PromiseCapabilityFunction ||
|
||||
value instanceof GlobalMethodReference ||
|
||||
value instanceof IntrinsicReference ||
|
||||
value instanceof ErrorConstructorReference ||
|
||||
// Callable namespaces dispatch like JS: Array/Object/Date/RegExp construct,
|
||||
// new-requiring constructors throw a TypeError. Math/JSON/console stay non-callable.
|
||||
(value instanceof GlobalNamespace && typeofValue(value) === "function") ||
|
||||
value instanceof PromiseNamespace
|
||||
|
||||
export const invokeIntrinsic = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
ref: IntrinsicReference,
|
||||
|
|
@ -43,11 +78,14 @@ export const invokeIntrinsic = <R>(
|
|||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
if (typeof ref.receiver === "string") {
|
||||
if (
|
||||
(ref.name === "replace" || ref.name === "replaceAll") &&
|
||||
(args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction)
|
||||
) {
|
||||
return invokeStringReplacer(runner, ref.receiver, ref.name, args, node)
|
||||
if (ref.name === "replace" || ref.name === "replaceAll") {
|
||||
if (isSupportedCallback(args[1])) return invokeStringReplacer(runner, ref.receiver, ref.name, args, node)
|
||||
if (typeofValue(args[1]) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
`String.${ref.name} cannot use this callable as a replacer; wrap it in an arrow function, e.g. (match) => tools.ns.tool(match).`,
|
||||
node,
|
||||
)
|
||||
}
|
||||
}
|
||||
return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node))
|
||||
}
|
||||
|
|
@ -269,49 +307,60 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
|
|||
return Array.isArray(args[0])
|
||||
case "of":
|
||||
return [...args]
|
||||
case "from": {
|
||||
if (args.length > 1) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.",
|
||||
node,
|
||||
"UnsupportedSyntax",
|
||||
[supportedSyntaxMessage],
|
||||
)
|
||||
}
|
||||
if (args[0] instanceof CodeModeMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item])
|
||||
if (args[0] instanceof CodeModeSet) return Array.from(args[0].set.values())
|
||||
if (args[0] instanceof CodeModeURLSearchParams) {
|
||||
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
|
||||
}
|
||||
const source = args[0]
|
||||
if (source instanceof CodeModePromise) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from received an un-awaited Promise; await it before creating the array.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (typeof source === "string") return Array.from(source)
|
||||
if (Array.isArray(source)) return [...source]
|
||||
if (
|
||||
source !== null &&
|
||||
typeof source === "object" &&
|
||||
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||
typeof (source as { length?: unknown }).length === "number"
|
||||
) {
|
||||
return Array.from(source as ArrayLike<unknown>)
|
||||
}
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
case "from":
|
||||
return arrayFromItems(args[0], node)
|
||||
default:
|
||||
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
|
||||
}
|
||||
}
|
||||
|
||||
const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
|
||||
if (source instanceof CodeModeMap) return Array.from(source.map.entries(), ([key, item]) => [key, item])
|
||||
if (source instanceof CodeModeSet) return Array.from(source.set.values())
|
||||
if (source instanceof CodeModeURLSearchParams) {
|
||||
return Array.from(source.params.entries(), ([key, value]) => [key, value])
|
||||
}
|
||||
if (source instanceof CodeModePromise) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from received an un-awaited Promise; await it before creating the array.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (typeof source === "string") return Array.from(source)
|
||||
if (Array.isArray(source)) return [...source]
|
||||
if (
|
||||
source !== null &&
|
||||
typeof source === "object" &&
|
||||
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
|
||||
typeof (source as { length?: unknown }).length === "number"
|
||||
) {
|
||||
return Array.from(source as ArrayLike<unknown>)
|
||||
}
|
||||
throw new InterpreterRuntimeError(
|
||||
"Array.from expects an array, string, Map, Set, or array-like value.",
|
||||
node,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
|
||||
export const invokeArrayFrom = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const items = arrayFromItems(args[0], node)
|
||||
if (args.length < 2 || args[1] === undefined) return Effect.succeed(items)
|
||||
const apply = applyCollectionCallback(runner, args[1], "Array.from", node)
|
||||
return Effect.gen(function* () {
|
||||
const values: Array<unknown> = []
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
values.push(yield* apply([items[index], index]))
|
||||
}
|
||||
return values
|
||||
})
|
||||
}
|
||||
|
||||
const invokeStringReplacer = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
value: string,
|
||||
|
|
@ -367,9 +416,12 @@ const invokeStringReplacer = <R>(
|
|||
args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof CodeModePromise
|
||||
? yield* runner.settlePromise(replacement)
|
||||
: replacement
|
||||
// Error values are branded plain objects; boundedData would strip the brand before coercion.
|
||||
output.push(
|
||||
value.slice(end, match.offset),
|
||||
coerceToString(boundedData(resolved, `String.${name} replacer result`)),
|
||||
errorBrandName(resolved)
|
||||
? coerceToString(resolved)
|
||||
: coerceToString(boundedData(resolved, `String.${name} replacer result`)),
|
||||
)
|
||||
end = match.offset + match.match.length
|
||||
}
|
||||
|
|
@ -384,22 +436,16 @@ export const applyCollectionCallback = <R>(
|
|||
name: string,
|
||||
node: AstNode,
|
||||
): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
|
||||
if (
|
||||
!(callback instanceof CodeModeFunction) &&
|
||||
!(callback instanceof CoercionFunction) &&
|
||||
!(callback instanceof UriFunction) &&
|
||||
!(callback instanceof PromiseCapabilityFunction)
|
||||
) {
|
||||
if (!isSupportedCallback(callback)) {
|
||||
if (typeofValue(callback) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
`${name} cannot use this callable as a callback; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`,
|
||||
node,
|
||||
)
|
||||
}
|
||||
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
|
||||
}
|
||||
return (callbackArgs) =>
|
||||
callback instanceof CoercionFunction
|
||||
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
|
||||
: callback instanceof UriFunction
|
||||
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
|
||||
: callback instanceof PromiseCapabilityFunction
|
||||
? Effect.sync(() => callback.settle(callbackArgs[0]))
|
||||
: runner.invokeFunction(callback, callbackArgs)
|
||||
return (callbackArgs) => runner.invokeCallable(callback, callbackArgs, node)
|
||||
}
|
||||
|
||||
const invokeMapMethod = <R>(
|
||||
|
|
@ -603,12 +649,12 @@ const invokeArrayMethod = <R>(
|
|||
case "reverse":
|
||||
return Effect.succeed(target.reverse())
|
||||
case "sort":
|
||||
return Effect.map(sortArray(runner, target, args[0], node), (sorted) => {
|
||||
return Effect.map(sortArray(runner, target, args[0], "Array.sort", node), (sorted) => {
|
||||
target.splice(0, target.length, ...sorted)
|
||||
return target
|
||||
})
|
||||
case "toSorted":
|
||||
return sortArray(runner, target, args[0], node)
|
||||
return sortArray(runner, target, args[0], "Array.toSorted", node)
|
||||
case "toReversed":
|
||||
return Effect.succeed([...target].reverse())
|
||||
case "with": {
|
||||
|
|
@ -782,12 +828,10 @@ const sortArray = <R>(
|
|||
runner: CallbackRunner<R>,
|
||||
target: Array<unknown>,
|
||||
comparator: unknown,
|
||||
name: string,
|
||||
node: AstNode,
|
||||
): Effect.Effect<Array<unknown>, unknown, R> => {
|
||||
if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) {
|
||||
throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node)
|
||||
}
|
||||
if (!(comparator instanceof CodeModeFunction)) {
|
||||
if (comparator === undefined) {
|
||||
return Effect.sync(() =>
|
||||
[...target].sort((a, b) => {
|
||||
const left = coerceToString(a)
|
||||
|
|
@ -796,6 +840,7 @@ const sortArray = <R>(
|
|||
}),
|
||||
)
|
||||
}
|
||||
const apply = applyCollectionCallback(runner, comparator, name, node)
|
||||
const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
|
||||
if (items.length <= 1) return Effect.succeed(items)
|
||||
const midpoint = Math.floor(items.length / 2)
|
||||
|
|
@ -807,7 +852,7 @@ const sortArray = <R>(
|
|||
let rightIndex = 0
|
||||
while (leftIndex < left.length && rightIndex < right.length) {
|
||||
// Treat a NaN comparator result as equal to preserve stable ordering.
|
||||
const order = coerceToNumber(yield* runner.invokeFunction(comparator, [left[leftIndex], right[rightIndex]]))
|
||||
const order = coerceToNumber(yield* apply([left[leftIndex], right[rightIndex]]))
|
||||
if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++])
|
||||
else merged.push(right[rightIndex++])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,14 @@ import type { SafeObject } from "../tool-runtime.js"
|
|||
import {
|
||||
type AstNode,
|
||||
CodeModeFunction,
|
||||
CoercionFunction,
|
||||
InterpreterRuntimeError,
|
||||
ProgramThrow,
|
||||
PromiseCapabilityFunction,
|
||||
PromiseInstanceMethodReference,
|
||||
PromiseMethodReference,
|
||||
UriFunction,
|
||||
} from "./model.js"
|
||||
import { caughtErrorValue, normalizeError } from "./errors.js"
|
||||
import { applyCollectionCallback, type CallbackRunner } from "./methods.js"
|
||||
import { applyCollectionCallback, isSupportedCallback, type CallbackRunner, type SupportedCallback } from "./methods.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
import { spreadItems } from "../stdlib/collections.js"
|
||||
import { createAggregateErrorValue } from "../stdlib/value.js"
|
||||
|
|
@ -258,20 +256,11 @@ class PromiseAnyFulfilled {
|
|||
constructor(readonly value: unknown) {}
|
||||
}
|
||||
|
||||
type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction | PromiseCapabilityFunction
|
||||
|
||||
const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
|
||||
if (
|
||||
value instanceof CodeModeFunction ||
|
||||
value instanceof CoercionFunction ||
|
||||
value instanceof UriFunction ||
|
||||
value instanceof PromiseCapabilityFunction
|
||||
) {
|
||||
return value
|
||||
}
|
||||
const reactionHandler = (value: unknown, method: string, node: AstNode): SupportedCallback | undefined => {
|
||||
if (isSupportedCallback(value)) return value
|
||||
if (typeofValue(value) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
`${method} handlers must be plain functions; wrap other callables in an arrow function, e.g. (value) => tools.ns.tool(value).`,
|
||||
`${method} cannot use this callable as a handler; wrap it in an arrow function, e.g. (value) => tools.ns.tool(value).`,
|
||||
node,
|
||||
)
|
||||
}
|
||||
|
|
@ -294,8 +283,8 @@ const chainReaction = <R>(
|
|||
runner: CallbackRunner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
source: CodeModePromise,
|
||||
onFulfilled: ReactionHandler | undefined,
|
||||
onRejected: ReactionHandler | undefined,
|
||||
onFulfilled: SupportedCallback | undefined,
|
||||
onRejected: SupportedCallback | undefined,
|
||||
method: string,
|
||||
node: AstNode,
|
||||
): Effect.Effect<CodeModePromise, never, R> => {
|
||||
|
|
@ -320,7 +309,7 @@ const chainFinally = <R>(
|
|||
runner: CallbackRunner<R>,
|
||||
promises: PromiseRuntime<R>,
|
||||
source: CodeModePromise,
|
||||
cleanup: ReactionHandler | undefined,
|
||||
cleanup: SupportedCallback | undefined,
|
||||
method: string,
|
||||
node: AstNode,
|
||||
): Effect.Effect<CodeModePromise, never, R> =>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import {
|
|||
UriFunction,
|
||||
} from "./model.js"
|
||||
import { caughtErrorValue, constructErrorValue } from "./errors.js"
|
||||
import { type CallbackRunner, invokeGlobalMethod, invokeIntrinsic } from "./methods.js"
|
||||
import { type CallbackRunner, invokeArrayFrom, invokeGlobalMethod, invokeIntrinsic } from "./methods.js"
|
||||
import {
|
||||
constructPromise,
|
||||
invokePromiseInstanceMethod,
|
||||
|
|
@ -153,6 +153,7 @@ export class Interpreter<R> {
|
|||
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),
|
||||
}
|
||||
|
||||
|
|
@ -997,6 +998,13 @@ export class Interpreter<R> {
|
|||
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)
|
||||
|
|
@ -1019,6 +1027,27 @@ export class Interpreter<R> {
|
|||
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>): CodeModeDate {
|
||||
if (args.length === 0) return new CodeModeDate(Date.now())
|
||||
if (args.length === 1) {
|
||||
|
|
@ -1041,7 +1070,7 @@ export class Interpreter<R> {
|
|||
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 {
|
||||
|
|
@ -1401,7 +1430,19 @@ export class Interpreter<R> {
|
|||
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)
|
||||
|
|
@ -1426,7 +1467,10 @@ export class Interpreter<R> {
|
|||
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
|
||||
return invokeGlobalMethod(callable, args, node)
|
||||
}
|
||||
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
|
||||
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`)
|
||||
|
|
@ -1443,6 +1487,22 @@ export class Interpreter<R> {
|
|||
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
|
||||
|
|
@ -1604,7 +1664,8 @@ export class Interpreter<R> {
|
|||
return Effect.gen(function* () {
|
||||
for (const elementValue of elements) {
|
||||
if (elementValue === null) {
|
||||
values.push(undefined)
|
||||
// A literal elision is a real hole, like JS: extend length without an own index.
|
||||
values.length += 1
|
||||
continue
|
||||
}
|
||||
const element = asNode(elementValue, "elements")
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import {
|
||||
type AstNode,
|
||||
CodeModeFunction,
|
||||
InterpreterRuntimeError,
|
||||
supportedSyntaxMessage,
|
||||
} from "../interpreter/model.js"
|
||||
import { type AstNode, InterpreterRuntimeError, supportedSyntaxMessage } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { copyIn, copyOut } from "../tool-runtime.js"
|
||||
|
||||
export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
||||
switch (name) {
|
||||
case "stringify": {
|
||||
const replacer = args[1]
|
||||
if (Array.isArray(replacer) || replacer instanceof CodeModeFunction) {
|
||||
if (Array.isArray(replacer) || typeofValue(replacer) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
"JSON.stringify replacers are not supported in CodeMode.",
|
||||
node,
|
||||
|
|
@ -25,6 +21,14 @@ export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
case "parse": {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw new InterpreterRuntimeError("JSON.parse expects a string.", node)
|
||||
if (typeofValue(args[1]) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
"JSON.parse revivers are not supported in CodeMode.",
|
||||
node,
|
||||
"UnsupportedSyntax",
|
||||
[supportedSyntaxMessage],
|
||||
)
|
||||
}
|
||||
try {
|
||||
return copyIn(JSON.parse(text), "JSON.parse result")
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -42,16 +42,26 @@ export const mathMethods = new Set([
|
|||
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
||||
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
||||
if (name === "random") return Math.random()
|
||||
const nums = args.map((arg) => {
|
||||
// Validate only the arguments the method consumes; like JS, extras are ignored
|
||||
// (so built-ins work as callbacks receiving (element, index, array)).
|
||||
const num = (index: number): number => {
|
||||
if (index >= args.length) return Number.NaN
|
||||
const arg = args[index]
|
||||
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
||||
return arg
|
||||
})
|
||||
const [a = Number.NaN, b = Number.NaN] = nums
|
||||
}
|
||||
const nums = () =>
|
||||
args.map((arg) => {
|
||||
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
|
||||
return arg
|
||||
})
|
||||
const a = num(0)
|
||||
const b = () => num(1)
|
||||
switch (name) {
|
||||
case "max":
|
||||
return Math.max(...nums)
|
||||
return Math.max(...nums())
|
||||
case "min":
|
||||
return Math.min(...nums)
|
||||
return Math.min(...nums())
|
||||
case "abs":
|
||||
return Math.abs(a)
|
||||
case "acos":
|
||||
|
|
@ -65,7 +75,7 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
case "atan":
|
||||
return Math.atan(a)
|
||||
case "atan2":
|
||||
return Math.atan2(a, b)
|
||||
return Math.atan2(a, b())
|
||||
case "atanh":
|
||||
return Math.atanh(a)
|
||||
case "floor":
|
||||
|
|
@ -83,9 +93,9 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
case "cbrt":
|
||||
return Math.cbrt(a)
|
||||
case "pow":
|
||||
return Math.pow(a, b)
|
||||
return Math.pow(a, b())
|
||||
case "hypot":
|
||||
return Math.hypot(...nums)
|
||||
return Math.hypot(...nums())
|
||||
case "cos":
|
||||
return Math.cos(a)
|
||||
case "cosh":
|
||||
|
|
@ -117,7 +127,7 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
case "clz32":
|
||||
return Math.clz32(a)
|
||||
case "imul":
|
||||
return Math.imul(a, b)
|
||||
return Math.imul(a, b())
|
||||
}
|
||||
throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ export const coerceToString = (value: unknown): string => {
|
|||
if (value instanceof CodeModeSet) return "[object Set]"
|
||||
if (value instanceof CodeModeURL) return value.url.href
|
||||
if (value instanceof CodeModeURLSearchParams) return value.params.toString()
|
||||
if (errorBrandName(value) !== undefined) {
|
||||
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
|
||||
const error = value as { name?: unknown; message?: unknown }
|
||||
const name = typeof error.name === "string" ? error.name : "Error"
|
||||
const message = typeof error.message === "string" ? error.message : ""
|
||||
if (message === "") return name
|
||||
if (name === "") return message
|
||||
return `${name}: ${message}`
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
||||
|
|
@ -57,6 +66,8 @@ export const coerceToNumber = (value: unknown): number => {
|
|||
|
||||
export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node: AstNode): unknown => {
|
||||
const raw = args[0]
|
||||
// Error values are plain SafeObjects; the boundedData path below would strip their brand.
|
||||
if (ref.name === "String" && errorBrandName(raw) !== undefined) return coerceToString(raw)
|
||||
if (isCodeModeValue(raw)) {
|
||||
if (ref.name === "Boolean") return true
|
||||
if (ref.name === "Number") return coerceToNumber(raw)
|
||||
|
|
|
|||
|
|
@ -270,7 +270,8 @@ export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
|
|||
return null
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => copyOut(item, undefinedAsNull))
|
||||
// Array.from densifies holes so sparse arrays normalize at the boundary like JSON does.
|
||||
return Array.from(value, (item) => copyOut(item, undefinedAsNull))
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === "object" && !(value instanceof ToolReference)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue