different-ai/openwork · error · InterpreterRuntimeError
for...in supports one declared binding.
Error message
for...in supports one declared binding.
What it means
Like for...of, the for...in head accepts only a single declared binding. If the left side is a VariableDeclaration whose declarations array has a length other than 1, the interpreter throws at the declaration node. Multiple declarators in a for-in head are invalid in real JavaScript too; this makes it an explicit runtime error for AST-driven execution.
Source
Thrown at packages/codemode/src/interpreter/runtime.ts:1191
// namespace/tool names at that node - the same enumeration Object.keys performs.
// Anything else (strings, Maps, Sets, numbers, null, ...) is a deliberate error rather
// than real JS's surprising behavior (indices for strings, zero iterations for
// Maps/Sets/null): the hint points at the constructs that do what the program means.
const keys = self.enumerableKeys(right)
if (keys === undefined) {
throw new InterpreterRuntimeError(
"for...in requires a plain object, array, or tools reference in CodeMode. Use for...of for arrays/strings/Maps/Sets, or Object.keys(value) for a key list.",
node,
)
}
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
let assignmentName: string | undefined
if (left.type === "VariableDeclaration") {
const declarations = getArray(left, "declarations")
if (declarations.length !== 1) {
throw new InterpreterRuntimeError("for...in supports one declared binding.", left)
}
const declarator = asNode(declarations[0], "declarations[0]")
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
} else if (left.type === "Identifier") {
assignmentName = getString(left, "name")
} else {
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
}
for (const key of keys) {
if (declaration) {
self.pushScope()
yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left)
} else if (assignmentName) {
self.setIdentifierValue(assignmentName, key, left)
}
View on GitHub (pinned to 2b7df46e8a)
Solutions
- Emit exactly one declarator: `for (const k in obj)`.
- Split extra declarations into separate statements before the loop.
- Fix the AST producer (generator/transformer) to guarantee one declaration per for-in head.
Example fix
// before (AST) declarations: [decl(k), decl(j)] // after declarations: [decl(k)]
Defensive patterns
Strategy: validation
Validate before calling
// AST check before execution
if (forInNode.left.type === "VariableDeclaration" && forInNode.left.declarations.length !== 1) {
throw new Error("for...in head must declare exactly one binding");
} Try / catch
try {
interpret(src);
} catch (e) {
if (e instanceof InterpreterRuntimeError && e.message.includes("for...in supports one")) {
// emit a single declarator in the for-in head
}
throw e;
} Prevention
- Emit exactly one declarator in for-in heads.
- Hoist any extra declarations before the loop.
- Validate generated AST loop nodes before execution.
When it happens
Trigger: A for...in AST whose VariableDeclaration `declarations` array contains zero or two-plus declarators — usually from programmatically generated ASTs.
Common situations: CodeMode ASTs emitted by an LLM or code generator that merged or dropped declarators; hand-built ASTs in tests.
Related errors
- Unsupported for...in binding.
- for...of supports one declared binding.
- Unsupported for...of binding.
- for...in requires a plain object, array, or tools reference
- Expected '${key}' to be a boolean.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/ea2df3986a4bc10a.
Report an issue: GitHub.