Skip to content

Commit 297019e

Browse files
authored
fix(codemode): report non-constructible new callees as TypeError (#48083)
1 parent 95503c1 commit 297019e

5 files changed

Lines changed: 104 additions & 9 deletions

File tree

packages/codemode/interpreter-support.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ ultimate source of truth.
138138
- [x] Sequence expressions (the comma operator).
139139
- [x] `await` for CodeMode promises and callable thenables; a plain value passes through unchanged, though every
140140
`await` still defers its continuation one reaction turn.
141-
- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
141+
- [x] `new` for Array, Object, Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. `new` on any
142+
other value throws a catchable `TypeError` naming the callee: other built-in functions such as `Number` say
143+
`new` is unsupported and point at the plain call, user-defined functions report the constructor gap below, and
144+
non-callable values are not constructors.
142145
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
143146
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
144147
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
@@ -367,7 +370,8 @@ ultimate source of truth.
367370
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
368371
`catch`.
369372
- [x] Source locations on unsupported-syntax diagnostics for JavaScript-shaped input; TypeScript transpilation may
370-
shift them.
373+
shift them. The diagnostic names the rejected node type and attaches a short orientation to the supported
374+
subset; this matrix is the full reference.
371375
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
372376
- [ ] Distinguish user-thrown failures from interpreter defects and explicit tool refusals from internal tool
373377
failures; preserve those categories in caught errors, promise rejection handlers, and `Promise.allSettled`

packages/codemode/src/interpreter/model.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,6 @@ export class GeneratorReturn {
8888

8989
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
9090

91-
export const supportedSyntaxMessage =
92-
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/any/resolve/reject over arrays mixing promises and plain values for parallel tool calls, promise chaining with .then/.catch/.finally, and new Promise((resolve, reject) => ...) construction."
93-
9491
export class InterpreterRuntimeError extends Error {
9592
readonly node?: AstNode
9693
errorName = "Error"
@@ -112,6 +109,10 @@ export class InterpreterRuntimeError extends Error {
112109
}
113110
}
114111

112+
// Orient the agent rather than enumerate JavaScript; interpreter-support.md is the full matrix.
113+
export const supportedSyntaxMessage =
114+
"Programs run a JavaScript subset for calling tools: plain and async functions, data literals, destructuring, standard control flow, await/Promise, and common built-ins (Array, Object, Math, JSON, Date, RegExp, Map, Set, URL). Classes, this, getters/setters, tagged templates, BigInt, and custom Symbols are unavailable; use plain functions and data objects instead."
115+
115116
export const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError =>
116117
new InterpreterRuntimeError(
117118
`Syntax '${kind}' is not supported. ${supportedSyntaxMessage}`,

packages/codemode/src/interpreter/runtime.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,19 @@ class Frame<R> {
12641264
const callee = yield* self.evaluateExpression(node.callee)
12651265
// Globals are built with this interpreter's R; `instanceof` cannot recover the type argument.
12661266
const construct = callee instanceof HostFunction ? (callee as HostFunction<R>).construct : undefined
1267-
if (construct === undefined) throw unsupportedSyntax("NewExpression", node)
1267+
if (construct === undefined) {
1268+
// `new` itself is supported, so a non-constructible callee is a TypeError like JS rather than
1269+
// unsupported syntax. Built-ins like Number are real constructors in JS, so do not claim
1270+
// otherwise; say `new` is unsupported for them and point at the plain call.
1271+
const name = calleeDescription(node.callee)
1272+
const message =
1273+
callee instanceof CodeModeFunction
1274+
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
1275+
: callee instanceof HostFunction
1276+
? `new ${name}(...) is not supported; call ${name}(...) without new instead.`
1277+
: `${name} is not a constructor.`
1278+
throw new InterpreterRuntimeError(message, node).as("TypeError")
1279+
}
12681280
const args = yield* self.evaluateCallArguments(node.arguments)
12691281
return yield* construct(args, node)
12701282
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { Effect, Schema } from "effect"
3+
import { CodeMode, Tool } from "../src/index.js"
4+
5+
// `new` is supported syntax; only the callee decides whether construction succeeds. A callee without
6+
// construction support is a TypeError naming it, like JS, rather than an unsupported-syntax diagnostic
7+
// that would suggest `new` itself is unavailable.
8+
const tools = {
9+
echo: Tool.make({
10+
description: "Echo",
11+
input: Schema.Struct({}),
12+
output: Schema.Struct({}),
13+
execute: () => Effect.succeed({}),
14+
}),
15+
}
16+
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools }))
17+
const value = async (code: string) => {
18+
const result = await run(code)
19+
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
20+
return result.value
21+
}
22+
const error = async (code: string) => {
23+
const result = await run(code)
24+
if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
25+
return result.error
26+
}
27+
28+
describe("new on a non-constructible callee", () => {
29+
test("built-in functions without construction point at the plain call", async () => {
30+
// Number is a real constructor in JS, so the message must not claim otherwise.
31+
const failure = await error(`return new Number(42)`)
32+
expect(failure.kind).toBe("ExecutionFailure")
33+
expect(failure.message).toStartWith("new Number(...) is not supported; call Number(...) without new instead.")
34+
expect(failure.suggestions).toBeUndefined()
35+
expect((await error(`return new String("a")`)).message).toStartWith("new String(...) is not supported")
36+
expect((await error(`return new Math.abs(1)`)).message).toStartWith(
37+
"new Math.abs(...) is not supported; call Math.abs(...) without new instead.",
38+
)
39+
})
40+
41+
test("non-callable values are not constructors", async () => {
42+
expect((await error(`return new tools.echo()`)).message).toStartWith("tools.echo is not a constructor.")
43+
expect((await error(`return new (1)()`)).message).toStartWith("The called value is not a constructor.")
44+
expect((await error(`const Date = 5; return new Date()`)).message).toStartWith("Date is not a constructor.")
45+
})
46+
47+
test("user-defined functions explain the documented gap", async () => {
48+
const failure = await error(`function Point(x) { return { x } }; return new Point(1)`)
49+
expect(failure.message).toStartWith(
50+
"Point cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.",
51+
)
52+
expect((await error(`const make = () => ({}); return new make()`)).message).toStartWith(
53+
"make cannot be constructed",
54+
)
55+
})
56+
57+
test("the failure is a catchable TypeError", async () => {
58+
expect(
59+
await value(`
60+
try { new Number(1) } catch (error) { return [error.name, error instanceof TypeError] }
61+
`),
62+
).toEqual(["TypeError", true])
63+
})
64+
65+
test("an undeclared callee still fails as an unknown identifier", async () => {
66+
expect((await error(`return new Function("return 1")`)).message).toContain("Function")
67+
expect((await error(`return new Function("return 1")`)).message).not.toContain("not a constructor")
68+
})
69+
70+
test("classes remain unsupported syntax", async () => {
71+
const failure = await error(`class A {}; return new A()`)
72+
expect(failure.kind).toBe("UnsupportedSyntax")
73+
expect(failure.message).toStartWith("Syntax 'ClassDeclaration' is not supported. Programs run a JavaScript subset")
74+
expect(failure.message).toContain("Classes, this, getters/setters, tagged templates, BigInt, and custom Symbols")
75+
})
76+
})

packages/codemode/test/stdlib.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ describe("Number and Math", () => {
5555
})
5656

5757
test("Number valueOf does not enable boxed numbers", async () => {
58-
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
58+
const failure = await error(`return new Number(42)`)
59+
expect(failure.kind).toBe("ExecutionFailure")
60+
expect(failure.message).toContain("new Number(...) is not supported; call Number(...) without new instead.")
5961
})
6062
})
6163

@@ -725,9 +727,9 @@ describe("stdlib integration", () => {
725727
expect(await value(`const make = (C) => new C([["a", 1]]); return make(Map).get("a")`)).toBe(1)
726728
expect(await value(`const t = { M: Map }; return new t.M() instanceof Map`)).toBe(true)
727729
const shadowed = await error(`const Date = 5; return new Date()`)
728-
expect(shadowed.kind).toBe("UnsupportedSyntax")
730+
expect(shadowed.message).toStartWith("Date is not a constructor.")
729731
const fn = await error(`const f = () => 1; return new f()`)
730-
expect(fn.kind).toBe("UnsupportedSyntax")
732+
expect(fn.message).toStartWith("f cannot be constructed")
731733
})
732734

733735
test("Object.is uses SameValue semantics", async () => {

0 commit comments

Comments
 (0)