Skip to content

gap: 'const ET = EventTarget; new ET()' yields an instance with no EventTarget surface (and the same for subclasses of AbortController/FormData/TextEncoder/DisposableStack/URLSearchParams) #7524

Description

@proggeramlug

Symptom

An EventTarget reached through a variable alias and constructed with new
produces an instance with no EventTarget surface:

const ET = EventTarget;
console.log(typeof ET);                              // node: function   perry: function
console.log(typeof new ET().addEventListener);       // node: function   perry: undefined

The direct forms are all correct — new EventTarget(), class Bus extends EventTarget {}, new BusRef() through a class alias, Reflect.construct(Bus, []),
a class expression, an fn-local subclass. test_gap_6301_event_target_subclass
covers those and passes. The alias-of-the-builtin shape is the one gap.

Found while closing #7518

Not a regression from #7520 — measured on the same host, same --profile perry-dev
build, before and after that fix:

before #7520 after #7520 node
new ET() where const ET = EventTarget threw TypeError: EventTarget is not a function undefined surface function

So #7520 turned a throw into a silent wrong answer for this shape, which is
strictly better and matches the pre-#6853 behaviour, but it is still not Node.

The construct goes through js_native_call_value on the globalThis EventTarget
value, which is a closure over global_this_builtin_noop_thunk — it allocates the
object but never stamps the EventTarget class id or attaches the listener bag, so
the #6301 prototype-chain fallback in js_native_call_method
(is_event_target_method_nameevent_target_method_bind) has nothing to
resolve against.

Adjacent, same shape, worth checking together (older and broader)

Aliasing is not required for these — the plain subclass already misses:

class A extends AbortController {}   console.log(typeof new A().abort);    // node: function  perry: undefined
class B extends FormData {}          console.log(typeof new B().append);   // node: function  perry: undefined
class C extends TextEncoder {}       console.log(typeof new C().encode);   // node: function  perry: undefined
class D extends DisposableStack {}   console.log(typeof new D().use);      // node: function  perry: undefined
class E extends URLSearchParams {}   console.log(typeof new E().get);      // node: function  perry: undefined

These also threw before #7520 and now return undefined. EventTarget is the
only construct-only web builtin with a subclass-surface fallback; the others have
none. native_instance_base in
crates/perry-codegen/src/lower_call/new_helpers.rs is the deliberately-narrow
list of bases whose surface is stamped onto the instance at super() time
(EventEmitter, Map, Set, Event, CustomEvent, DOMException), and none of
the above is in it. This is the documented "native base-class subclassing" weak
area in CLAUDE.md.

Repro

cat > /tmp/aliased_et.ts <<'EOF'
const ET = EventTarget;
console.log("aliased EventTarget typeof:", typeof ET);
console.log("aliased EventTarget instance:", typeof new ET().addEventListener);
EOF
perry /tmp/aliased_et.ts -o /tmp/aliased_et && /tmp/aliased_et
diff <(node --experimental-strip-types /tmp/aliased_et.ts) <(/tmp/aliased_et)

Node 26.5.1 (the .node-version pin).

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