fix(codemode): name the received value in data diagnostics and document intentional gaps (#48211)

This commit is contained in:
Aiden Cline
2026-09-09 14:48:51 -05:00
committed by GitHub
parent 9ae6b21f6a
commit 7f2510c5ca
7 changed files with 68 additions and 30 deletions
+3 -1
View File
@@ -214,6 +214,7 @@ ultimate source of truth.
- [x] `Object.keys` over arrays and tool references.
- [x] Object identity is preserved by in-CodeMode Object helpers.
- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
- [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS.
- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
- [x] `Object.is` for supported data values.
@@ -364,7 +365,8 @@ ultimate source of truth.
or without `new`.
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. Errors have no
`stack`; the diagnostic carries the source location instead.
- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
@@ -77,6 +77,23 @@ export const rejectCircularInsertion = (
}
}
export const describeValue = (value: unknown): string => {
if (value === null) return "null"
if (Array.isArray(value)) return "an array"
if (value instanceof Values.Promise) return "an un-awaited Promise"
if (value instanceof ToolReference) return "a tool reference"
if (value instanceof Values.Date) return "a Date"
if (value instanceof Values.RegExp) return "a RegExp"
if (value instanceof Values.Map) return "a Map"
if (value instanceof Values.Set) return "a Set"
if (value instanceof Values.URL) return "a URL"
if (value instanceof Values.URLSearchParams) return "a URLSearchParams"
if (value instanceof CodeModeGenerator) return "a generator"
if (isRuntimeReference(value)) return "a function"
if (typeof value === "object") return "a data object"
return `a ${typeof value}`
}
export const typeofValue = (value: unknown): string => {
if (
value instanceof HostFunction ||
+15 -5
View File
@@ -71,7 +71,13 @@ import { HostFunction, HostNamespace } from "./host.js"
import { invokeIntrinsic } from "./methods.js"
import { preserveConsumerError, type Runner } from "./runner.js"
import { invokePromiseInstanceMethod, PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js"
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
import {
containsOpaqueReference,
describeValue,
isRuntimeReference,
rejectCircularInsertion,
typeofValue,
} from "./references.js"
import { ScopeStack } from "./scope.js"
import { arrayMethods, mapMethods, setMethods } from "../stdlib/collections.js"
import { dateMethods } from "../stdlib/date.js"
@@ -1025,7 +1031,7 @@ class Frame<R> {
if (pattern.type === "ObjectPattern") {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
"Object destructuring requires a data object or array value.",
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
"InvalidDataValue",
)
@@ -1091,7 +1097,7 @@ class Frame<R> {
if (pattern.type === "ObjectPattern") {
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
throw new InterpreterRuntimeError(
"Object destructuring requires a data object or array value.",
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
pattern,
"InvalidDataValue",
)
@@ -1896,7 +1902,11 @@ class Frame<R> {
const spread = yield* self.evaluateExpression(property.argument)
if (spread === null || spread === undefined || Values.isValue(spread)) continue
if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
throw new InterpreterRuntimeError("Object spread requires a data object.", property, "InvalidDataValue")
throw new InterpreterRuntimeError(
`Object spread requires a data object, received ${describeValue(spread)}.`,
property,
"InvalidDataValue",
)
}
for (const [key, value] of Object.entries(spread)) {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property)
@@ -2126,7 +2136,7 @@ class Frame<R> {
if (isRuntimeReference(objectValue)) {
throw new InterpreterRuntimeError(
"Runtime references are opaque and do not expose properties.",
`Cannot read properties of ${describeValue(objectValue)}; only data values expose properties.`,
objectNode,
"InvalidDataValue",
)
+2 -9
View File
@@ -1,8 +1,8 @@
import { Effect } from "effect"
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
import { type AstNode, CodeModeGenerator, InterpreterRuntimeError } from "../interpreter/model.js"
import { describeValue } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
import { Values } from "../values.js"
const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> => {
if (args.length !== 1) return [...args]
@@ -16,13 +16,6 @@ const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> =>
}
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
if (source instanceof Values.Promise) {
throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.",
node,
"InvalidDataValue",
)
}
if (
source !== null &&
typeof source === "object" &&
@@ -35,7 +28,7 @@ const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: num
return { length: normalized, source }
}
throw new InterpreterRuntimeError(
"Array.from expects an array, string, Map, Set, or array-like value.",
`Array.from expects an array, string, Map, Set, or array-like value, received ${describeValue(source)}.`,
node,
"InvalidDataValue",
)
+6 -2
View File
@@ -2,7 +2,7 @@ import { Effect } from "effect"
import { isBlockedMember, type SafeObject } from "../data.js"
import { HostFunction, requiresNew } from "../interpreter/host.js"
import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js"
import { isRuntimeReference } from "../interpreter/references.js"
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
import { applyCollectionCallback, preserveConsumerError, type Runner, toPrimitive } from "../interpreter/runner.js"
import { Values } from "../values.js"
import { coerceToString } from "./value.js"
@@ -73,7 +73,11 @@ const coerceGroupByPropertyKey = <R>(
): Effect.Effect<string, unknown, R> => {
if (value instanceof Values.Promise) return Effect.succeed("[object Promise]")
if (!Values.isValue(value) && isRuntimeReference(value)) {
throw new InterpreterRuntimeError("Object.groupBy callback must return a data value.", node, "InvalidDataValue")
throw new InterpreterRuntimeError(
`Object.groupBy callback must return a data value, received ${describeValue(value)}.`,
node,
"InvalidDataValue",
)
}
return Effect.map(toPrimitive(runner, value, "string", node), coerceToString)
}
+9 -10
View File
@@ -2,7 +2,12 @@ import { Effect } from "effect"
import { isBlockedMember, toProgram } from "../data.js"
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "../interpreter/references.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"
@@ -12,20 +17,14 @@ 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 {}
if (input instanceof Values.Promise) {
const prototype = input === null || typeof input !== "object" ? undefined : Object.getPrototypeOf(input)
if (prototype !== null && prototype !== Object.prototype) {
throw new InterpreterRuntimeError(
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
`Object.${name} expects a data object or array, received ${describeValue(input)}.`,
node,
"InvalidDataValue",
)
}
if (input === null || typeof input !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
const prototype = Object.getPrototypeOf(input)
if (prototype !== null && prototype !== Object.prototype) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
return input as Record<string, unknown>
}
+16 -3
View File
@@ -85,9 +85,22 @@ describe("Object.keys over arrays", () => {
expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"])
})
test("non-object inputs still fail clearly", async () => {
const failure = await error(`return Object.keys("nope")`)
expect(failure.message).toContain("Object.keys expects a data object or array")
test("non-object inputs name what was received", async () => {
expect((await error(`return Object.keys("nope")`)).message).toContain(
"Object.keys expects a data object or array, received a string.",
)
expect((await error(`return Object.entries(42)`)).message).toContain("received a number.")
expect((await error(`return Object.values(null)`)).message).toContain("received null.")
expect((await error(`return Object.keys(tools.github.list_issues({ value: "x" }))`)).message).toContain(
"received an un-awaited Promise.",
)
expect((await error(`return Object.entries(() => 1)`)).message).toContain("received a function.")
expect((await error(`return { ...[1] }`)).message).toContain(
"Object spread requires a data object, received an array.",
)
expect((await error(`const { a } = new Map(); return a`)).message).toContain("received a Map.")
expect((await error(`return Array.from(7)`)).message).toContain("received a number.")
expect((await error(`return (() => 1).x`)).message).toContain("Cannot read properties of a function")
})
})