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_name → event_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).
Symptom
An
EventTargetreached through a variable alias and constructed withnewproduces an instance with no EventTarget surface:
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_subclasscovers 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-devbuild, before and after that fix:
new ET()whereconst ET = EventTargetTypeError: EventTarget is not a functionundefinedsurfacefunctionSo #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_valueon the globalThisEventTargetvalue, which is a closure over
global_this_builtin_noop_thunk— it allocates theobject 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_name→event_target_method_bind) has nothing toresolve against.
Adjacent, same shape, worth checking together (older and broader)
Aliasing is not required for these — the plain subclass already misses:
These also threw before #7520 and now return
undefined.EventTargetis theonly construct-only web builtin with a subclass-surface fallback; the others have
none.
native_instance_baseincrates/perry-codegen/src/lower_call/new_helpers.rsis the deliberately-narrowlist of bases whose surface is stamped onto the instance at
super()time(
EventEmitter,Map,Set,Event,CustomEvent,DOMException), and none ofthe above is in it. This is the documented "native base-class subclassing" weak
area in CLAUDE.md.
Repro
Node 26.5.1 (the
.node-versionpin).