Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
8.8 kB · 205 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206import { Effect } from "effect"import type { Extension } from "../extension.js"import { type ExtensionInvocation, hooked } from "../tool-runtime.js"import type { Interpreter } from "./interpreter.js"import { createErrorValue, isErrorType } from "./intrinsics.js"import { MAX_VALUE_DEPTH } from "./limits.js"import { PendingThrow, Throw, typeError } from "./model.js"import { fn } from "./native.js"import { define, get, has, hidden, keys, Arr, Bytes, DateObj, ErrorObj, HeadersObj, MapObj, Obj, RegExpObj, SetObj, URLObj, URLSearchParamsObj, coerceToString, type Value,} from "./objects.js"import { describeValue, isOpaque } from "./references.js"
/** * The global bindings of one run's extensions. Everything crossing the boundary is converted: plain data and * built-in wrappers are copied, a host function becomes a program function whose calls cross the same way, and a * host Promise becomes a program promise. */export const extensionGlobals = <R>( ctx: Interpreter<R>, extensions: ReadonlyArray<Extension>,): ReadonlyArray<readonly [string, Value]> => { const builtins = ctx.builtins
const toHost = (value: Value, label: string, depth = 0, seen = new Set<object>()): unknown => { if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) if (isPrimitive(value)) return value if (!(value instanceof Obj) || isOpaque(value)) { throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`) } if (seen.has(value)) throw typeError(`${label} contains a circular value.`) seen.add(value) const next = (item: Value) => toHost(item, label, depth + 1, seen) if (value instanceof ErrorObj) { const name = coerceToString(get(value, "name")) const message = get(value, "message") const text = message === undefined ? "" : coerceToString(message) const copied = name === "AggregateError" ? new AggregateError([], text) : new (hostErrors.get(name) ?? Error)(text) for (const key of new Set(["cause", ...keys(value)])) { if (uncrossed.has(key) || !has(value, key)) continue const item = crossing(() => next(get(value, key))) if (item === left) continue Object.defineProperty(copied, key, { value: item, writable: true, configurable: true, enumerable: key !== "cause", }) } seen.delete(value) return copied } const copied = value.toHost(next) seen.delete(value) return copied }
const fromHost = (value: unknown, label: string, depth = 0, seen = new Set<object>()): Value => { if (depth > MAX_VALUE_DEPTH) throw typeError(`${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) if (isPrimitive(value)) return value if (typeof value === "function") return wrap(value, label) if (value !== null && typeof value === "object") { const next = (item: unknown, path: string) => fromHost(item, path, depth + 1, seen) if (value instanceof Date) return new DateObj(builtins.Date, value.getTime()) if (value instanceof RegExp) return new RegExpObj(builtins.RegExp, value.source, value.flags) if (value instanceof Uint8Array) return new Bytes(builtins.Uint8Array, new Uint8Array(value)) if (value instanceof ArrayBuffer) return new Bytes(builtins.Uint8Array, new Uint8Array(value.slice(0))) if (value instanceof Error) { if (seen.has(value)) throw typeError(`${label} produced a circular value.`) seen.add(value) const copied = createErrorValue(builtins[isErrorType(value.name) ? value.name : "Error"], value.message) const fields = value as unknown as Record<string, unknown> for (const key of new Set(["cause", ...Object.keys(value)])) { if (uncrossed.has(key) || !(key in value) || typeof fields[key] === "function") continue const item = crossing(() => next(fields[key], `${label}.${key}`)) if (item === left) continue define(copied, key, item, key === "cause" ? hidden : undefined) } seen.delete(value) return copied } if (value instanceof URL) return new URLObj(builtins.URL, builtins.URLSearchParams, new URL(value.href)) if (value instanceof URLSearchParams) { return new URLSearchParamsObj(builtins.URLSearchParams, new URLSearchParams(value)) } if (value instanceof Headers) return new HeadersObj(builtins.Headers, new Headers(value)) if (value instanceof Map) { const wrapped = new MapObj(builtins.Map) for (const [key, item] of value) wrapped.map.set(next(key, label), next(item, label)) return wrapped } if (value instanceof Set) { const wrapped = new SetObj(builtins.Set) for (const item of value) wrapped.set.add(next(item, label)) return wrapped } if (seen.has(value)) throw typeError(`${label} produced a circular value.`) seen.add(value) if (Array.isArray(value)) { const copied = new Arr( builtins.Array, value.map((item, index) => next(item, `${label}[${index}]`)), ) seen.delete(value) return copied } const prototype = Object.getPrototypeOf(value) if (prototype === Object.prototype || prototype === null) { const copied = new Obj(builtins.Object) for (const [key, item] of Object.entries(value)) define(copied, key, next(item, `${label}.${key}`)) seen.delete(value) return copied } } throw typeError(`${label} produced ${describeHost(value)}, which the program cannot hold.`) }
// A host function as a program function. Arguments cross in; a global's call runs inside the host's extension // hooks, which see the host's own error on failure; then whatever came back, or was thrown, crosses out so the // program catches a copy. Functions inside results are part of a value's API and skip the hooks. const wrap = (value: Function, label: string, describe?: (args: ReadonlyArray<unknown>) => ExtensionInvocation) => fn<R>(builtins, value.name, value.length, (_, values) => { const args = values.map((item, index) => toHost(item, `Argument ${index + 1} to ${label}`)) const hooks = ctx.tools.hooks const settle = (run: Effect.Effect<unknown, unknown, R>) => (describe === undefined ? run : hooked(describe(args), hooks["extension.before"], hooks["extension.after"], run) ).pipe( Effect.mapError((reason) => new Throw(fromHost(reason, label))), Effect.map((settled) => fromHost(settled, label)), ) let result: unknown try { result = value.apply(undefined, args) } catch (reason) { return settle(Effect.fail(reason)) } if (!(result instanceof Promise)) return settle(Effect.succeed(result)) return ctx.pending.create(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason }))) })
return extensions.flatMap((extension) => Object.entries(extension.globals).map( ([name, value]) => [name, wrap(value, name, (args) => ({ extension: extension.name, name, args }))] as const, ), )}
/** * An error crosses as its name, message, `cause`, and own enumerable fields, such as Node's `code`, `errno`, * `syscall`, and `path`. `stack` stays on its own side, and no field may shadow an Error method. A field that cannot * cross (a socket, a handle, a function) is left behind so the error itself always arrives. */const uncrossed = new Set(["stack", "constructor", "toString", "__proto__"])const left = Symbol("left behind")const crossing = <T>(convert: () => T): T | typeof left => { try { return convert() } catch (reason) { if (reason instanceof PendingThrow) return left throw reason }}
const hostErrors = new Map<string, ErrorConstructor>([ ["TypeError", TypeError], ["RangeError", RangeError], ["SyntaxError", SyntaxError], ["ReferenceError", ReferenceError], ["EvalError", EvalError], ["URIError", URIError],])
// The primitives the interpreter operates on; symbols and BigInts are not among them.const isPrimitive = (value: unknown): value is string | number | boolean | null | undefined => value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean"
const describeHost = (value: unknown): string => { if (typeof value !== "object" || value === null) return `a ${typeof value}` const name = (value as { constructor?: { name?: string } }).constructor?.name return name === undefined || name === "" ? "an object" : `a ${name}`}