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/const under this combination, so a legal post-initialization read throws.
- The message names
undefined instead of the binding. Node says Cannot 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.
A forward reference between two mutually recursive
constarrows throwsReferenceError: Cannot access undefined before initializationat call time, long after both bindings are initialized. Deterministic, 5/5 runs. Not a regression — it reproduces identically on28c292517and on that commit plus #9720.even(10)runs after bothevenandoddhave been initialized, so there is no TDZ window — node returnstrue. Perry readsodd's forward-declared box and still finds the TDZ sentinel there.Two defects, one line
let/constunder this combination, so a legal post-initialization read throws.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:offplus the mutual pair alone passes, and so doesoffplus any single one offact/fib/ the multi-declarator /sub2plus 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_idsincrates/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.