feat(codemode): support JSON callbacks (#38006)
This commit is contained in:
parent
39fdd67123
commit
065b108bba
9 changed files with 431 additions and 61 deletions
|
|
@ -8,6 +8,7 @@ import {
|
|||
GlobalNamespace,
|
||||
IntrinsicReference,
|
||||
InterpreterRuntimeError,
|
||||
JsonMethodReference,
|
||||
PromiseCapabilityFunction,
|
||||
PromiseNamespace,
|
||||
UriFunction,
|
||||
|
|
@ -25,7 +26,6 @@ import {
|
|||
isCodeModeValue,
|
||||
} from "../values.js"
|
||||
import { dateSetterArgumentCount, invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
|
||||
import { invokeJsonMethod } from "../stdlib/json.js"
|
||||
import { invokeMathMethod } from "../stdlib/math.js"
|
||||
import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js"
|
||||
import { invokeObjectMethod } from "../stdlib/object.js"
|
||||
|
|
@ -54,6 +54,7 @@ export type SupportedCallback =
|
|||
| UriFunction
|
||||
| PromiseCapabilityFunction
|
||||
| GlobalMethodReference
|
||||
| JsonMethodReference
|
||||
| IntrinsicReference
|
||||
| ErrorConstructorReference
|
||||
| GlobalNamespace
|
||||
|
|
@ -65,6 +66,7 @@ export const isSupportedCallback = (value: unknown): value is SupportedCallback
|
|||
value instanceof UriFunction ||
|
||||
value instanceof PromiseCapabilityFunction ||
|
||||
value instanceof GlobalMethodReference ||
|
||||
value instanceof JsonMethodReference ||
|
||||
value instanceof IntrinsicReference ||
|
||||
value instanceof ErrorConstructorReference ||
|
||||
// Callable namespaces dispatch like JS: Array/Object/Date/RegExp construct,
|
||||
|
|
@ -168,7 +170,7 @@ export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unkno
|
|||
if (ref.namespace === "Map" || ref.namespace === "Set" || ref.namespace === "URLSearchParams") {
|
||||
throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
|
||||
}
|
||||
return invokeJsonMethod(ref.name, args, node)
|
||||
throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
|
||||
}
|
||||
|
||||
const requireDataArgument = (name: string, index: number, arg: unknown, node: AstNode): unknown => {
|
||||
|
|
|
|||
|
|
@ -99,11 +99,15 @@ export class GlobalNamespace {
|
|||
|
||||
export class GlobalMethodReference {
|
||||
constructor(
|
||||
readonly namespace: GlobalNamespaceName | "Number" | "String",
|
||||
readonly namespace: Exclude<GlobalNamespaceName, "JSON"> | "Number" | "String",
|
||||
readonly name: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class JsonMethodReference {
|
||||
constructor(readonly name: "parse" | "stringify") {}
|
||||
}
|
||||
|
||||
export class CoercionFunction {
|
||||
constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN") {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
GlobalNamespace,
|
||||
InterpreterRuntimeError,
|
||||
IntrinsicReference,
|
||||
JsonMethodReference,
|
||||
PromiseCapabilityFunction,
|
||||
PromiseInstanceMethodReference,
|
||||
PromiseMethodReference,
|
||||
|
|
@ -23,6 +24,7 @@ export const isRuntimeReference = (value: unknown): boolean =>
|
|||
value instanceof IntrinsicReference ||
|
||||
value instanceof GlobalNamespace ||
|
||||
value instanceof GlobalMethodReference ||
|
||||
value instanceof JsonMethodReference ||
|
||||
value instanceof PromiseNamespace ||
|
||||
value instanceof PromiseMethodReference ||
|
||||
value instanceof PromiseInstanceMethodReference ||
|
||||
|
|
@ -82,12 +84,7 @@ export const containsOpaqueReference = (value: unknown): boolean => {
|
|||
}
|
||||
|
||||
// Reject cycles before mutation so later boundary walks remain safe.
|
||||
export const rejectCircularInsertion = (
|
||||
container: object,
|
||||
value: unknown,
|
||||
label: string,
|
||||
node: AstNode,
|
||||
): void => {
|
||||
export const rejectCircularInsertion = (container: object, value: unknown, label: string, node: AstNode): void => {
|
||||
const pending: Array<Iterator<unknown>> = [[value].values()]
|
||||
const seen = new Set<object>()
|
||||
while (pending.length > 0) {
|
||||
|
|
@ -111,6 +108,7 @@ export const typeofValue = (value: unknown): string => {
|
|||
value instanceof CoercionFunction ||
|
||||
value instanceof IntrinsicReference ||
|
||||
value instanceof GlobalMethodReference ||
|
||||
value instanceof JsonMethodReference ||
|
||||
value instanceof PromiseMethodReference ||
|
||||
value instanceof PromiseInstanceMethodReference ||
|
||||
value instanceof PromiseNamespace ||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
IntrinsicReference,
|
||||
InterpreterRuntimeError,
|
||||
isRecord,
|
||||
JsonMethodReference,
|
||||
type MemberReference,
|
||||
OptionalShortCircuit,
|
||||
PromiseCapabilityFunction,
|
||||
|
|
@ -55,7 +56,7 @@ import { ScopeStack } from "./scope.js"
|
|||
import { arrayMethods, mapMethods, mapStatics, 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 { invokeJsonMethod, jsonStatics, type JsonMethodName } 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"
|
||||
|
|
@ -102,7 +103,6 @@ import {
|
|||
const globalStaticMembers: Partial<Record<GlobalNamespaceName, Set<string>>> = {
|
||||
Object: objectStatics,
|
||||
Math: mathMethods,
|
||||
JSON: jsonStatics,
|
||||
Array: arrayStatics,
|
||||
console: consoleMethods,
|
||||
Date: dateStatics,
|
||||
|
|
@ -1624,6 +1624,9 @@ export class Interpreter<R> {
|
|||
}
|
||||
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
|
||||
}
|
||||
if (callable instanceof JsonMethodReference) {
|
||||
return yield* invokeJsonMethod(self.runner, callable.name, args, node)
|
||||
}
|
||||
if (callable instanceof CoercionFunction) {
|
||||
return boundedData(invokeCoercion(callable, args, node), `${callable.name} result`)
|
||||
}
|
||||
|
|
@ -1889,6 +1892,7 @@ export class Interpreter<R> {
|
|||
| PromiseInstanceMethodReference
|
||||
| IntrinsicReference
|
||||
| GlobalMethodReference
|
||||
| JsonMethodReference
|
||||
| ComputedValue
|
||||
| typeof OptionalShortCircuit
|
||||
| undefined,
|
||||
|
|
@ -1936,6 +1940,10 @@ export class Interpreter<R> {
|
|||
if (objectValue.name === "Math" && mathConstants.has(key)) {
|
||||
return new ComputedValue((Math as unknown as Record<string, number>)[key])
|
||||
}
|
||||
if (objectValue.name === "JSON") {
|
||||
if (jsonStatics.has(key)) return new JsonMethodReference(key as JsonMethodName)
|
||||
return new ComputedValue(undefined)
|
||||
}
|
||||
if (globalStaticMembers[objectValue.name]?.has(key)) {
|
||||
return new GlobalMethodReference(objectValue.name, key)
|
||||
}
|
||||
|
|
@ -2065,7 +2073,8 @@ export class Interpreter<R> {
|
|||
reference instanceof PromiseMethodReference ||
|
||||
reference instanceof PromiseInstanceMethodReference ||
|
||||
reference instanceof IntrinsicReference ||
|
||||
reference instanceof GlobalMethodReference
|
||||
reference instanceof GlobalMethodReference ||
|
||||
reference instanceof JsonMethodReference
|
||||
)
|
||||
return reference
|
||||
if (Array.isArray(reference.target)) {
|
||||
|
|
@ -2100,6 +2109,7 @@ export class Interpreter<R> {
|
|||
reference instanceof PromiseInstanceMethodReference ||
|
||||
reference instanceof IntrinsicReference ||
|
||||
reference instanceof GlobalMethodReference ||
|
||||
reference instanceof JsonMethodReference ||
|
||||
reference.target instanceof CodeModeURL
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Only data fields may be deleted in CodeMode.", target, "InvalidDataValue")
|
||||
|
|
@ -2127,7 +2137,8 @@ export class Interpreter<R> {
|
|||
reference instanceof PromiseMethodReference ||
|
||||
reference instanceof PromiseInstanceMethodReference ||
|
||||
reference instanceof IntrinsicReference ||
|
||||
reference instanceof GlobalMethodReference
|
||||
reference instanceof GlobalMethodReference ||
|
||||
reference instanceof JsonMethodReference
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Only data fields may be assigned in CodeMode.", node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,149 @@
|
|||
import { type AstNode, InterpreterRuntimeError, supportedSyntaxMessage } from "../interpreter/model.js"
|
||||
import { Effect } from "effect"
|
||||
import type { CallbackRunner } from "../interpreter/methods.js"
|
||||
import { applyCollectionCallback } from "../interpreter/methods.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { typeofValue } from "../interpreter/references.js"
|
||||
import { copyIn, copyOut } from "../tool-runtime.js"
|
||||
import { copyIn, copyOut, type SafeObject } from "../tool-runtime.js"
|
||||
import {
|
||||
CodeModeDate,
|
||||
CodeModeMap,
|
||||
CodeModeRegExp,
|
||||
CodeModeSet,
|
||||
CodeModeURL,
|
||||
CodeModeURLSearchParams,
|
||||
} from "../values.js"
|
||||
|
||||
export const jsonStatics = new Set(["parse", "stringify"])
|
||||
export type JsonMethodName = "parse" | "stringify"
|
||||
|
||||
export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
|
||||
switch (name) {
|
||||
case "stringify": {
|
||||
const replacer = args[1]
|
||||
if (Array.isArray(replacer) || typeofValue(replacer) === "function") {
|
||||
throw new InterpreterRuntimeError(
|
||||
"JSON.stringify replacers are not supported in CodeMode.",
|
||||
node,
|
||||
"UnsupportedSyntax",
|
||||
[supportedSyntaxMessage],
|
||||
)
|
||||
}
|
||||
const space = args[2]
|
||||
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
|
||||
return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value"), "json"), null, indent)
|
||||
}
|
||||
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) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
node,
|
||||
).as("SyntaxError")
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node)
|
||||
export const invokeJsonMethod = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
name: JsonMethodName,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
return name === "parse" ? parse(runner, args, node) : stringify(runner, args, node)
|
||||
}
|
||||
|
||||
const parse = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const text = args[0]
|
||||
if (typeof text !== "string") throw new InterpreterRuntimeError("JSON.parse expects a string.", node)
|
||||
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return copyIn(JSON.parse(text), "JSON.parse result")
|
||||
} catch (error) {
|
||||
throw new InterpreterRuntimeError(
|
||||
`JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
node,
|
||||
).as("SyntaxError")
|
||||
}
|
||||
})()
|
||||
if (typeofValue(args[1]) !== "function") return Effect.succeed(parsed)
|
||||
|
||||
const apply = applyCollectionCallback(runner, args[1], "JSON.parse", node)
|
||||
const root: SafeObject = Object.create(null) as SafeObject
|
||||
root[""] = parsed
|
||||
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = holder[key as keyof typeof holder]
|
||||
if (Array.isArray(value)) {
|
||||
const length = value.length
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const revived = yield* visit(value, String(index))
|
||||
if (revived === undefined) Reflect.deleteProperty(value, index)
|
||||
else value[index] = revived
|
||||
}
|
||||
} else if (isPlainObject(value)) {
|
||||
for (const name of Object.keys(value)) {
|
||||
const revived = yield* visit(value, name)
|
||||
if (revived === undefined) Reflect.deleteProperty(value, name)
|
||||
else value[name] = revived
|
||||
}
|
||||
}
|
||||
return yield* apply([key, value])
|
||||
})
|
||||
return visit(root, "")
|
||||
}
|
||||
|
||||
const stringify = <R>(
|
||||
runner: CallbackRunner<R>,
|
||||
args: Array<unknown>,
|
||||
node: AstNode,
|
||||
): Effect.Effect<unknown, unknown, R> => {
|
||||
const space = args[2]
|
||||
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
|
||||
const replacer = args[1]
|
||||
const callable = typeofValue(replacer) === "function"
|
||||
const checked = copyIn(args[0], "JSON.stringify value", callable)
|
||||
const input = callable ? args[0] : checked
|
||||
|
||||
if (Array.isArray(replacer)) {
|
||||
const properties = replacer
|
||||
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
|
||||
.map(String)
|
||||
return Effect.succeed(JSON.stringify(copyOut(input, "json"), properties, indent))
|
||||
}
|
||||
if (!callable) {
|
||||
return Effect.succeed(JSON.stringify(copyOut(input, "json"), null, indent))
|
||||
}
|
||||
|
||||
const apply = applyCollectionCallback(runner, replacer, "JSON.stringify", node)
|
||||
const root: SafeObject = Object.create(null) as SafeObject
|
||||
root[""] = input
|
||||
const stack = new Set<object>()
|
||||
const visit = (holder: SafeObject | Array<unknown>, key: string): Effect.Effect<unknown, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const value = yield* apply([key, toJSONValue(holder[key as keyof typeof holder])])
|
||||
if (value === undefined || typeofValue(value) === "function") return undefined
|
||||
copyIn(value, "JSON.stringify replacer result", true)
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value : null
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
||||
if (Array.isArray(value)) {
|
||||
if (stack.has(value))
|
||||
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
|
||||
stack.add(value)
|
||||
const result: Array<unknown> = []
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
result.push((yield* visit(value, String(index))) ?? null)
|
||||
}
|
||||
stack.delete(value)
|
||||
return result
|
||||
}
|
||||
if (!isPlainObject(value)) return {}
|
||||
if (stack.has(value))
|
||||
throw new InterpreterRuntimeError("Converting circular structure to JSON.", node).as("TypeError")
|
||||
stack.add(value)
|
||||
const result: SafeObject = Object.create(null) as SafeObject
|
||||
for (const name of Object.keys(value)) {
|
||||
const item = yield* visit(value, name)
|
||||
if (item !== undefined) result[name] = item
|
||||
}
|
||||
stack.delete(value)
|
||||
return result
|
||||
})
|
||||
|
||||
return Effect.map(visit(root, ""), (value) => JSON.stringify(value, null, indent))
|
||||
}
|
||||
|
||||
const toJSONValue = (value: unknown): unknown => {
|
||||
if (value instanceof CodeModeDate) {
|
||||
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
|
||||
}
|
||||
if (value instanceof CodeModeURL) return value.url.href
|
||||
return value
|
||||
}
|
||||
|
||||
const isPlainObject = (value: unknown): value is SafeObject =>
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
!(value instanceof CodeModeDate) &&
|
||||
!(value instanceof CodeModeRegExp) &&
|
||||
!(value instanceof CodeModeMap) &&
|
||||
!(value instanceof CodeModeSet) &&
|
||||
!(value instanceof CodeModeURL) &&
|
||||
!(value instanceof CodeModeURLSearchParams)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue