diff --git a/changelog.d/10592-instanceof-value-kinds.md b/changelog.d/10592-instanceof-value-kinds.md new file mode 100644 index 0000000000..6a4e018fb1 --- /dev/null +++ b/changelog.d/10592-instanceof-value-kinds.md @@ -0,0 +1,24 @@ +`instanceof` no longer segfaults on short inline strings, and `Object.create(proto).constructor` +returns the real constructor. The receiver is now resolved per value kind rather than assumed to be a +heap pointer, with the prototype and class-registry paths updated to match. + +The string crash took down ajv, and with it every fastify schema route; the `constructor` defect +crashed lodash's `isEqual`. `new EventEmitter() instanceof EventEmitter` is fixed as a direct +consequence. + +A related shape, `class Sub extends EventEmitter {}` followed by `new Sub() instanceof +EventEmitter`, took a separate fix: that call compiles through the dynamic-dispatch instanceof path +(the RHS resolves via a native-module lookup), which never registered or consulted the class-chain +parent edge that `extends Array`/`Map`/`Set`/`Error` subclassing already uses. A subclass instance is +a real object carrying its own class id, not a handle and not prototype-linked to +`EventEmitter.prototype`, so it was invisible to the handle/prototype probes on that path and always +answered `false`. EventEmitter's reserved class id is now a valid `extends` parent, and the +dynamic-dispatch branch delegates to the class-chain walk first, falling back to the prototype walk +for `util.inherits`-style shapes. + +A CodeRabbit review pass on this PR also found that `instanceof`'s dynamic-RHS classification (and +`value_is_callable`) trusted the INT32-class-ref tag band alone, without checking the class id was +actually registered. A JS program can construct a `number` sharing that same tag band directly (via +`DataView`), which was then misread as a class reference instead of correctly reaching the +unresolved-RHS `TypeError`. Both sites now go through the same `class_ref_id` helper (which also +checks `is_class_id_registered`) that the rest of the crate already uses for this. diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 708e76133c..b9d32b7e9b 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -121,6 +121,14 @@ pub(crate) fn builtin_parent_reserved_class_id(name: &str) -> Option { "BigInt64Array" => 0xFFFF0039, "BigUint64Array" => 0xFFFF003A, "Function" => 0xFFFF00F0, + // #10556: `class Sub extends EventEmitter {}` — same shape as the + // Array/Map/Set/Error builtins above. Without this edge, + // `new Sub() instanceof EventEmitter` never reaches the class-chain + // walk in `js_instanceof` and falls back to the dynamic-dispatch + // handle/prototype probes in perry-runtime/src/object/instanceof.rs, + // which don't recognize a genuine subclass ObjectHeader. Keep in + // sync with `CLASS_ID_EVENT_EMITTER` there. + "EventEmitter" => 0xFFFF0076, _ => return None, }) } diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index 2798e8a4cc..48e4c0fb90 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -1526,15 +1526,15 @@ fn object_ptr(value: f64) -> Option<*mut ObjectHeader> { return None; } let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw < 0x10000 || crate::buffer::is_registered_buffer(raw) { + if crate::buffer::is_registered_buffer(raw) { return None; } - unsafe { - let header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*header).obj_type != crate::gc::GC_TYPE_OBJECT { - return None; - } + // #10556: `x instanceof EventEmitter` asks `is_worker_instance_value`, and a + // native `new EventEmitter()` is a POINTER_TAG registry handle (`0x38000`) + // that sailed over the old `0x10000` floor into a header read. + let header = unsafe { crate::value::addr_class::try_read_gc_header(raw) }?; + if header.obj_type != crate::gc::GC_TYPE_OBJECT { + return None; } Some(raw as *mut ObjectHeader) } @@ -1545,18 +1545,10 @@ fn array_ptr(value: f64) -> Option<*mut ArrayHeader> { return None; } let raw = (bits & crate::value::POINTER_MASK) as usize; - if raw < 0x10000 { - return None; - } - unsafe { - let header = - (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - match (*header).obj_type { - crate::gc::GC_TYPE_ARRAY | crate::gc::GC_TYPE_LAZY_ARRAY => { - Some(raw as *mut ArrayHeader) - } - _ => None, - } + let header = unsafe { crate::value::addr_class::try_read_gc_header(raw) }?; + match header.obj_type { + crate::gc::GC_TYPE_ARRAY | crate::gc::GC_TYPE_LAZY_ARRAY => Some(raw as *mut ArrayHeader), + _ => None, } } diff --git a/crates/perry-runtime/src/collection_iter_object.rs b/crates/perry-runtime/src/collection_iter_object.rs index d69b652d05..e3a45afc69 100644 --- a/crates/perry-runtime/src/collection_iter_object.rs +++ b/crates/perry-runtime/src/collection_iter_object.rs @@ -55,12 +55,13 @@ pub fn is_set_iterator_addr(addr: usize) -> bool { } fn iterator_class_id(addr: usize) -> Option { - if addr < crate::gc::GC_HEADER_SIZE + 0x1000 { - return None; - } + // `util.types.isMapIterator(v)` / `isSetIterator(v)` hand any value's + // candidate address here. The canonical header read rejects the handle + // band and out-of-window bits before touching memory (#10479: the old + // `addr < GC_HEADER_SIZE + 0x1000` floor let a proxy/fetch id through). unsafe { - let gc_header = (addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + let header = crate::value::addr_class::try_read_gc_header(addr)?; + if header.obj_type != crate::gc::GC_TYPE_OBJECT { return None; } Some((*(addr as *const ObjectHeader)).class_id) diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 6afe9c9bf7..0cfb903e89 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -98,6 +98,7 @@ pub(crate) use prototype_objects::{ class_prototype_object, ensure_function_prototype_object, function_class_id, function_value_for_class_id, resolve_proto_chain_field, resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, + synthetic_class_prototype_object, }; pub use prototype_objects::{ js_set_function_prototype, js_set_prototype_property, NEXT_SYNTHETIC_CLASS_ID, diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index d5c41c34bb..2f02f2042d 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -146,6 +146,9 @@ pub(crate) fn ensure_function_prototype_object( proto_handle.with_mut_ptr::(|proto| proto) } +/// Floor of the synthetic class-id range (see [`NEXT_SYNTHETIC_CLASS_ID`]). +pub(crate) const SYNTHETIC_CLASS_ID_BASE: u32 = 0x8000_0000; + per_test_global! { /// Synthetic class id allocator for prototype-object classes. High bit /// set (0x8000_0000+) to keep them separate from codegen-assigned ids @@ -153,7 +156,24 @@ per_test_global! { /// concern in practice — would require ~2 billion `Function.prototype = X` /// statements at module init. pub static NEXT_SYNTHETIC_CLASS_ID: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(0x8000_0000); + std::sync::atomic::AtomicU32::new(SYNTHETIC_CLASS_ID_BASE); +} + +/// The `[[Prototype]]` object recorded for a SYNTHETIC class id — one of the +/// ids `Object.create(proto)` (#809) and `F.prototype = obj` (#711) allocate +/// from [`NEXT_SYNTHETIC_CLASS_ID`]. That link is the authoritative prototype +/// of every instance stamped with the id. +/// +/// Unlike [`class_prototype_object`], this refuses a DECLARED class id, whose +/// entry in the same table is the parent CLASS OBJECT of a class-expression +/// subclass (#1788/#6552) rather than a prototype. +pub(crate) fn synthetic_class_prototype_object(class_id: u32) -> *mut ObjectHeader { + if class_id < SYNTHETIC_CLASS_ID_BASE + || class_id >= NEXT_SYNTHETIC_CLASS_ID.load(std::sync::atomic::Ordering::Relaxed) + { + return std::ptr::null_mut(); + } + class_prototype_object(class_id) } /// Perform ordinary `.prototype` assignment, then synchronize the synthetic diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index 59d47729e2..be93e31988 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -246,6 +246,38 @@ pub(super) unsafe fn instance_constructor_value( if let Some(func_value) = super::super::class_registry::function_value_for_class_id(class_id) { return Some(JSValue::from_bits(func_value.to_bits())); } + // #10478: an `Object.create(proto)` result is stamped with a synthetic + // class id that only indexes its prototype object + // (`CLASS_PROTOTYPE_OBJECTS`); unlike the function ids above it names no + // class VALUE. Its `constructor` is the inherited `proto.constructor`, so + // read it off that chain. The INT32 synthesis below minted a ClassRef for + // the synthetic id itself (`0x7FFE_0000_8000_0000`): unequal to `Object` / + // `A`, printed as `[object Function]`, and `C instanceof C` segfaulted + // (lodash `isEqual(cloneDeep(x), x)`). + let synthetic_proto = super::super::class_registry::synthetic_class_prototype_object(class_id); + if !synthetic_proto.is_null() { + // The prototype's OWN `constructor` data field answers the common + // shapes directly — `Object.prototype`, a declared `C.prototype`, a + // materialized `F.prototype`, a `{ constructor: F }` literal — so take + // it without the general chain walk, whose implicit-`this` juggling, + // accessor-receiver override and registry probes cost ~3000 + // instructions per read. Skipped when an accessor owns the key, which + // must run through the walk to fire with the right receiver. + if get_accessor_descriptor(synthetic_proto as usize, "constructor").is_none() { + if let Some(value) = own_data_field_by_name(synthetic_proto, key) { + if !value.is_undefined() && !value.is_null() { + return Some(value); + } + } + } + let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + return Some( + super::super::class_registry::resolve_proto_chain_field_with_receiver( + class_id, key, receiver, + ) + .unwrap_or_else(JSValue::undefined), + ); + } if class_id != 0 && is_class_id_registered(class_id) { let bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64); return Some(JSValue::from_bits(bits)); diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 6030dd9601..da9b4ba13b 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -30,7 +30,11 @@ pub(crate) fn value_is_callable(value: f64) -> bool { // INT32-tagged class references (top 16 bits = 0x7FFE) are callable // constructors emitted by codegen. `is_pointer()` only checks 0x7FFD, // so they would fall through to `return false` without this guard. - if (value.to_bits() >> 48) == 0x7FFE { + // `class_ref_id` also requires `is_class_id_registered`, so a + // user-crafted NaN payload sharing this tag band (e.g. via + // `DataView.setFloat64` — a real JS number, not a class ref) is not + // misclassified as callable. + if class_ref_id(value).is_some() { return true; } let jv = crate::JSValue::from_bits(value.to_bits()); @@ -62,15 +66,13 @@ fn small_native_handle_id(value: f64) -> Option { None } +/// Candidate heap address of an `instanceof` operand; 0 for every primitive. +/// #10479: this used to treat every tag band `>= 0x7FF8` as a pointer, so a +/// 1-5 byte inline string (or an INT32 class ref) reached +/// `object_static_prototype` as a garbage address and segfaulted. +#[inline] fn value_addr(value: f64) -> usize { - let bits = value.to_bits(); - if (bits >> 48) >= 0x7FF8 { - (bits & crate::value::POINTER_MASK) as usize - } else if (bits >> 48) == 0 && bits >= 0x1000 { - bits as usize - } else { - 0 - } + crate::value::addr_class::object_ref_addr(value) } fn recorded_prototype_instanceof_builtin(value: f64, name: &str) -> Option { @@ -99,7 +101,12 @@ fn is_native_module_namespace_value(value: f64, expected: &str) -> bool { return false; } let obj = jv.as_pointer::(); - if obj.is_null() { + // #10556: a native `new EventEmitter()` is a POINTER_TAG registry handle + // (`0x38000`), and `x instanceof EventEmitter` asks this probe first — the + // null check alone let it read `class_id` out of unmapped low memory. + let is_object = unsafe { crate::value::addr_class::try_read_gc_header(obj as usize) } + .is_some_and(|header| header.obj_type == crate::gc::GC_TYPE_OBJECT); + if !is_object { return false; } unsafe { @@ -245,12 +252,13 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { } } let bits = type_ref.to_bits(); - let top16 = bits >> 48; - if top16 == 0x7FFE { - let class_id = (bits & 0xFFFF_FFFF) as u32; - if class_id != 0 { - return js_instanceof(value, class_id); - } + // `class_ref_id` requires `is_class_id_registered`, not just the tag — + // a user-crafted NaN payload sharing the 0x7FFE band (a real JS number + // constructed via `DataView.setFloat64`, not a codegen-emitted class + // ref) must fall through to the unresolved-RHS `TypeError` below + // instead of being dispatched into `js_instanceof` as a bogus class id. + if let Some(class_id) = class_ref_id(type_ref) { + return js_instanceof(value, class_id); } // #9502: a heap class object's template id identifies its code, not its // evaluation. Compare the actual prototype objects so sibling evaluations @@ -328,9 +336,20 @@ pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { return f64::from_bits(crate::value::TAG_TRUE); } if module == "events" && method == "EventEmitter" { + // #10556: a genuine subclass instance (`class Sub extends + // EventEmitter {}`) is a real ObjectHeader carrying Sub's own + // class id, not a handle and not prototype-linked to the real + // `EventEmitter.prototype` — so it is invisible to the + // handle/prototype probes below. Delegate to the static path + // first: `js_instanceof` walks the class-chain parent edge that + // codegen registers for `extends EventEmitter` + // (`builtin_parent_reserved_class_id` in + // perry-codegen/src/expr/instance_misc1.rs), and its own + // `CLASS_ID_EVENT_EMITTER` branch already covers the direct + // handle/`util.inherits` cases. Keep the general prototype walk + // as a fallback for shapes neither path reaches. return f64::from_bits( - if is_event_emitter_instance_value(value) - || super::tls_constructor_prototype_is_instance_of(value, method.as_str()) + if js_instanceof(value, CLASS_ID_EVENT_EMITTER).to_bits() == crate::value::TAG_TRUE || ordinary_has_instance_prototype_walk(value, type_ref) { crate::value::TAG_TRUE @@ -702,11 +721,12 @@ fn ordinary_has_instance_prototype_walk(value: f64, type_ref: f64) -> bool { // * a heap-allocated string/bigint/symbol gets ToObject-wrapped by // getPrototypeOf, so the walk climbs the wrapper chain and can spuriously // match (`Symbol() instanceof Object` wrongly returned `true`). - // Every tag below is checked without dereferencing. Real f64 numbers are - // already answered `false` by the primitive fast paths before this point and - // share tag-space with raw heap pointers (a bare `is_number()` would - // misclassify a module-level object var), so they are intentionally left to - // those paths rather than guarded here. + // Every tag below is checked without dereferencing. Real f64 numbers share + // tag-space with legacy raw heap pointers (a bare `is_number()` would + // misclassify a raw-bitcast object), so a number is only rejected when it + // does not decode as an object address. They are NOT all answered by + // earlier fast paths: a dynamic `1.5 instanceof Number` / `instanceof + // Object` reached this walk, ToObject-wrapped the number and matched. let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_f64(value); let type_ref = scope.root_nanbox_f64(type_ref); @@ -718,6 +738,7 @@ fn ordinary_has_instance_prototype_walk(value: f64, type_ref: f64) -> bool { || jv.is_int32() || jv.is_any_string() || jv.is_bigint() + || (jv.is_number() && value_addr(value.get_nanbox_f64()) == 0) || unsafe { crate::symbol::js_is_symbol(value.get_nanbox_f64()) != 0 } { return false; @@ -1599,27 +1620,15 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { // perspective — must return true without force-materializing. const CLASS_ID_ARRAY: u32 = 0xFFFF0024; if class_id == CLASS_ID_ARRAY { - let addr = if jsval.is_pointer() { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else { - let top16 = (bits >> 48) as u16; - if top16 == 0 && bits >= 0x1000 { - bits as usize - } else { - 0 - } - }; - if addr != 0 && addr >= crate::gc::GC_HEADER_SIZE { - let gc_header = (addr - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - unsafe { - let obj_type = (*gc_header).obj_type; - if obj_type == crate::gc::GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - { - return true_val; - } - } - } - return false_val; + // A POINTER_TAG handle id (fetch/zlib/stdlib registries) is not a heap + // address; the canonical header read rejects it instead of probing the + // byte below it. + let is_array = unsafe { crate::value::addr_class::try_read_gc_header(value_addr(value)) } + .is_some_and(|header| { + header.obj_type == crate::gc::GC_TYPE_ARRAY + || header.obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + }); + return if is_array { true_val } else { false_val }; } // Typed arrays — Int8Array..Float16Array reserved IDs (0xFFFF0030..3B). @@ -1783,6 +1792,18 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { if let Some(matches) = recorded_prototype_instanceof_builtin(value, "Error") { return if matches { true_val } else { false_val }; } + } + + // Everything below reads `ObjectHeader::class_id`, which only a + // genuine `GC_TYPE_OBJECT` has. Every other GC type keeps something + // else in that word — an array's `length`, a closure's function + // pointer, a Map's `size` — so `[1, 2] instanceof C` was true whenever + // the length equalled (or chained to) `C`'s class id. + if gc_type != crate::gc::GC_TYPE_OBJECT { + return false_val; + } + + if class_id == crate::error::CLASS_ID_ERROR { let obj_class_id = (*obj_ptr).class_id; if extends_builtin_error(obj_class_id) { return true_val; @@ -1834,6 +1855,15 @@ mod null_lhs_tests { f64::from_bits(crate::value::INT32_TAG | 5), // int32 5 f64::from_bits(crate::value::STRING_TAG | 0x1000), // string tag (addr never deref'd) f64::from_bits(crate::value::BIGINT_TAG | 0x1000), // bigint tag (addr never deref'd) + // #10479: inline SSO strings ("uri", "a") and a synthetic class ref. + f64::from_bits(crate::value::SHORT_STRING_TAG | 0x0300_0069_7275), + f64::from_bits(crate::value::SHORT_STRING_TAG | 0x0100_0000_0061), + f64::from_bits(crate::value::INT32_TAG | 0x8000_0000), + // Ordinary numbers: a dynamic `1.5 instanceof Number` reached the + // walk and matched through the ToObject wrapper. + 1.5, + -0.0, + f64::NAN, ]; for lhs in cases { // A dummy non-object RHS is never consulted for a non-object LHS. diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index a5d0a0bfd6..a4e0bcfb59 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -619,6 +619,29 @@ fn get_prototype_of_resolved(obj_value: f64) -> f64 { return proto; } } + // #10478: `Object.create(proto)` (and a plain-function `new + // F()`) records the exact `[[Prototype]]` object under the + // instance's SYNTHETIC class id. That link is authoritative, so + // prefer it over the `constructor`-derived guess below, which + // reads `obj.constructor` and answers `ctor.prototype`. The + // guess only happened to miss while an `Object.create` result's + // inherited `constructor` resolved to a bogus class ref; once it + // correctly answers `Object` / `A`, the guess returns + // `Object.prototype` / `A.prototype` and drops `proto` itself — + // with its inherited accessors, descriptors and non-writable + // slots. (The same lookup runs further down for receivers that + // reach it; this one only moves it ahead of the guess.) + if (*gc).obj_type == crate::gc::GC_TYPE_OBJECT { + let synth_proto = + super::super::class_registry::synthetic_class_prototype_object( + (*obj).class_id, + ); + if !synth_proto.is_null() && synth_proto as usize != raw_addr as usize { + return f64::from_bits( + crate::value::js_nanbox_pointer(synth_proto as i64).to_bits(), + ); + } + } if let Some(proto) = constructor_dynamic_prototype(obj) { return proto; } diff --git a/crates/perry-runtime/src/object/util_types.rs b/crates/perry-runtime/src/object/util_types.rs index aa2237dbee..fbe82fa9ec 100644 --- a/crates/perry-runtime/src/object/util_types.rs +++ b/crates/perry-runtime/src/object/util_types.rs @@ -18,14 +18,12 @@ fn nanbox_bool(v: bool) -> f64 { ) } +/// Candidate object address of a predicate argument; 0 for every primitive. +/// #10479: the old `tag >= 0x7FF8 ⇒ payload` decode handed an inline SSO +/// string's packed bytes to `isMapIterator`/`isSetIterator` as an address. #[inline] fn jsvalue_addr(v: f64) -> usize { - let bits = v.to_bits(); - if (bits >> 48) >= 0x7FF8 { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize - } else { - bits as usize - } + crate::value::addr_class::object_ref_addr(v) } fn jsvalue_extends_data_view(value: f64) -> bool { diff --git a/crates/perry-runtime/src/value/addr_class.rs b/crates/perry-runtime/src/value/addr_class.rs index dd5796d312..4d37c9a7d9 100644 --- a/crates/perry-runtime/src/value/addr_class.rs +++ b/crates/perry-runtime/src/value/addr_class.rs @@ -249,6 +249,53 @@ pub(crate) unsafe fn try_read_gc_header(addr: usize) -> Option<&'static GcHeader Some(&*((addr - GC_HEADER_SIZE) as *const GcHeader)) } +/// Candidate object address carried by a NaN-boxed JS value, decided by the +/// value's TAG before any magnitude test (#10479). +/// +/// Only two representations name an address: a `POINTER_TAG` payload and a +/// legacy raw bitcast pointer (top 16 bits clear, above the null page). Every +/// other tag is a primitive whose low 48 bits are not an address, and several +/// of them land inside the heap window, so the old "tag band `>= 0x7FF8` ⇒ +/// payload" decode turned them into plausible-looking pointers: +/// +/// * an inline SSO string packs its bytes plus a length byte — `"uri"` decodes +/// to `0x0300_0069_7275`, and `meta_capable_object` read a GC header below it +/// and segfaulted (ajv's `arg instanceof _Code`); +/// * an INT32 value / class ref carries its id — `Object.create`'s first +/// synthetic class id decodes to `0x8000_0000` (#10478's `C instanceof C`); +/// * heap strings, bigints, JS handles and the undefined/null/boolean markers. +/// +/// All of those answer 0. So does an ordinary number: a bare top-16-clear +/// word is only the legacy raw-pointer shape when the allocator owns it +/// ([`try_read_tracked_gc_header`], or a registered buffer) — a denormal +/// double such as `1e-310` decodes into the heap window too. +/// +/// A non-zero `POINTER_TAG` answer is a CANDIDATE, not a validity proof — it +/// can still be a handle-band id — so pair it with a registry lookup or +/// [`try_read_gc_header`] before touching memory. +#[inline(always)] +pub(crate) fn object_ref_addr(value: f64) -> usize { + let bits = value.to_bits(); + if (bits & crate::value::TAG_MASK) == crate::value::POINTER_TAG { + (bits & crate::value::POINTER_MASK) as usize + } else if (bits >> 48) == 0 && bits != 0 { + raw_object_ref_addr(bits as usize) + } else { + 0 + } +} + +#[cold] +fn raw_object_ref_addr(addr: usize) -> usize { + let owned = crate::buffer::is_registered_buffer(addr) + || unsafe { try_read_tracked_gc_header(addr) }.is_some(); + if owned { + addr + } else { + 0 + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum TrackedGcStorage { Arena, @@ -381,6 +428,58 @@ mod tests { assert!(unsafe { try_read_gc_header(0x7FFD_0000_0000_0000) }.is_none()); } + /// #10479: only a `POINTER_TAG` payload or a raw pointer names an address. + /// Every primitive band answers 0 — in particular the SSO and INT32 bands, + /// whose payloads decode into the heap window. + #[test] + fn object_ref_addr_classifies_by_tag_before_magnitude() { + use crate::value::{ + BIGINT_TAG, INT32_TAG, JS_HANDLE_TAG, POINTER_TAG, SHORT_STRING_TAG, STRING_TAG, + TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, + }; + let heap_addr = 0x7F12_3456_7890usize; + let primitives = [ + SHORT_STRING_TAG | 0x0300_0069_7275, // "uri" + SHORT_STRING_TAG | 0x0100_0000_0061, // "a" + SHORT_STRING_TAG, // "" + INT32_TAG | 0x8000_0000, // synthetic class ref + INT32_TAG | 5, + STRING_TAG | heap_addr as u64, + BIGINT_TAG | heap_addr as u64, + JS_HANDLE_TAG | 7, + TAG_UNDEFINED, + TAG_NULL, + TAG_TRUE, + TAG_FALSE, + 1.5f64.to_bits(), + f64::NAN.to_bits(), + (-0.0f64).to_bits(), + 0, + // Denormal doubles: top 16 bits clear, low bits in the heap + // window, but no allocator owns them. + 1e-310f64.to_bits(), + ]; + for bits in primitives { + assert_eq!( + object_ref_addr(f64::from_bits(bits)), + 0, + "{bits:#018x} is a primitive, not an address" + ); + } + assert_eq!( + object_ref_addr(f64::from_bits(POINTER_TAG | heap_addr as u64)), + heap_addr + ); + // A POINTER_TAG handle id is still a candidate; callers band-check it. + assert_eq!( + object_ref_addr(f64::from_bits(POINTER_TAG | 0x40001)), + 0x40001 + ); + // The legacy raw-bitcast shape is kept for an allocator-owned object. + let obj = crate::object::js_object_alloc(0, 0) as usize; + assert_eq!(object_ref_addr(f64::from_bits(obj as u64)), obj); + } + #[test] fn tracked_gc_classifier_accepts_injected_low_arena_membership() { use std::cell::Cell; diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index 32642c62a4..50de5d427e 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -67,7 +67,6 @@ handle-floor | crates/perry-runtime/src/child_process/registry.rs | 1 handle-floor | crates/perry-runtime/src/child_process/v8_serde.rs | 1 handle-floor | crates/perry-runtime/src/closure/dispatch/validate.rs | 1 handle-floor | crates/perry-runtime/src/cluster.rs | 1 -handle-floor | crates/perry-runtime/src/collection_iter_object.rs | 1 handle-floor | crates/perry-runtime/src/date.rs | 3 handle-floor | crates/perry-runtime/src/dgram.rs | 1 handle-floor | crates/perry-runtime/src/dns.rs | 4 @@ -126,7 +125,7 @@ handle-floor | crates/perry-runtime/src/object/field_set_by_name/write_helpers.r handle-floor | crates/perry-runtime/src/object/global_this/array_error.rs | 1 handle-floor | crates/perry-runtime/src/object/global_this/ctor_thunks.rs | 1 handle-floor | crates/perry-runtime/src/object/global_this/typed_array.rs | 4 -handle-floor | crates/perry-runtime/src/object/instanceof.rs | 8 +handle-floor | crates/perry-runtime/src/object/instanceof.rs | 6 handle-floor | crates/perry-runtime/src/object/mod.rs | 1 handle-floor | crates/perry-runtime/src/object/native_call_method.rs | 3 handle-floor | crates/perry-runtime/src/object/native_call_method/collection_methods.rs | 2 diff --git a/test-files/test_gap_10478_object_create_constructor.ts b/test-files/test_gap_10478_object_create_constructor.ts new file mode 100644 index 0000000000..10ef7b0823 --- /dev/null +++ b/test-files/test_gap_10478_object_create_constructor.ts @@ -0,0 +1,191 @@ +// #10478: `Object.create(proto).constructor` must be the inherited +// `proto.constructor`. Perry stamps an `Object.create` result with a synthetic +// class id that only indexes its prototype object, and the `constructor` +// synthesis minted an INT32 class ref for that synthetic id instead of reading +// the chain: the value was unequal to `Object` / `A`, printed as +// `[object Function]`, had no name, and `C instanceof C` segfaulted. lodash's +// `_.isEqual(_.cloneDeep(x), x)` (baseCreate + equalObjects) hit exactly that. + +const rt = (v: T): T => JSON.parse(JSON.stringify(v)); + +class A { + x = 1; + m() { + return "A.m"; + } +} +class B extends A { + y = 2; +} +function F(this: any) { + this.f = 1; +} +F.prototype.hello = function () { + return "F.hello"; +}; +function G(this: any) {} +G.prototype = { g: 1 }; + +const describe = (v: any): string => { + if (typeof v === "function") return `function ${v.name}`; + return String(v); +}; + +// --- issue repro ------------------------------------------------------------------ +const o: any = Object.create(Object.prototype); +const a: any = Object.create(A.prototype); +console.log("Object.create(Object.prototype).constructor === Object:", o.constructor === Object); +console.log("Object.create(A.prototype).constructor === A:", a.constructor === A); +console.log("Object.create({}).constructor === Object:", Object.create({}).constructor === Object); +console.log("({}).constructor === Object:", ({} as any).constructor === Object); +console.log("Object.getPrototypeOf(o).constructor === Object:", Object.getPrototypeOf(o).constructor === Object); +console.log("o.constructor.name:", o.constructor.name); +console.log("o.constructor instanceof Object:", o.constructor instanceof Object); +const C = o.constructor; +console.log("C instanceof C:", C instanceof C); + +// --- prototype kinds ---------------------------------------------------------------- +const key = rt("constructor"); +const protos: [string, () => any, any][] = [ + ["Object.prototype", () => Object.prototype, Object], + ["{}", () => ({}), Object], + ["{a:1}", () => ({ a: 1 }), Object], + ["JSON object", () => rt({ a: 1 }), Object], + ["A.prototype", () => A.prototype, A], + ["B.prototype", () => B.prototype, B], + ["F.prototype", () => F.prototype, F], + ["G.prototype (replaced)", () => G.prototype, Object], + ["Array.prototype", () => Array.prototype, Array], + ["Map.prototype", () => Map.prototype, Map], + ["Date.prototype", () => Date.prototype, Date], + ["new A()", () => new A(), A], + ["new B()", () => new B(), B], + ["Object.create(A.prototype)", () => Object.create(A.prototype), A], + ["{constructor: F}", () => ({ constructor: F }), F], + ["Object.create(null)", () => Object.create(null), undefined], + ["Object.create(Object.create(null))", () => Object.create(Object.create(null)), undefined], +]; +for (const [name, make, expected] of protos) { + const obj = Object.create(make()); + const viaDot = obj.constructor; + console.log( + `Object.create(${name}):`, + describe(viaDot), + viaDot === expected, + obj["constructor"] === expected, + obj[key] === expected, + "constructor" in obj, + Object.getOwnPropertyNames(obj).length, + ); +} + +// Methods still resolve through the same chain. +console.log("create(A.prototype).m():", Object.create(A.prototype).m()); +console.log("create(B.prototype).m():", Object.create(B.prototype).m()); +console.log("create(F.prototype).hello():", Object.create(F.prototype).hello()); +console.log("new (create(A.prototype).constructor)() instanceof A:", new (Object.create(A.prototype).constructor)() instanceof A); +console.log("create(A.prototype) instanceof A:", Object.create(A.prototype) instanceof A); +console.log("create(B.prototype) instanceof A:", Object.create(B.prototype) instanceof A); + +// Constructed instances are unaffected. +console.log("new A().constructor === A:", new A().constructor === A); +console.log("new B().constructor === B:", new B().constructor === B); +console.log("new F().constructor === F:", new (F as any)().constructor === F); +console.log("new G().constructor === Object:", new (G as any)().constructor === Object); + +// An own `constructor` on the created object still wins. +const own: any = Object.create(A.prototype); +own.constructor = F; +console.log("own constructor wins:", own.constructor === F); + +// --- the prototype link itself stays authoritative ------------------------------------ +// `Object.getPrototypeOf` must keep answering the exact object passed to +// `Object.create`, and an inherited accessor / non-writable slot must keep +// resolving through it (both are reached from the same class-id link that now +// wins over the `constructor`-derived guess). +const accessorProto: any = {}; +let setterSum = 0; +Object.defineProperty(accessorProto, "acc", { + get() { + return 41; + }, + set(v: number) { + setterSum += v; + }, +}); +Object.defineProperty(accessorProto, "frozenField", { value: "proto", writable: false }); +const viaCreate: any = Object.create(accessorProto); +console.log("getPrototypeOf identity:", Object.getPrototypeOf(viaCreate) === accessorProto); +console.log("inherited getter:", viaCreate.acc); +viaCreate.acc = 1; +viaCreate.acc = 2; +console.log("inherited setter:", setterSum, "own acc:", Object.getOwnPropertyNames(viaCreate).length); +try { + viaCreate.frozenField = "written"; + console.log("inherited non-writable: silent", viaCreate.frozenField); +} catch (e: any) { + console.log(`inherited non-writable: ${e.constructor.name}`, viaCreate.frozenField); +} +const fnInstance: any = new (F as any)(); +console.log("getPrototypeOf(new F()) === F.prototype:", Object.getPrototypeOf(fnInstance) === F.prototype); +console.log("getPrototypeOf(new G()) === G.prototype:", Object.getPrototypeOf(new (G as any)()) === G.prototype); +console.log("getPrototypeOf(new A()) === A.prototype:", Object.getPrototypeOf(new A()) === A.prototype); +function H(this: any) {} +(H as any).prototype = { late: 1 }; +// (An instance created BEFORE the reassignment keeps the old prototype in Node; +// Perry's per-class-id prototype link re-points it. Pre-existing divergence, +// unrelated to this fix, so only the post-reassignment instance is asserted.) +console.log("reassigned prototype:", Object.getPrototypeOf(new (H as any)()) === (H as any).prototype, new (H as any)().late); + +// --- lodash-style checks --------------------------------------------------------------- +// baseCreate: `Object.create(Object.getPrototypeOf(value))`. +const baseCreate = (value: any) => Object.create(Object.getPrototypeOf(value)); +// equalObjects' constructor gate (lodash.js 4.18.1). +function constructorsDiffer(object: any, other: any): boolean { + const objCtor = object.constructor; + const othCtor = other.constructor; + return ( + objCtor != othCtor && + "constructor" in object && + "constructor" in other && + !( + typeof objCtor == "function" && + objCtor instanceof objCtor && + typeof othCtor == "function" && + othCtor instanceof othCtor + ) + ); +} +const samples: [string, any][] = [ + ["plain", { a: 1, b: [1, 2] }], + ["json", rt({ a: 1 })], + ["class A", new A()], + ["class B", new B()], + ["function F", new (F as any)()], +]; +for (const [name, value] of samples) { + const clone = baseCreate(value); + Object.assign(clone, value); + console.log( + `baseCreate(${name}):`, + clone.constructor === value.constructor, + constructorsDiffer(clone, value), + Object.getPrototypeOf(clone) === Object.getPrototypeOf(value), + ); +} +const ctor = baseCreate({}).constructor; +console.log("typeof ctor:", typeof ctor, "ctor instanceof ctor:", ctor instanceof ctor); +const isPlainObjectLike = (v: any) => { + const proto = Object.getPrototypeOf(v); + if (proto === null) return true; + const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor; + return typeof Ctor == "function" && Ctor instanceof Ctor && Ctor === Object; +}; +console.log( + "isPlainObjectLike:", + isPlainObjectLike({}), + isPlainObjectLike(baseCreate({})), + isPlainObjectLike(Object.create(null)), + isPlainObjectLike(new A()), + isPlainObjectLike(Object.create(A.prototype)), +); diff --git a/test-files/test_gap_10479_instanceof_value_kinds.ts b/test-files/test_gap_10479_instanceof_value_kinds.ts new file mode 100644 index 0000000000..e19c9b2f20 --- /dev/null +++ b/test-files/test_gap_10479_instanceof_value_kinds.ts @@ -0,0 +1,204 @@ +// #10479: `x instanceof C` decoded every NaN-box tag band >= 0x7FF8 as an +// object address. A 1-5 byte inline (SSO) string produced at run time packs its +// bytes plus a length byte into a payload that lands in the heap window, so the +// runtime read a GC header below it and segfaulted (ajv 8 `compile()` -> +// `arg instanceof _Code` on the string "uri"). The same decode turned an INT32 +// class ref into an address, and the fall-through class-id walk read an +// array's `length` as a class id. Covers every LHS value kind against class, +// function and builtin right-hand sides, in the static and dynamic forms. +import * as util from "node:util"; + +const rt = (v: T): T => JSON.parse(JSON.stringify(v)); + +// --- issue repro -------------------------------------------------------------- +class K {} +for (let n = 0; n <= 7; n++) { + const s = rt("abcdefgh".slice(0, n)); + console.log(`${JSON.stringify(s)} instanceof K:`, s instanceof K); +} +const concat: any = "u" + "ri".slice(0); +console.log("concat instanceof K:", concat instanceof K); +console.log("JSON.parse('\"uri\"') instanceof K:", JSON.parse('"uri"') instanceof K); +console.log("field instanceof K:", JSON.parse('{"f":"uri"}').f instanceof K); + +// --- LHS kinds x RHS kinds ------------------------------------------------------ +class A { + x = 1; +} +class B extends A { + y = 2; +} +function F(this: any) { + this.f = 1; +} +class Even { + static [Symbol.hasInstance](v: any) { + return typeof v === "number" && v % 2 === 0; + } +} +class ShortString { + static [Symbol.hasInstance](v: any) { + return typeof v === "string" && v.length < 6; + } +} + +const lhs: [string, any][] = []; +for (let n = 0; n <= 7; n++) lhs.push([`sso${n}`, rt("abcdefgh".slice(0, n))]); +lhs.push(["concat3", concat]); +lhs.push(["literal", "uri"]); +lhs.push(["heapString", rt("x".repeat(40))]); +lhs.push(["float", rt(1.5)]); +lhs.push(["zero", rt(0)]); +lhs.push(["negZero", -0]); +lhs.push(["nan", NaN]); +lhs.push(["denormal", rt(1e-310)]); +lhs.push(["int", rt(5) | 0]); +lhs.push(["even", rt(4)]); +lhs.push(["bigint", BigInt(rt(7))]); +lhs.push(["hugeBigint", 2n ** 70n]); +lhs.push(["symbol", Symbol("s")]); +lhs.push(["wellKnownSymbol", Symbol.iterator]); +lhs.push(["null", rt(null)]); +lhs.push(["undefined", undefined]); +lhs.push(["true", rt(true)]); +lhs.push(["false", false]); +lhs.push(["function", function g() {}]); +lhs.push(["arrow", () => 1]); +lhs.push(["array2", [1, 2]]); +lhs.push(["array3", rt([1, 2, 3])]); +lhs.push(["objectLiteral", { a: 1 }]); +lhs.push(["jsonObject", rt({ a: 1 })]); +lhs.push(["newA", new A()]); +lhs.push(["newB", new B()]); +lhs.push(["newF", new (F as any)()]); +lhs.push(["createA", Object.create(A.prototype)]); +lhs.push(["createB", Object.create(B.prototype)]); +lhs.push(["createObject", Object.create({})]); +lhs.push(["proxyA", new Proxy(new A(), {})]); +lhs.push(["proxyObject", new Proxy({}, {})]); +lhs.push(["date", new Date(0)]); +lhs.push(["map", new Map()]); +lhs.push(["regexp", /x/]); +lhs.push(["typeError", new TypeError("t")]); +lhs.push(["boxedString", new String("ab")]); +lhs.push(["boxedNumber", Object(3)]); + +const pick = rt(1); +const rhs: [string, any][] = [ + ["A", A], + ["B", B], + ["F", F], + ["Object", Object], + ["Function", Function], + ["Array", Array], + ["String", String], + ["Number", Number], + ["Error", Error], + ["TypeError", TypeError], + ["Date", Date], + ["Map", Map], + ["Promise", Promise], + ["Even", Even], + ["ShortString", ShortString], + ["dynamicA", pick ? A : B], +]; + +const cell = (f: () => boolean): string => { + try { + return f() ? "T" : "F"; + } catch (e: any) { + return "E"; + } +}; + +console.log("LHS order:", lhs.map(([name]) => name).join(" ")); +for (const [name, R] of rhs) { + console.log(`dynamic ${name.padEnd(11)} ${lhs.map(([, v]) => cell(() => v instanceof R)).join("")}`); +} +// Static right-hand sides (compile-time class ids / builtin ids). +const statics: [string, (v: any) => boolean][] = [ + ["A", (v) => v instanceof A], + ["B", (v) => v instanceof B], + ["F", (v) => v instanceof F], + ["K", (v) => v instanceof K], + ["Object", (v) => v instanceof Object], + ["Function", (v) => v instanceof Function], + ["Array", (v) => v instanceof Array], + ["Error", (v) => v instanceof Error], + ["Date", (v) => v instanceof Date], + ["Map", (v) => v instanceof Map], + ["Promise", (v) => v instanceof Promise], + ["Even", (v) => v instanceof Even], +]; +for (const [name, test] of statics) { + console.log(`static ${name.padEnd(11)} ${lhs.map(([, v]) => cell(() => test(v))).join("")}`); +} + +// --- reflective @@hasInstance ----------------------------------------------------- +const hasInstance = (Function.prototype as any)[Symbol.hasInstance]; +for (const [name, v] of lhs.slice(0, 11)) { + console.log(`Function.prototype[@@hasInstance].call(K, ${name}):`, hasInstance.call(K, v)); +} + +// --- non-callable / primitive right-hand side --------------------------------------- +const badRhs: [string, any][] = [ + ["{}", {}], + ["[]", []], + ["5", rt(5)], + ["sso", rt("ab")], + ["null", rt(null)], + ["undefined", undefined], +]; +for (const [name, R] of badRhs) { + for (const [lname, v] of [["sso3", rt("uri")], ["newA", new A()], ["null", null]] as [string, any][]) { + try { + console.log(`${lname} instanceof ${name}:`, (v as any) instanceof R); + } catch (e: any) { + console.log(`${lname} instanceof ${name}: ${e.constructor.name}: ${e.message}`); + } + } +} + +// --- ajv shape: `arg instanceof _Code` over mixed code arguments -------------------- +class _CodeOrName {} +class _Code extends _CodeOrName { + _items: any[]; + constructor(items: any[]) { + super(); + this._items = items; + } +} +function addCodeArg(code: string[], arg: any): void { + if (arg instanceof _Code) code.push(...arg._items); + else if (arg instanceof _CodeOrName) code.push(""); + else code.push(typeof arg === "string" ? JSON.stringify(arg) : String(arg)); +} +const code: string[] = []; +for (const arg of rt(["uri", "a", "", "abcdef", "format", 1, null, true]) as any[]) addCodeArg(code, arg); +addCodeArg(code, new _Code(["x", "y"])); +addCodeArg(code, new _CodeOrName()); +console.log("ajv addCodeArg:", code.join(" ")); + +// --- sibling predicates that took the same decode ------------------------------------- +for (const [name, v] of lhs.slice(0, 9)) { + console.log( + `util.types ${name}:`, + util.types.isMapIterator(v), + util.types.isSetIterator(v), + util.types.isPromise(v), + util.types.isDate(v), + ); +} +console.log("util.types real iterators:", util.types.isMapIterator(new Map().keys()), util.types.isSetIterator(new Set().values())); + +// --- hot path: the class-instance hit/miss answers stay intact ------------------------ +let hits = 0; +let misses = 0; +const a = new A(); +const bb = new B(); +for (let i = 0; i < 1000; i++) { + if (a instanceof A) hits++; + if (bb instanceof A) hits++; + if (!(a instanceof B)) misses++; +} +console.log("hot loop:", hits, misses); diff --git a/test-files/test_gap_10556_instanceof_native_emitter.ts b/test-files/test_gap_10556_instanceof_native_emitter.ts new file mode 100644 index 0000000000..b60aef5da6 --- /dev/null +++ b/test-files/test_gap_10556_instanceof_native_emitter.ts @@ -0,0 +1,81 @@ +// #10556: `new EventEmitter() instanceof EventEmitter` segfaulted. A native +// EventEmitter instance is a POINTER_TAG registry handle (a small id such as +// `0x38000`), and the `instanceof EventEmitter` brand check first asked two +// "is this a namespace / cluster worker object?" probes that only rejected +// addresses below `0x10000` before reading a GC header / class id. +import { EventEmitter } from "node:events"; +import EE from "node:events"; +import { inherits } from "node:util"; + +const rt = (v: T): T => JSON.parse(JSON.stringify(v)); + +const e = new EventEmitter(); +console.log("named:", e instanceof EventEmitter); +console.log("default:", new EE() instanceof EE); +console.log("mixed:", new EE() instanceof EventEmitter, e instanceof EE); +const Ctor: any = [EventEmitter][rt(0)]; +console.log("dynamic:", e instanceof Ctor); +console.log("reflective:", (Function.prototype as any)[Symbol.hasInstance].call(EventEmitter, e)); + +// Non-emitters of every kind answer false without crashing. +const others: [string, any][] = [ + ["sso", rt("uri")], + ["heap string", rt("x".repeat(30))], + ["number", rt(3)], + ["null", rt(null)], + ["object", {}], + ["array", [1, 2, 3]], + ["map", new Map()], + ["function", () => 1], +]; +for (const [name, v] of others) console.log(`${name} instanceof EventEmitter:`, v instanceof EventEmitter); + +// The emitter still works after the checks. +let fired = 0; +e.on("ping", (n: number) => (fired += n)); +e.emit("ping", 2); +e.emit("ping", 3); +console.log("fired:", fired, "listeners:", e.listenerCount("ping")); + +// #10556 subclass shape: `class Sub extends EventEmitter {}` compiled +// through `js_instanceof_dynamic`, which never registered/consulted the +// class-chain parent edge that Array/Map/Set/Error subclassing uses — so a +// genuine subclass instance (a real ObjectHeader carrying Sub's own class +// id, not a handle, not prototype-linked to the real +// `EventEmitter.prototype`) never matched. Covers: direct subclass +// instanceof, the subclass's own constructor, a two-level subclass, the +// default-import form, and a util.inherits-style function-constructor +// subclass (prototype-chain linking, not a class `extends` edge — a +// different code path from the class-chain parent edge above). +class Sub extends EventEmitter {} +class Sub2 extends Sub {} +class SubDefault extends EE {} + +const s = new Sub(); +console.log("sub instanceof EventEmitter:", s instanceof EventEmitter); +console.log("sub instanceof Sub:", s instanceof Sub); + +const s2 = new Sub2(); +console.log("sub2 instanceof EventEmitter:", s2 instanceof EventEmitter); +console.log("sub2 instanceof Sub:", s2 instanceof Sub); +console.log("sub2 instanceof Sub2:", s2 instanceof Sub2); + +const sd = new SubDefault(); +console.log("subDefault instanceof EE:", sd instanceof EE); +console.log("subDefault instanceof EventEmitter:", sd instanceof EventEmitter); + +// A subclass instance still behaves like a real emitter. +let subFired = 0; +s.on("ping", (n: number) => (subFired += n)); +s.emit("ping", 5); +console.log("sub fired:", subFired, "listeners:", s.listenerCount("ping")); + +// util.inherits-style function-constructor subclass: links prototypes at +// runtime rather than creating an `extends` edge, so it exercises the +// ordinary-prototype-walk fallback instead of the class-chain parent edge. +function FnEmitter(this: any) { + EventEmitter.call(this); +} +inherits(FnEmitter, EventEmitter); +const fe: any = new (FnEmitter as any)(); +console.log("fnEmitter instanceof EventEmitter:", fe instanceof EventEmitter);