Skip to content

Immediately-constructed class expression loses its builtin parent edge: new (class extends Event/EventTarget {})() is parentless #6336

Description

@proggeramlug

Summary

An immediately-constructed class expressionnew (class extends Base {})() — never registers its Subclass → Base edge in the class registry when Base is a builtin. The instance comes out parentless: instanceof Base is false, and every inherited native method/field reads as undefined.

The same class expression works fine as soon as it's bound to a name first, which is what makes this a registration-timing hole rather than a class-expression hole.

Repro

// ── builtin parent, immediately constructed: BROKEN ──
const ev = new (class extends Event {})("tick");
console.log(ev instanceof Event, ev.type);          // perry: false undefined   | node: true tick

const et = new (class extends EventTarget {})();
console.log(et instanceof EventTarget);             // perry: false             | node: true
console.log(typeof et.addEventListener);            // perry: undefined         | node: function
et.addEventListener("q", () => {});                 // perry: TypeError: value is not a function

// ── same shape, USER parent: works ──
class Base { hello() { return "hi"; } }
const p = new (class extends Base {})();
console.log(p instanceof Base, typeof p.hello);     // perry: true function     | node: true function

// ── builtin parent, bound to a const first: works ──
const K = class extends EventTarget {};
console.log(new K() instanceof EventTarget);        // perry: true              | node: true

Also affected, and worth noting because it's a different symptom of the same area:

const E2 = class extends Event {};
new E2("x");                                        // perry: TypeError: Event is not a function

Expected

All of the above match node --experimental-strip-types: instanceof is true and the inherited surface resolves.

Notes / where to look

  • This is pre-existing and not specific to any one builtin — it reproduces on Event, which long predates the EventTarget work in fix(runtime): EventTarget methods must live on the prototype so subclasses inherit them (#6301) #6311/runtime: class X extends EventTarget doesn't inherit addEventListener/dispatchEvent (root cause of #5931, breaks cac) #6301. Error and Array do not reproduce (they have their own construct paths), which suggests the gap is in how the class-expression lowering resolves a builtin extends target by name.
  • The registry edge for a builtin parent is normally wired at class-definition time by js_register_class_parent_dynamic, which resolves the parent through global_builtin_constructor_class_id (crates/perry-runtime/src/object/instanceof.rs). For a class expression in callee position (new (class … {})()) that definition-time registration appears never to run, so get_parent_class_id(subclass_id) returns nothing and every chain walk (class_chain_is_event_target, is_event_instance, instanceof) bails.
  • Consequence: any feature that identifies a receiver by class chain silently degrades to "not a member of the base" for this shape. That's the shared failure mode behind both the Event and EventTarget rows above.

Found while verifying a review finding on #6311 (EventTarget subclassing); filing separately rather than growing that PR's scope, since it equally affects Event on main.

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