opencode/packages/codemode/src/stdlib/value.ts

114 lines
4.9 KiB
TypeScript

export const errorConstructors = new Set([
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
])
export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
const ErrorBrand: unique symbol = Symbol("codemode.error")
export const createErrorValue = (name: string, message: string): SafeObject => {
const value = Object.assign(Object.create(null) as SafeObject, { name, message })
Object.defineProperty(value, ErrorBrand, { value: name })
return value
}
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): SafeObject =>
Object.assign(createErrorValue("AggregateError", message), { errors })
export const errorBrandName = (value: unknown): string | undefined =>
value !== null && typeof value === "object"
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
: undefined
export const boundedData = (value: unknown, label: string): unknown => copyIn(value, label, true)
export const coerceToString = (value: unknown): string => {
if (value === null) return "null"
if (value === undefined) return "undefined"
if (value instanceof CodeModeDate)
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
if (value instanceof CodeModeRegExp) return `/${value.regex.source}/${value.regex.flags}`
if (value instanceof CodeModeMap) return "[object Map]"
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(",")
: "[object Object]"
}
return String(value)
}
export const coerceToNumber = (value: unknown): number => {
if (value instanceof CodeModeDate) return value.time
if (isCodeModeValue(value)) return Number.NaN
// Arrays coerce through our own string coercion: host Number(array) joins with host
// ToPrimitive, which throws on the null-prototype objects the interpreter produces.
if (Array.isArray(value)) return Number(coerceToString(value))
return value !== null && typeof value === "object" ? Number.NaN : Number(value)
}
export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node: AstNode): unknown => {
// Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the
// other coercers match native through the undefined-argument path below.
if (args.length === 0) {
if (ref.name === "Number") return 0
if (ref.name === "String") return ""
}
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)
if (ref.name === "String") return coerceToString(raw)
if (ref.name === "isFinite") return Number.isFinite(coerceToNumber(raw))
if (ref.name === "isNaN") return Number.isNaN(coerceToNumber(raw))
if (ref.name === "parseInt") return parseInt(coerceToString(raw))
return parseFloat(coerceToString(raw))
}
const value = boundedData(raw, `${ref.name} input`)
if (ref.name === "Number") return coerceToNumber(value)
if (ref.name === "Boolean") return Boolean(value)
if (ref.name === "isFinite") return Number.isFinite(coerceToNumber(value))
if (ref.name === "isNaN") return Number.isNaN(coerceToNumber(value))
if (ref.name === "parseInt") {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node)
}
return parseInt(coerceToString(value), radix)
}
if (ref.name === "parseFloat") return parseFloat(coerceToString(value))
return coerceToString(value)
}
import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js"
import { copyIn, type SafeObject } from "../tool-runtime.js"
import {
isCodeModeValue,
CodeModeDate,
CodeModeMap,
CodeModeRegExp,
CodeModeSet,
CodeModeURL,
CodeModeURLSearchParams,
} from "../values.js"