You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(codegen): don't scalar-replace an instance whose chain reaches an unmodeled base (#6343) (#6357)
Escape analysis promoted a non-escaping `new C()` to one alloca per DECLARED
field and inlined the constructor stores. That model is only faithful when
codegen can see the whole construction — and a native base cannot be seen.
`EventEmitter` and the `node:stream` classes install their method surface as
OWN PROPERTIES on the instance at subclass-init time
(`js_object_set_field_by_name(obj, "emit", <native closure>)`), not on a
prototype. A runtime-installed own property has no declared slot, so it was
simply absent from the promoted set:
class X extends EventEmitter { a = 1 }
const x = new X();
x.a // 1 (declared field — promoted)
x.emit // undefined (installed own property — no slot)
Silent wrong answer, no error. A method call on the receiver forces the heap
path and hides it, so the shape that bites is a bare property read next to a
field — exactly what `typeof x.emit` does.
`class_chain_has_unmodeled_base` walks the `extends` chain and disqualifies a
candidate when the chain reaches a base whose construction is opaque:
* `native_extends` — events / node:stream / Web Streams / async_hooks / ws;
found by walking the CHAIN, so `class Leaf extends Mid extends
EventEmitter` is caught too, not just a literal `extends EventEmitter`;
* `extends <expr>` (incl. a lexically shadowed heritage name) — an arbitrary
runtime parent whose constructor can install anything;
* a parent name that resolves to no visible class — a builtin (`Error`,
`Map`, `Set`, …) or an import whose stub never landed.
A cyclic chain fails closed. This is the third member of the family that
already holds #313 (`this`-as-value), #573 (builtin `Error`) and #5872
(dispatch stability), and it is precise for the same reason they are: a chain
of ordinary user classes is fully modeled, so the plain non-escaping class
keeps its scalar replacement and the #945 IR guard's zero-alloc / zero-dispatch
fast path is untouched.
Fixture `test_gap_6343_scalar_native_base_surface.ts` covers EventEmitter,
`Readable`, `Writable`, an indirect chain, and an Error subclass, with
controls for a plain class and a pure user-class chain.
Co-authored-by: Ralph Küpper <ralph3@skelpo.com>
0 commit comments