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

  1. Emit exactly one declarator: `for (const k in obj)`.
  2. Split extra declarations into separate statements before the loop.
  3. 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

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/ea2df3986a4bc10a. Report an issue: GitHub.