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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue