Skip to content

Commit 7f2510c

Browse files
authored
fix(codemode): name the received value in data diagnostics and document intentional gaps (#48211)
1 parent 9ae6b21 commit 7f2510c

7 files changed

Lines changed: 68 additions & 30 deletions

File tree

packages/codemode/interpreter-support.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ ultimate source of truth.
214214
- [x] `Object.keys` over arrays and tool references.
215215
- [x] Object identity is preserved by in-CodeMode Object helpers.
216216
- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
217+
- [x] Circular references are rejected when created (`o.self = o`, `array.push(array)`), not at serialization as in JS.
217218
- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
218219
cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
219220
- [x] `Object.is` for supported data values.
@@ -364,7 +365,8 @@ ultimate source of truth.
364365
or without `new`.
365366
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
366367
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
367-
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
368+
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. Errors have no
369+
`stack`; the diagnostic carries the source location instead.
368370
- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
369371
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
370372
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program

packages/codemode/src/interpreter/references.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ export const rejectCircularInsertion = (
7777
}
7878
}
7979

80+
export const describeValue = (value: unknown): string => {
81+
if (value === null) return "null"
82+
if (Array.isArray(value)) return "an array"
83+
if (value instanceof Values.Promise) return "an un-awaited Promise"
84+
if (value instanceof ToolReference) return "a tool reference"
85+
if (value instanceof Values.Date) return "a Date"
86+
if (value instanceof Values.RegExp) return "a RegExp"
87+
if (value instanceof Values.Map) return "a Map"
88+
if (value instanceof Values.Set) return "a Set"
89+
if (value instanceof Values.URL) return "a URL"
90+
if (value instanceof Values.URLSearchParams) return "a URLSearchParams"
91+
if (value instanceof CodeModeGenerator) return "a generator"
92+
if (isRuntimeReference(value)) return "a function"
93+
if (typeof value === "object") return "a data object"
94+
return `a ${typeof value}`
95+
}
96+
8097
export const typeofValue = (value: unknown): string => {
8198
if (
8299
value instanceof HostFunction ||

packages/codemode/src/interpreter/runtime.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ import { HostFunction, HostNamespace } from "./host.js"
7171
import { invokeIntrinsic } from "./methods.js"
7272
import { preserveConsumerError, type Runner } from "./runner.js"
7373
import { invokePromiseInstanceMethod, PromiseRuntime, resolvePromise, resolvePromiseValue } from "./promises.js"
74-
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
74+
import {
75+
containsOpaqueReference,
76+
describeValue,
77+
isRuntimeReference,
78+
rejectCircularInsertion,
79+
typeofValue,
80+
} from "./references.js"
7581
import { ScopeStack } from "./scope.js"
7682
import { arrayMethods, mapMethods, setMethods } from "../stdlib/collections.js"
7783
import { dateMethods } from "../stdlib/date.js"
@@ -1025,7 +1031,7 @@ class Frame<R> {
10251031
if (pattern.type === "ObjectPattern") {
10261032
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
10271033
throw new InterpreterRuntimeError(
1028-
"Object destructuring requires a data object or array value.",
1034+
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
10291035
pattern,
10301036
"InvalidDataValue",
10311037
)
@@ -1091,7 +1097,7 @@ class Frame<R> {
10911097
if (pattern.type === "ObjectPattern") {
10921098
if (value === null || typeof value !== "object" || isRuntimeReference(value)) {
10931099
throw new InterpreterRuntimeError(
1094-
"Object destructuring requires a data object or array value.",
1100+
`Object destructuring requires a data object or array value, received ${describeValue(value)}.`,
10951101
pattern,
10961102
"InvalidDataValue",
10971103
)
@@ -1896,7 +1902,11 @@ class Frame<R> {
18961902
const spread = yield* self.evaluateExpression(property.argument)
18971903
if (spread === null || spread === undefined || Values.isValue(spread)) continue
18981904
if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
1899-
throw new InterpreterRuntimeError("Object spread requires a data object.", property, "InvalidDataValue")
1905+
throw new InterpreterRuntimeError(
1906+
`Object spread requires a data object, received ${describeValue(spread)}.`,
1907+
property,
1908+
"InvalidDataValue",
1909+
)
19001910
}
19011911
for (const [key, value] of Object.entries(spread)) {
19021912
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property)
@@ -2126,7 +2136,7 @@ class Frame<R> {
21262136

21272137
if (isRuntimeReference(objectValue)) {
21282138
throw new InterpreterRuntimeError(
2129-
"Runtime references are opaque and do not expose properties.",
2139+
`Cannot read properties of ${describeValue(objectValue)}; only data values expose properties.`,
21302140
objectNode,
21312141
"InvalidDataValue",
21322142
)

packages/codemode/src/stdlib/array.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Effect } from "effect"
22
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
33
import { type AstNode, CodeModeGenerator, InterpreterRuntimeError } from "../interpreter/model.js"
4+
import { describeValue } from "../interpreter/references.js"
45
import { applyCollectionCallback, preserveConsumerError, type Runner } from "../interpreter/runner.js"
5-
import { Values } from "../values.js"
66

77
const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> => {
88
if (args.length !== 1) return [...args]
@@ -16,13 +16,6 @@ const constructArray = (args: Array<unknown>, node: AstNode): Array<unknown> =>
1616
}
1717

1818
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
19-
if (source instanceof Values.Promise) {
20-
throw new InterpreterRuntimeError(
21-
"Array.from received an un-awaited Promise; await it before creating the array.",
22-
node,
23-
"InvalidDataValue",
24-
)
25-
}
2619
if (
2720
source !== null &&
2821
typeof source === "object" &&
@@ -35,7 +28,7 @@ const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: num
3528
return { length: normalized, source }
3629
}
3730
throw new InterpreterRuntimeError(
38-
"Array.from expects an array, string, Map, Set, or array-like value.",
31+
`Array.from expects an array, string, Map, Set, or array-like value, received ${describeValue(source)}.`,
3932
node,
4033
"InvalidDataValue",
4134
)

packages/codemode/src/stdlib/collections.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Effect } from "effect"
22
import { isBlockedMember, type SafeObject } from "../data.js"
33
import { HostFunction, requiresNew } from "../interpreter/host.js"
44
import { type AstNode, InterpreterRuntimeError, isRecord } from "../interpreter/model.js"
5-
import { isRuntimeReference } from "../interpreter/references.js"
5+
import { describeValue, isRuntimeReference } from "../interpreter/references.js"
66
import { applyCollectionCallback, preserveConsumerError, type Runner, toPrimitive } from "../interpreter/runner.js"
77
import { Values } from "../values.js"
88
import { coerceToString } from "./value.js"
@@ -73,7 +73,11 @@ const coerceGroupByPropertyKey = <R>(
7373
): Effect.Effect<string, unknown, R> => {
7474
if (value instanceof Values.Promise) return Effect.succeed("[object Promise]")
7575
if (!Values.isValue(value) && isRuntimeReference(value)) {
76-
throw new InterpreterRuntimeError("Object.groupBy callback must return a data value.", node, "InvalidDataValue")
76+
throw new InterpreterRuntimeError(
77+
`Object.groupBy callback must return a data value, received ${describeValue(value)}.`,
78+
node,
79+
"InvalidDataValue",
80+
)
7781
}
7882
return Effect.map(toPrimitive(runner, value, "string", node), coerceToString)
7983
}

packages/codemode/src/stdlib/object.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { Effect } from "effect"
22
import { isBlockedMember, toProgram } from "../data.js"
33
import { HostFunction, sync, syncCall } from "../interpreter/host.js"
44
import { type AstNode, AsyncIteratorSymbol, InterpreterRuntimeError, IteratorSymbol } from "../interpreter/model.js"
5-
import { containsOpaqueReference, rejectCircularInsertion, typeofValue } from "../interpreter/references.js"
5+
import {
6+
containsOpaqueReference,
7+
describeValue,
8+
rejectCircularInsertion,
9+
typeofValue,
10+
} from "../interpreter/references.js"
611
import { preserveConsumerError, type Runner } from "../interpreter/runner.js"
712
import { ToolReference } from "../tool-runtime.js"
813
import { Values } from "../values.js"
@@ -12,20 +17,14 @@ import { coerceToString } from "./value.js"
1217
const requireObject = (name: string, input: unknown, node: AstNode): Record<string, unknown> => {
1318
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
1419
if (Values.isValue(input)) return {}
15-
if (input instanceof Values.Promise) {
20+
const prototype = input === null || typeof input !== "object" ? undefined : Object.getPrototypeOf(input)
21+
if (prototype !== null && prototype !== Object.prototype) {
1622
throw new InterpreterRuntimeError(
17-
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
23+
`Object.${name} expects a data object or array, received ${describeValue(input)}.`,
1824
node,
1925
"InvalidDataValue",
2026
)
2127
}
22-
if (input === null || typeof input !== "object") {
23-
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
24-
}
25-
const prototype = Object.getPrototypeOf(input)
26-
if (prototype !== null && prototype !== Object.prototype) {
27-
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
28-
}
2928
return input as Record<string, unknown>
3029
}
3130

packages/codemode/test/enumeration.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,22 @@ describe("Object.keys over arrays", () => {
8585
expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"])
8686
})
8787

88-
test("non-object inputs still fail clearly", async () => {
89-
const failure = await error(`return Object.keys("nope")`)
90-
expect(failure.message).toContain("Object.keys expects a data object or array")
88+
test("non-object inputs name what was received", async () => {
89+
expect((await error(`return Object.keys("nope")`)).message).toContain(
90+
"Object.keys expects a data object or array, received a string.",
91+
)
92+
expect((await error(`return Object.entries(42)`)).message).toContain("received a number.")
93+
expect((await error(`return Object.values(null)`)).message).toContain("received null.")
94+
expect((await error(`return Object.keys(tools.github.list_issues({ value: "x" }))`)).message).toContain(
95+
"received an un-awaited Promise.",
96+
)
97+
expect((await error(`return Object.entries(() => 1)`)).message).toContain("received a function.")
98+
expect((await error(`return { ...[1] }`)).message).toContain(
99+
"Object spread requires a data object, received an array.",
100+
)
101+
expect((await error(`const { a } = new Map(); return a`)).message).toContain("received a Map.")
102+
expect((await error(`return Array.from(7)`)).message).toContain("received a number.")
103+
expect((await error(`return (() => 1).x`)).message).toContain("Cannot read properties of a function")
91104
})
92105
})
93106

0 commit comments

Comments
 (0)