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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions