Repository navigation
Description
A forward reference between two mutually recursive const arrows throws ReferenceError: Cannot access undefined before initialization at call time, long after both bindings are initialized. Deterministic, 5/5 runs. Not a regression — it reproduces identically on 28c292517 and on that commit plus #9720.
type Off = () => string;
const listeners: (() => string)[] = [];
const ev = { on(cb: () => string): Off { listeners.push(cb); return () => "off-called"; } };
function main(): void {
const fact = (n: number): number => (n <= 1 ? 1 : n * fact(n - 1));
console.log("fact=" + fact(5));
const off = ev.on(() => off());
console.log("off=" + listeners[0]!());
const sub = { unsub: (): string => "unsubbed" };
const sub2 = ((o: { next: () => string }) => { listeners.push(o.next); return sub; })({ next: () => sub2.unsub() });
console.log("sub2=" + listeners[1]!());
const a = (): string => b() + "/a",
b = (): string => "b";
console.log("multi=" + a());
const fib = function rec(n: number): number { return n < 2 ? n : rec(n - 1) + rec(n - 2); };
console.log("fib=" + fib(10));
// mutual recursion across statements
const even = (n: number): boolean => (n === 0 ? true : odd(n - 1));
const odd = (n: number): boolean => (n === 0 ? false : even(n - 1));
console.log("even10=" + even(10) + " odd7=" + odd(7));
}
main();node 26.5.1 : fact=120 / off=off-called / sub2=unsubbed / multi=b/a / fib=55 / even10=true odd7=true
perry : fact=120 / off=off-called / sub2=unsubbed / multi=b/a / fib=55
ReferenceError: Cannot access undefined before initialization
at even (<anonymous>)
at main (<anonymous>)
even(10) runs after both even and odd have been initialized, so there is no TDZ window — node returns true. Perry reads odd's forward-declared box and still finds the TDZ sentinel there.
Two defects, one line
- The TDZ sentinel is not cleared (or the wrong slot is read) for a forward-captured
let/constunder this combination, so a legal post-initialization read throws. - The message names
undefinedinstead of the binding. Node saysCannot access 'odd' before initialization. Whatever formats this message is not getting the name, which makes every instance of this error hard to attribute — worth fixing independently of (1).
What it takes to trigger
Deleting the const off = ev.on(() => off()) line makes it pass, so a binding whose own initializer closes over it is necessary. It is not sufficient: off plus the mutual pair alone passes, and so does off plus any single one of fact / fib / the multi-declarator / sub2 plus the pair. The failure needs the fuller combination above — likely a capacity or ordering effect in the per-scope forward-registration bookkeeping (pre_register_forward_captured_lets / lexical_forward_decls / tdz_forward_ids in crates/perry-hir/src/lower_decl/block.rs) rather than any one shape.
Found while validating #9720 (which is a different bug in the same pre-pass — that one is a missing registration, this one is a registration whose sentinel outlives initialization). #9720 neither causes nor fixes this; both arms of its A/B fail identically here.
Activity
proggeramlug commentedon Sep 5, 2026
Fixed by #9762, landed on
mainvia merge train #9804. The train merged as its own branch, so the close-keyword in #9762 never fired — closing manually. Verified the fix is onmainby patch-id (git cherry), not just by the PR being closed.