mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-27 03:56:18 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99f86595d4 | ||
|
|
ea12b70411 | ||
|
|
f70da77f62 |
@@ -36,7 +36,8 @@ ultimate source of truth.
|
||||
- [x] Template literals with interpolation.
|
||||
- [x] Regular-expression literals.
|
||||
- [x] `NaN` and `Infinity` globals.
|
||||
- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries.
|
||||
- [x] `BigInt(...)`, bigint literals, exact arithmetic, updates, `toString(radix)`, and `valueOf()`; bigint values stay
|
||||
native across tool calls and become precise decimal strings only in final program results.
|
||||
- [ ] Arbitrary Symbol primitive values and symbol-keyed properties. The confined `Symbol.iterator` and
|
||||
`Symbol.asyncIterator` keys are available only for custom iterator protocols.
|
||||
- [ ] Tagged-template calls.
|
||||
|
||||
@@ -45,9 +45,9 @@ export const normalizeError = (error: unknown): Diagnostic => {
|
||||
message = (value as { message: string }).message
|
||||
} else {
|
||||
try {
|
||||
message = JSON.stringify(copyOut(value, "json")) ?? String(value)
|
||||
message = JSON.stringify(copyOut(value, "nullify")) ?? coerceToString(value)
|
||||
} catch {
|
||||
message = String(value)
|
||||
message = coerceToString(value)
|
||||
}
|
||||
}
|
||||
return { kind: "ExecutionFailure", message: `Uncaught: ${message}` }
|
||||
|
||||
@@ -97,6 +97,17 @@ export const invokeIntrinsic = <R>(
|
||||
if (typeof ref.receiver === "number") {
|
||||
return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
|
||||
}
|
||||
if (typeof ref.receiver === "bigint") {
|
||||
if (ref.name === "valueOf") return Effect.succeed(ref.receiver)
|
||||
const radix = args[0]
|
||||
if (radix !== undefined && typeof radix !== "number") {
|
||||
throw new InterpreterRuntimeError("BigInt.toString expects a numeric radix.", node).as("TypeError")
|
||||
}
|
||||
if (typeof radix === "number" && (radix < 2 || radix > 36)) {
|
||||
throw new InterpreterRuntimeError("BigInt.toString radix must be between 2 and 36.", node).as("RangeError")
|
||||
}
|
||||
return Effect.succeed(ref.receiver.toString(radix))
|
||||
}
|
||||
if (Array.isArray(ref.receiver)) {
|
||||
return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export class JsonMethodReference {
|
||||
}
|
||||
|
||||
export class CoercionFunction {
|
||||
constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat" | "isFinite" | "isNaN") {}
|
||||
constructor(readonly name: "Number" | "String" | "Boolean" | "BigInt" | "parseInt" | "parseFloat" | "isFinite" | "isNaN") {}
|
||||
}
|
||||
|
||||
export class UriFunction {
|
||||
|
||||
@@ -311,6 +311,7 @@ export class Interpreter<R> {
|
||||
globalScope.set("Math", { mutable: false, value: new GlobalNamespace("Math") })
|
||||
globalScope.set("JSON", { mutable: false, value: new GlobalNamespace("JSON") })
|
||||
globalScope.set("Number", { mutable: false, value: new CoercionFunction("Number") })
|
||||
globalScope.set("BigInt", { mutable: false, value: new CoercionFunction("BigInt") })
|
||||
globalScope.set("String", { mutable: false, value: new CoercionFunction("String") })
|
||||
globalScope.set("Boolean", { mutable: false, value: new CoercionFunction("Boolean") })
|
||||
globalScope.set("Array", { mutable: false, value: new GlobalNamespace("Array") })
|
||||
@@ -1893,18 +1894,18 @@ export class Interpreter<R> {
|
||||
|
||||
// CodeMode numeric coercion, not host Number(): null-prototype data objects would make
|
||||
// the host throw during ToPrimitive, and opaque runtime references must reject clearly.
|
||||
const operand = (current: unknown): number => {
|
||||
const operand = (current: unknown): number | bigint => {
|
||||
if (containsOpaqueReference(current)) {
|
||||
throw new InterpreterRuntimeError(`'${operator}' requires a data value.`, argument, "InvalidDataValue")
|
||||
}
|
||||
return coerceToNumber(current)
|
||||
return typeof current === "bigint" ? current : coerceToNumber(current)
|
||||
}
|
||||
|
||||
if (argument.type === "Identifier") {
|
||||
return Effect.sync(() => {
|
||||
const name = getString(argument, "name")
|
||||
const current = operand(this.scopes.get(name, argument))
|
||||
const next = current + increment
|
||||
const next = typeof current === "bigint" ? current + BigInt(increment) : current + increment
|
||||
this.scopes.set(name, next, argument)
|
||||
return prefix ? next : current
|
||||
})
|
||||
@@ -1913,7 +1914,7 @@ export class Interpreter<R> {
|
||||
if (argument.type === "MemberExpression") {
|
||||
return this.modifyMember(argument, (current) => {
|
||||
const value = operand(current)
|
||||
const next = value + increment
|
||||
const next = typeof value === "bigint" ? value + BigInt(increment) : value + increment
|
||||
return Effect.succeed({ write: true, next, result: prefix ? next : value })
|
||||
})
|
||||
}
|
||||
@@ -2581,6 +2582,11 @@ export class Interpreter<R> {
|
||||
return new ComputedValue(undefined)
|
||||
}
|
||||
|
||||
if (typeof objectValue === "bigint") {
|
||||
if (key === "toString" || key === "valueOf") return new IntrinsicReference(objectValue, key)
|
||||
return new ComputedValue(undefined)
|
||||
}
|
||||
|
||||
if (objectValue instanceof CoercionFunction) {
|
||||
if (typeof key === "string" && isBlockedMember(key)) {
|
||||
throw new InterpreterRuntimeError(`${objectValue.name}.${key} is not available.`, propertyNode)
|
||||
|
||||
@@ -89,6 +89,12 @@ export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node
|
||||
const value = boundedData(raw, `${ref.name} input`)
|
||||
if (ref.name === "Number") return coerceToNumber(value)
|
||||
if (ref.name === "Boolean") return Boolean(value)
|
||||
if (ref.name === "BigInt") {
|
||||
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") {
|
||||
throw new InterpreterRuntimeError("BigInt expects a string, integer, bigint, or boolean.", node).as("TypeError")
|
||||
}
|
||||
return BigInt(value)
|
||||
}
|
||||
if (ref.name === "isFinite") return Number.isFinite(coerceToNumber(value))
|
||||
if (ref.name === "isNaN") return Number.isNaN(coerceToNumber(value))
|
||||
if (ref.name === "parseInt") {
|
||||
|
||||
@@ -149,7 +149,8 @@ const copyBounded = (
|
||||
value === undefined ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "number"
|
||||
typeof value === "number" ||
|
||||
typeof value === "bigint"
|
||||
) {
|
||||
return value
|
||||
}
|
||||
@@ -256,11 +257,12 @@ const copyBounded = (
|
||||
// "json" mirrors JSON.stringify (undefined object values drop, undefined array elements become
|
||||
// null, a bare undefined passes through): use it wherever data leaves as JSON, like tool
|
||||
// arguments and stringify-style formatting. "nullify" turns every undefined, including a bare
|
||||
// one, into null: use it for program results, where the consumer must never see undefined.
|
||||
// one, into null and bigint values into precise decimal strings: use it for final program results.
|
||||
export type CopyOutMode = "json" | "nullify"
|
||||
|
||||
export const copyOut = (value: unknown, mode: CopyOutMode): unknown => {
|
||||
if (value === undefined && mode === "nullify") return null
|
||||
if (typeof value === "bigint" && mode === "nullify") return value.toString()
|
||||
if (typeof value === "number" && !Number.isFinite(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const MAX_RENDER_DEPTH = 8
|
||||
type RenderContext = {
|
||||
readonly definitions: Readonly<Record<string, JsonSchema>>
|
||||
readonly pretty: boolean
|
||||
readonly bigints?: WeakSet<JsonSchema>
|
||||
}
|
||||
|
||||
const hasUnresolvedRef = (
|
||||
@@ -88,6 +89,7 @@ const renderSchema = (
|
||||
seen: ReadonlySet<string> = new Set(),
|
||||
): string => {
|
||||
if (depth > MAX_RENDER_DEPTH) return "unknown"
|
||||
if (ctx.bigints?.has(schema)) return "bigint"
|
||||
const nested =
|
||||
schema.definitions === undefined && schema.$defs === undefined
|
||||
? ctx
|
||||
@@ -165,14 +167,52 @@ const renderSchema = (
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
const markBigInts = (ast: unknown, schema: JsonSchema, bigints: WeakSet<JsonSchema>): void => {
|
||||
if (ast === null || typeof ast !== "object" || !("_tag" in ast)) return
|
||||
if (ast._tag === "BigInt") {
|
||||
bigints.add(schema)
|
||||
return
|
||||
}
|
||||
if (ast._tag === "Objects" && "propertySignatures" in ast && Array.isArray(ast.propertySignatures)) {
|
||||
for (const field of ast.propertySignatures) {
|
||||
if (typeof field !== "object" || field === null || !("name" in field) || !("type" in field)) continue
|
||||
if (typeof field.name !== "string") continue
|
||||
const property = schema.properties?.[field.name]
|
||||
if (property !== undefined) markBigInts(field.type, property, bigints)
|
||||
}
|
||||
if ("indexSignatures" in ast && Array.isArray(ast.indexSignatures) && typeof schema.additionalProperties === "object") {
|
||||
for (const index of ast.indexSignatures) {
|
||||
if (typeof index === "object" && index !== null && "type" in index) {
|
||||
markBigInts(index.type, schema.additionalProperties, bigints)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (ast._tag === "Arrays" && "rest" in ast && Array.isArray(ast.rest) && schema.items !== undefined) {
|
||||
for (const item of ast.rest) markBigInts(item, schema.items, bigints)
|
||||
return
|
||||
}
|
||||
if (ast._tag === "Union" && "types" in ast && Array.isArray(ast.types)) {
|
||||
const alternatives = schema.anyOf ?? schema.oneOf
|
||||
if (alternatives === undefined) return
|
||||
for (const [index, item] of ast.types.entries()) {
|
||||
const alternative = alternatives[index]
|
||||
if (alternative !== undefined) markBigInts(item, alternative, bigints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => {
|
||||
try {
|
||||
const visible = decoded ? Schema.toType(schema) : schema
|
||||
const visible = decoded ? Schema.toType(schema) : Schema.toEncoded(schema)
|
||||
const document = Schema.toJsonSchemaDocument(visible) as {
|
||||
readonly schema: JsonSchema
|
||||
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
||||
}
|
||||
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty })
|
||||
const bigints = new WeakSet<JsonSchema>()
|
||||
markBigInts(visible.ast, document.schema, bigints)
|
||||
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty, bigints })
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
@@ -437,6 +437,97 @@ describe("CodeMode console capture", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("CodeMode bigint values", () => {
|
||||
test("preserves exact bigint arithmetic and stringifies nested final results", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
const value = BigInt("9007199254740993")
|
||||
return { value, doubled: value * 2n, nested: [value + 1n], hex: value.toString(16) }
|
||||
`,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
value: "9007199254740993",
|
||||
doubled: "18014398509481986",
|
||||
nested: ["9007199254740994"],
|
||||
hex: "20000000000001",
|
||||
},
|
||||
toolCalls: [],
|
||||
})
|
||||
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
|
||||
})
|
||||
|
||||
test("returns a structured diagnostic when an uncaught object contains bigint values", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `throw { value: 9007199254740993n, nested: [1n] }`,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "ExecutionFailure",
|
||||
message: 'Uncaught: {"value":"9007199254740993","nested":["1"]}',
|
||||
},
|
||||
toolCalls: [],
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves native bigint values across chained schema-backed tool calls", async () => {
|
||||
const lookup = Tool.make({
|
||||
description: "Look up an exact integer",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ value: Schema.BigIntFromString }),
|
||||
execute: () => Effect.succeed({ value: "9007199254740993" }),
|
||||
})
|
||||
const double = Tool.make({
|
||||
description: "Double an exact integer",
|
||||
input: Schema.Struct({ value: Schema.BigInt }),
|
||||
output: Schema.BigInt,
|
||||
execute: ({ value }) => Effect.succeed(value * 2n),
|
||||
})
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.make({ tools: { lookup, double } }).execute(`
|
||||
const result = await tools.lookup({})
|
||||
return await tools.double({ value: result.value + 1n })
|
||||
`),
|
||||
)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
ok: true,
|
||||
value: "18014398509481988",
|
||||
toolCalls: [{ name: "lookup" }, { name: "double" }],
|
||||
})
|
||||
})
|
||||
|
||||
test("increments bigint bindings and object properties without losing precision", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.execute({
|
||||
code: `
|
||||
let value = 9007199254740993n
|
||||
const record = { value }
|
||||
value++
|
||||
++record.value
|
||||
return [value, record.value, typeof value, String(value)]
|
||||
`,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.ok ? result.value : result.error).toStrictEqual([
|
||||
"9007199254740994",
|
||||
"9007199254740994",
|
||||
"bigint",
|
||||
"9007199254740994",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("CodeMode output budget", () => {
|
||||
test("absent maxOutputBytes means no truncation at all", async () => {
|
||||
const result = await Effect.runPromise(
|
||||
|
||||
@@ -307,6 +307,16 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
|
||||
})
|
||||
})
|
||||
|
||||
describe("copyOut bigint handling per boundary mode", () => {
|
||||
test("preserves native bigint tool arguments but stringifies final result values", () => {
|
||||
expect(ToolRuntime.copyOut(9007199254740993n, "json")).toBe(9007199254740993n)
|
||||
expect(ToolRuntime.copyOut(9007199254740993n, "nullify")).toBe("9007199254740993")
|
||||
expect(ToolRuntime.copyOut({ value: [9007199254740993n] }, "nullify")).toStrictEqual({
|
||||
value: ["9007199254740993"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("copyOut undefined handling per boundary mode", () => {
|
||||
test("json mode mirrors JSON.stringify for undefined", () => {
|
||||
expect(ToolRuntime.copyOut({ q: undefined, keep: 1 }, "json")).toStrictEqual({ keep: 1 })
|
||||
|
||||
@@ -69,6 +69,30 @@ describe("pretty signature rendering", () => {
|
||||
expect(outputTypeScript(lookupOrder)).toBe("{ status: string }")
|
||||
})
|
||||
|
||||
test("renders native bigint schemas accurately in tool inputs and decoded outputs", () => {
|
||||
const native = Tool.make({
|
||||
description: "Double an exact integer",
|
||||
input: Schema.Struct({ value: Schema.BigInt, values: Schema.Array(Schema.BigInt) }),
|
||||
output: Schema.BigInt,
|
||||
execute: ({ value }) => Effect.succeed(value * 2n),
|
||||
})
|
||||
const decoded = Tool.make({
|
||||
description: "Look up an exact integer",
|
||||
input: Schema.Struct({ encoded: Schema.BigIntFromString }),
|
||||
output: Schema.Struct({ value: Schema.BigIntFromString }),
|
||||
execute: () => Effect.succeed({ value: "9007199254740993" }),
|
||||
})
|
||||
|
||||
expect(inputTypeScript(native)).toBe("{ value: bigint; values: Array<bigint> }")
|
||||
expect(outputTypeScript(native)).toBe("bigint")
|
||||
expect(inputTypeScript(decoded)).toBe("{ encoded: string }")
|
||||
expect(outputTypeScript(decoded)).toBe("{ value: bigint }")
|
||||
expect(CodeMode.make({ tools: { native, decoded } }).catalog().map((item) => item.signature)).toStrictEqual([
|
||||
"tools.decoded(input: {\n encoded: string,\n}): Promise<{\n value: bigint,\n}>",
|
||||
"tools.native(input: {\n value: bigint,\n values: Array<bigint>,\n}): Promise<bigint>",
|
||||
])
|
||||
})
|
||||
|
||||
test("nested objects recurse with increasing indent and their own JSDoc", () => {
|
||||
const pretty = jsonSchemaToTypeScript(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user