Summary
A class whose parent is wired at runtime (RegisterClassParentDynamic — i.e. any
mixin/factory class that does NOT take the const M = Mixin(Base) fast path)
inherits the base's prototype methods but not its instance field
initializers. The field reads undefined.
class Base {
value = "base value";
hello() { return "base hello"; }
}
function ViaConst(B: any) {
const K = class extends B { greet() { return "const"; } };
return K; // returns a LocalGet, not a bare class expr
}
const C = ViaConst(Base);
console.log(new C().hello()); // node: "base hello" perry: "base hello" ✅ methods work
console.log((new C() as any).value);
// node: "base value" perry: undefined ❌
console.log(new C() instanceof Base); // true on both ✅
Same for the other shapes that decline the fast path:
function Greetable(B: any) { return class extends B { greet() { return "hi"; } }; }
function Serializable(B: any) { return class extends B { ser() { return "ser"; } }; }
// composed in one expression — the arg is a Call, not an Ident
const Comp = Serializable(Greetable(Base) as any);
(new Comp() as any).value // node: "base value" perry: undefined ❌
// mixin over a mixin result
const Mid = Greetable(Base);
const Top = Serializable(Mid as any);
(new Top() as any).value // node: "base value" perry: undefined ❌
(new Top() as any).greet() // node: "hi" perry: TypeError: greet is not a function ❌
new Top() instanceof Mid // node: true perry: false ❌
Why the fast-path shape is fine
const M = Mixin(Base) over the canonical
function Mixin(B) { return class extends B {…} } is intercepted in HIR lowering
(crates/perry-hir/src/lower/stmt.rs), which synthesizes a real class with a
static extends Base. Static parents get the field-init inheritance
(apply_field_initializers_recursive), so that shape reports "base value"
correctly — see the matrix in #6355. It's specifically the dynamic parent edge
that doesn't replay the parent's field initializers.
Expected
Instance field initializers of the base should run for a class whose parent edge is
registered dynamically, and the grandparent's methods should resolve through a
two-level dynamic chain.
Notes
Found while fixing #5952 (mixin-factory binding). It is a separate, pre-existing
mechanism — present identically before and after #6355, which deliberately does not
touch it. The #5952 fixture
(test-files/test_gap_5952_mixin_factory_binding.ts) asserts only the cells that
match node today and leaves these out.
Verified on main @ be5ed2bfc and on fix/5952-mixin-factory-binding, byte for
byte identical.
Summary
A class whose parent is wired at runtime (
RegisterClassParentDynamic— i.e. anymixin/factory class that does NOT take the
const M = Mixin(Base)fast path)inherits the base's prototype methods but not its instance field
initializers. The field reads
undefined.Same for the other shapes that decline the fast path:
Why the fast-path shape is fine
const M = Mixin(Base)over the canonicalfunction Mixin(B) { return class extends B {…} }is intercepted in HIR lowering(
crates/perry-hir/src/lower/stmt.rs), which synthesizes a real class with astatic
extends Base. Static parents get the field-init inheritance(
apply_field_initializers_recursive), so that shape reports"base value"correctly — see the matrix in #6355. It's specifically the dynamic parent edge
that doesn't replay the parent's field initializers.
Expected
Instance field initializers of the base should run for a class whose parent edge is
registered dynamically, and the grandparent's methods should resolve through a
two-level dynamic chain.
Notes
Found while fixing #5952 (mixin-factory binding). It is a separate, pre-existing
mechanism — present identically before and after #6355, which deliberately does not
touch it. The #5952 fixture
(
test-files/test_gap_5952_mixin_factory_binding.ts) asserts only the cells thatmatch node today and leaves these out.
Verified on
main@be5ed2bfcand onfix/5952-mixin-factory-binding, byte forbyte identical.