Skip to content

Dynamic-parent (mixin) class does not inherit the base's instance field initializers #6356

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions