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
A non-capturing function-body class declaration shares one class across evaluations, so a factory's heritage chain collapses (mk(null) === mk(A)) #9502
node : ident false false
gp true
perry: ident true true
gp false
Each call to mk must produce a distinct class whose superclass is the
previous one. Perry produces one class for all three evaluations, so A === B,
and the heritage edge B extends A is unrepresentable — getPrototypeOf(B)
answers with whatever single heritage the shared template recorded (Object)
instead of A.
Why it happens
A class DECLARATION in a function body only gets a per-evaluation heap class
object when fresh_binding is true — crates/perry-hir/src/lower_decl/body_stmt.rs:
let fresh_binding = has_private_elements
|| !computed_keys.is_empty()
|| (!captured_exprs.is_empty() && !has_static_state);
captured_exprs comes from ctx.lookup_class_captures(...), which sees the
class BODY. A class whose only per-evaluation input is its heritage
expression captures nothing in its body, has no private elements and no
computed keys — so it keeps the shared-template lowering and every evaluation
answers to one class id.
Both neighbouring shapes are already correct, which is what makes this the
narrow remaining case:
Widening the predicate to class.extends_expr.is_some() is the obvious fix and
is a lowering change with a wide blast radius: extends_expr is Some for
a great many ordinary shapes (class X extends _mod.Base {} inside a webpack
factory, aliased heritage, lexically-shadowed parent names — see lower_decl/class_decl.rs), so every function-nested class with a non-static
parent name would start materializing a per-evaluation class object. That is
why #9406 left it alone: it was fixing a SIGSEGV, not re-shaping lowering.
#9406 fixed the crash this shape used to produce (the second evaluation stashed ClassRef(D) against D itself, so super() re-entered D's own constructor
forever). It deliberately did not change identity. Before that fix, getPrototypeOf(B) === A accidentally read true — because the self-edge made getPrototypeOf(B) answer B, and B === A. It now reads false, which is
the honest answer for one collapsed class.
#6465 is the stale tracker for this. It is closed (2026-07-17, when the capturing half landed), but lower_decl/body_stmt.rs still says:
Capturing classes with public static state retain the shared-template path for
now, but private elements always require a fresh evaluation — including
private static state. Prototype identity is still shared per template — the
remaining gap tracked on #6465.
so the code points a reader at a closed issue. That comment should name this
issue instead.
Environment
perrymaster, Node oracle 26.5.1 (.node-version). Reproduces on e284cabc3 (#9406's merge base) and with #9406 applied — it is not a
regression from that PR, and that PR does not fix it.
Fixed by #9753, which landed on main via merge train #9798. The train was merged as its own branch, so the Fixes #9502 keyword in #9753 never fired — closing manually. Verified the fix is on main by patch-id (git cherry), not just by the PR being closed.
Activity
proggeramlug commentedon Sep 5, 2026
Fixed by #9753, which landed on
mainvia merge train #9798. The train was merged as its own branch, so theFixes #9502keyword in #9753 never fired — closing manually. Verified the fix is onmainby patch-id (git cherry), not just by the PR being closed.