mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
174 lines
6.9 KiB
TypeScript
174 lines
6.9 KiB
TypeScript
import { Effect } from "effect"
|
|
import { toProgram } from "../data.js"
|
|
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
|
|
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
|
|
import {
|
|
containsOpaqueReference,
|
|
describeValue,
|
|
rejectCircularInsertion,
|
|
typeofValue,
|
|
} from "../interpreter/references.js"
|
|
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
|
|
import { ToolReference } from "../tool-runtime.js"
|
|
import { Values } from "../values.js"
|
|
import { groupBy } from "./collections.js"
|
|
import { coerceToString } from "./value.js"
|
|
|
|
const requireObject = (name: string, input: unknown, node: AstNode): Record<string, unknown> => {
|
|
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
|
|
if (Values.isValue(input)) return {}
|
|
const prototype = input === null || typeof input !== "object" ? undefined : Object.getPrototypeOf(input)
|
|
if (prototype !== null && prototype !== Object.prototype) {
|
|
throw new InterpreterRuntimeError(
|
|
`Object.${name} expects a data object or array, received ${describeValue(input)}.`,
|
|
node,
|
|
"InvalidDataValue",
|
|
)
|
|
}
|
|
return input as Record<string, unknown>
|
|
}
|
|
|
|
export const objectAssign = (args: Array<unknown>, node: AstNode): unknown => {
|
|
const target = args[0]
|
|
if (target === null || typeof target !== "object" || Array.isArray(target) || Values.isValue(target)) {
|
|
throw new InterpreterRuntimeError("Object.assign expects a data object target.", node)
|
|
}
|
|
const out = target as Record<string, unknown>
|
|
const seen = new Set<object>()
|
|
const guardedSet = (key: PropertyKey, item: unknown): void => {
|
|
rejectCircularInsertion(out, item, "Object.assign result", node, seen)
|
|
if (!Reflect.set(out, key, item))
|
|
throw new InterpreterRuntimeError(`Object.assign could not assign property '${String(key)}'.`, node).as(
|
|
"TypeError",
|
|
)
|
|
}
|
|
for (const source of args.slice(1)) {
|
|
if (source === null || source === undefined || Values.isValue(source)) continue
|
|
if (typeof source !== "object" || Array.isArray(source)) {
|
|
throw new InterpreterRuntimeError("Object.assign expects data objects.", node)
|
|
}
|
|
for (const key of Reflect.ownKeys(source)) {
|
|
if (typeof key === "string") {
|
|
if (Object.prototype.propertyIsEnumerable.call(source, key)) guardedSet(key, Reflect.get(source, key))
|
|
continue
|
|
}
|
|
if (key !== AsyncIteratorSymbol && key !== IteratorSymbol) continue
|
|
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue
|
|
guardedSet(key, Reflect.get(source, key))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
const objectFromEntries = <R>(
|
|
runner: Runner<R>,
|
|
source: unknown,
|
|
node: AstNode,
|
|
): Effect.Effect<Record<string, unknown>, unknown, R> => {
|
|
const out: Record<string, unknown> = Object.create(null)
|
|
return Effect.gen(function* () {
|
|
const cursor = yield* runner.syncIterator(source, node)
|
|
if (cursor === undefined) {
|
|
throw new InterpreterRuntimeError("Object.fromEntries expects a synchronous iterable of entries.", node).as(
|
|
"TypeError",
|
|
)
|
|
}
|
|
while (true) {
|
|
const step = yield* cursor.next
|
|
if (step.done) return out
|
|
yield* preserveConsumerError(
|
|
cursor,
|
|
Effect.sync(() => {
|
|
if (
|
|
step.value === null ||
|
|
typeof step.value !== "object" ||
|
|
Values.isValue(step.value) ||
|
|
containsOpaqueReference(step.value)
|
|
) {
|
|
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as(
|
|
"TypeError",
|
|
)
|
|
}
|
|
const entry = step.value as Record<string, unknown>
|
|
toProgram(entry[0], "Object.fromEntries key")
|
|
toProgram(entry[1], "Object.fromEntries value")
|
|
const key = coerceToString(entry[0])
|
|
out[key] = entry[1]
|
|
}),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
const constructObject = (args: Array<unknown>, node: AstNode): unknown => {
|
|
const first = args[0]
|
|
if (first === null || first === undefined) return Object.create(null)
|
|
if (typeof first === "object") return first
|
|
throw new InterpreterRuntimeError(
|
|
`Object(${typeof first}) wrapper objects are not supported; use the primitive value directly.`,
|
|
node,
|
|
)
|
|
}
|
|
|
|
// Tool references are not data; only Object.keys(tools) reads them, for tool names.
|
|
const rejectTools = (name: string, args: Array<unknown>, node: AstNode): void => {
|
|
if (!(args[0] instanceof ToolReference)) return
|
|
throw new InterpreterRuntimeError(
|
|
`Object.${name}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or search({ query }) for signatures.`,
|
|
node,
|
|
"InvalidDataValue",
|
|
)
|
|
}
|
|
|
|
const objectStatic = (name: string, impl: (args: Array<unknown>, node: AstNode) => unknown) =>
|
|
sync(`Object.${name}`, (args, node) => {
|
|
rejectTools(name, args, node)
|
|
return impl(args, node)
|
|
})
|
|
|
|
// Object constructs identically with or without new, like JS. Only `keys` copies its result into the
|
|
// program; `values`, `entries`, `assign`, and `fromEntries` hand back the program's own values.
|
|
export const objectGlobal = <R>(runner: Runner<R>, toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>) =>
|
|
new HostFunction<R>({
|
|
name: "Object",
|
|
call: syncCall(constructObject),
|
|
construct: syncCall(constructObject),
|
|
instanceOf: (value) => value !== null && (typeof value === "object" || typeofValue(value) === "function"),
|
|
members: {
|
|
keys: sync("Object.keys", (args, node) =>
|
|
toProgram(
|
|
args[0] instanceof ToolReference
|
|
? [...toolKeys(args[0].path)]
|
|
: Object.keys(requireObject("keys", args[0], node)),
|
|
"Object.keys result",
|
|
),
|
|
),
|
|
values: objectStatic("values", (args, node) => Object.values(requireObject("values", args[0], node))),
|
|
entries: objectStatic("entries", (args, node) =>
|
|
Object.entries(requireObject("entries", args[0], node)).map(([key, item]) => [key, item]),
|
|
),
|
|
hasOwn: objectStatic("hasOwn", (args, node) =>
|
|
Object.hasOwn(
|
|
requireObject("hasOwn", args[0], node),
|
|
args[1] === AsyncIteratorSymbol || args[1] === IteratorSymbol ? args[1] : String(args[1]),
|
|
),
|
|
),
|
|
is: objectStatic("is", (args, node) => {
|
|
if (containsOpaqueReference(args[0]) || containsOpaqueReference(args[1])) {
|
|
throw new InterpreterRuntimeError("Object.is requires data values.", node, "InvalidDataValue")
|
|
}
|
|
return Object.is(args[0], args[1])
|
|
}),
|
|
assign: objectStatic("assign", objectAssign),
|
|
fromEntries: new HostFunction<R>({
|
|
name: "Object.fromEntries",
|
|
call: (args, node) =>
|
|
Effect.suspend(() => {
|
|
rejectTools("fromEntries", args, node)
|
|
return objectFromEntries(runner, args[0], node)
|
|
}),
|
|
}),
|
|
groupBy: groupBy(runner, "Object"),
|
|
},
|
|
})
|