feat(codemode): unify callback acceptance and support built-in references (#36771)

This commit is contained in:
Aiden Cline 2026-07-14 15:25:39 -05:00 committed by GitHub
commit ea89a2f619
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 530 additions and 124 deletions

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)