You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Closure in a let/const initializer cannot see the binding that declarator introduces (ReferenceError; breaks claude install) #9718
A closure created inside a let/const declarator's initializer that references the binding that declarator introduces is compiled as if the binding did not exist: calling it later throws ReferenceError: <name> is not defined. Node only has a TDZ window here, so a callback invoked after initialization is required to work.
Minimized, fails on current main (0e3ce3812):
constmk=(o: {cb: ()=>void}): Promise<{u: ()=>void}>=>{setTimeout(o.cb,1);returnPromise.resolve({u: ()=>console.log("unmount")});};asyncfunctioncall(): Promise<void>{const{u: O}=awaitmk({cb: ()=>{O();}});// <- O declared by THIS declaratorconsole.log("call returned");}voidcall();
node : call returned / unmount
perry : call returned / ReferenceError: O is not defined
Which shapes fail
A closure created after the declaration sees the binding fine. Every shape where the closure is built in the initializer fails, so this is not destructuring-specific:
shape
node
perry
const {u:O} = await p; later(() => O()) — closure after the decl
OK
OK
const {u:O} = await mk({cb: () => O()})
OK
O is not defined
const {u:O} = mk({cb: () => O()}) (no await)
OK
O is not defined
const O = await mk({cb: () => O()}) (plain binding)
Repro files as used: test-files/ fixture in the PR; the matrix above is a single program that prints one line per shape.
Where it bites in the wild
This is the root cause of the B60_install_badtarget divergence carried in #9575 (claude install not-a-version: node exits 1, perry exits 0 with one byte less stdout). cli_2.1.112.js's install subcommand is exactly this shape:
onDone fires after the render resolves, throws ReferenceError: O is not defined, and claude-code logs uncaught_exception (3/3 runs, CLAUDE_CODE_DIAGNOSTICS_FILE). The uncaught throw is what drops the trailing newline and the exit code — so B60 is not an install-path bug at all, and #9487's closure did not miss a hunk.
Two observable consequences worth noting separately:
ReferenceError: <name> is not defined where the binding demonstrably exists in the enclosing scope — capture analysis is not putting the declarator's own binding in scope for closures inside its initializer.
The uncaught exception exits 0 under perry (node exits 1 on an uncaught exception). That second part may deserve its own fix.
Suspect area: closure capture collection (collect_local_refs_expr and friends) walks the declarator initializer before the declarator's binding is registered in the enclosing scope, so the free variable resolves to nothing instead of to the (still-uninitialized) local.
Activity
fix(hir): let a declarator's own initializer forward-capture its binding