diff --git a/changelog.d/10640-instanceof-classexprfresh-shared-id.md b/changelog.d/10640-instanceof-classexprfresh-shared-id.md new file mode 100644 index 0000000000..8aa6dc9ede --- /dev/null +++ b/changelog.d/10640-instanceof-classexprfresh-shared-id.md @@ -0,0 +1,18 @@ +### Fixed + +- `instanceof` against a `ClassExprFresh` parent (a heritage-carrying class + expression — captures, statics, private elements, or a self-binding) + resolved the dynamic parent by the SHARED template `class_id` rather than + per evaluation. An instance built from an EARLIER evaluation of a + repeatedly-evaluated factory, constructed after a LATER evaluation of the + same factory had run, constructed with the correct parent (a prior fix, + #9364/#6438, already gives each evaluation its own pinned heritage for + `super()`/capture resolution) but could fail `instanceof` against its own + true parent — the later evaluation's parent shadowed it in the shared, + last-write-wins `CLASS_REGISTRY` that `instanceof`'s class-chain walk read. + Fixed by pinning the constructing class object onto each new instance too + (`class_registry/evaluation_heritage.rs`) and giving `instanceof`'s chain + walk a value-aware path (`instanceof.rs`'s `class_chain_reaches_dynamic`) + that prefers a pinned VALUE at each hop over the shared class_id table, + gated behind a monotone latch so the common (never-evaluated-twice) case + is unaffected. Runtime-only; no codegen changes. (#10624) diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 6286afc168..35b5804f2e 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -1183,6 +1183,19 @@ pub(crate) unsafe fn replay_class_object_constructor( let scope = crate::gc::RuntimeHandleScope::new(); let classobj_handle = scope.root_nanbox_f64(classobj_value); let inst_handle = scope.root_raw_mut_ptr(inst); + // #10624: remember which specific evaluation of this template built + // `inst`, so a later `instanceof` check against it can walk THAT + // evaluation's own pinned heritage instead of the shared, possibly + // since-overwritten class_id registry (`object/instanceof.rs`'s + // `class_chain_reaches_dynamic`). Runs before anything below can + // allocate/collect and return early, so `inst` is pinned regardless of + // which path this replay takes. + inst_handle.with_mut_ptr::(|inst| { + super::class_registry::pin_instance_constructing_class( + inst, + classobj_handle.get_nanbox_f64(), + ); + }); // Spec: a derived class with no own `constructor` gets the implicit // `constructor(...args) { super(...args) }` — the nearest ancestor's ctor // must run with the same argument list. `lookup_class_constructor` holds diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 85557855b5..e43d06b822 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -53,7 +53,8 @@ pub mod decl_prototype_table; mod dispatch; pub(crate) mod evaluation_heritage; pub(crate) use evaluation_heritage::{ - active_class_evaluation_parent, is_self_heritage_value, push_active_class_evaluation, + active_class_evaluation_parent, instance_pinned_constructing_class, is_self_heritage_value, + pin_instance_constructing_class, push_active_class_evaluation, }; mod function_prototype; mod gc_roots; @@ -217,8 +218,8 @@ pub(crate) use parent_static::{ class_own_symbol_method, class_private_instance_getter_value, class_private_instance_setter_apply, class_static_accessor_getter_value, class_static_accessor_setter_apply, class_symbol_getter_value, class_symbol_setter_apply, - get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, - register_class, register_class_dynamic_static_accessor, + dynamic_value_class_id, get_parent_class_id, lookup_class_symbol_method_in_chain, + lookup_static_method_in_chain, register_class, register_class_dynamic_static_accessor, }; pub use parent_static::{ is_class_object_ptr, is_class_object_value, is_registered_class_prototype_object, diff --git a/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs b/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs index d882ba2d72..303b3e5dac 100644 --- a/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs +++ b/crates/perry-runtime/src/object/class_registry/evaluation_heritage.rs @@ -152,6 +152,78 @@ pub(crate) fn is_self_heritage_value(class_id: u32, parent_bits: u64) -> bool { parent_bits & 0xFFFF_0000_0000_0000 == INT32_TAG && parent_bits as u32 == class_id } +/// #10624: monotone "has any class object ever pinned its own heritage" +/// flag. `js_class_object_pin_parent` arms it before its own write, so +/// anything it can EVER make true (an instance pinned to its constructing +/// class object, or a class_id that has more than one live per-evaluation +/// parent) is only reachable once this is armed. `instanceof`'s value-aware +/// chain walk (`object/instanceof.rs`'s `class_chain_reaches_dynamic`) is +/// gated on it: the overwhelming majority of programs never evaluate a +/// heritage-carrying class expression more than once, and this keeps that +/// case exactly as cheap as it was before this fix (one relaxed-cost atomic +/// load) instead of paying a table lookup at every hop of every +/// `instanceof` check. See `registry_latch.rs`. +pub(crate) static CLASS_OBJECT_HERITAGE_PIN_LATCH: crate::registry_latch::RegistryLatch = + crate::registry_latch::RegistryLatch::new(); + +/// Own-property key under which a genuine INSTANCE (constructed via +/// `new ()`) remembers which SPECIFIC evaluation +/// built it (#10624). +/// +/// `js_class_object_pin_parent`'s pin lives on the CLASS OBJECT and answers +/// "what is MY parent" — `super()`, the prototype chain, and capture +/// resolution above all already consult it. Nothing, though, gave the +/// resulting INSTANCE a way back to that same evaluation: an instance +/// carries only its class's shared TEMPLATE `class_id` +/// (`ObjectHeader.class_id`), identical for every evaluation of the same +/// factory. `instanceof`'s class-chain walk therefore fell back to +/// `CLASS_REGISTRY`/`get_parent_class_id` — the same last-write-wins table +/// `super()` used to read before #9364 — so an instance built from an +/// EARLIER evaluation, checked after a LATER evaluation of the same +/// template has run, could construct correctly (via the class-object pin +/// above) yet fail `instanceof` against its own true parent (the later +/// evaluation's parent shadows it in that shared table). +/// +/// Pinning the constructing class object onto the instance too closes that +/// gap: `object/instanceof.rs`'s `class_chain_reaches_dynamic` walks from +/// THIS value, following the exact same per-evaluation pin chain +/// `pinned_class_object_for_ancestor` already walks for capture resolution, +/// instead of the shared class_id table. +pub(crate) const INSTANCE_CONSTRUCTING_CLASS_KEY: &str = "__perry_ctor_class_object"; + +/// Pin the per-evaluation class OBJECT that is about to construct `inst` +/// onto `inst` itself (#10624). A no-op when `classobj_value` is not itself +/// a per-evaluation class object, or carries no heritage of its own to +/// disambiguate — an ordinary class DECLARATION (or a heritage-less class +/// expression) has none of the ambiguity this exists to resolve, and the +/// plain class_id registry is already exact for those. +pub(crate) fn pin_instance_constructing_class(inst: *mut ObjectHeader, classobj_value: f64) { + if inst.is_null() || !is_class_object_value(classobj_value) { + return; + } + let class_ptr = crate::value::js_nanbox_get_pointer(classobj_value) as *const ObjectHeader; + if class_ptr.is_null() || class_object_pinned_parent(class_ptr).is_none() { + return; + } + // `js_class_object_pin_parent` already armed `CLASS_OBJECT_HERITAGE_PIN_LATCH` + // before writing `class_ptr`'s own pin above (the ordering rule in + // `registry_latch.rs`) — that write happens-before this one in this + // thread's program order, so the latch is already armed here. + let key_bytes = INSTANCE_CONSTRUCTING_CLASS_KEY.as_bytes(); + let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); + crate::object::js_object_set_field_by_name(inst, key, classobj_value); +} + +/// Read back the pin [`pin_instance_constructing_class`] wrote, or `None` +/// when `obj` was never pinned — including the common case where the latch +/// alone already answers "no" without scanning `obj`'s own fields at all. +pub(crate) fn instance_pinned_constructing_class(obj: *const ObjectHeader) -> Option { + if CLASS_OBJECT_HERITAGE_PIN_LATCH.is_idle() { + return None; + } + class_object_own_field_bytes(obj, INSTANCE_CONSTRUCTING_CLASS_KEY.as_bytes()) +} + #[cfg(test)] #[path = "evaluation_heritage/tests.rs"] mod tests; diff --git a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs index 9be1c09723..8fe6dc2edf 100644 --- a/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs +++ b/crates/perry-runtime/src/object/class_registry/evaluation_heritage/tests.rs @@ -176,3 +176,63 @@ fn an_active_replay_does_not_answer_for_another_class_id() { ); } } + +#[test] +fn sibling_class_objects_of_the_same_template_keep_distinct_pins() { + let _lock = crate::gc::global_side_table_test_lock(); + const TEMPLATE: u32 = 0x0936_40C0; + const FIRST_PARENT: u32 = 0x0936_40C1; + const LAST_PARENT: u32 = 0x0936_40C2; + register(TEMPLATE); + register(FIRST_PARENT); + register(LAST_PARENT); + + let scope = crate::gc::RuntimeHandleScope::new(); + + // First evaluation: pins FIRST_PARENT onto its own class object. + let first_handle = scope.root_raw_mut_ptr(crate::object::js_object_alloc(TEMPLATE, 0)); + first_handle.with_mut_ptr::(|class| { + crate::object::class_registry::js_object_mark_class(class as i64) + }); + js_register_class_parent_dynamic(TEMPLATE, class_ref(FIRST_PARENT)); + first_handle.with_mut_ptr::(|class| { + super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE) + }); + let first_pin = first_handle.with_mut_ptr::(|class| { + super::super::parent_static::class_object_pinned_parent(class as *const crate::ObjectHeader) + }); + assert_eq!( + first_pin.map(|v| v.to_bits()), + Some(class_ref(FIRST_PARENT).to_bits()), + "first evaluation's own pin must be readable immediately after being written", + ); + + // Second evaluation of the SAME template: pins LAST_PARENT onto a + // DIFFERENT class object, overwriting the shared CLASS_DYNAMIC_PARENT_VALUE + // stash for TEMPLATE. + let last_handle = scope.root_raw_mut_ptr(crate::object::js_object_alloc(TEMPLATE, 0)); + last_handle.with_mut_ptr::(|class| { + crate::object::class_registry::js_object_mark_class(class as i64) + }); + js_register_class_parent_dynamic(TEMPLATE, class_ref(LAST_PARENT)); + last_handle.with_mut_ptr::(|class| { + super::super::parent_static::js_class_object_pin_parent(class as i64, TEMPLATE) + }); + + // The EARLIER evaluation's own pin must be UNCHANGED by the later one. + let first_pin_again = first_handle.with_mut_ptr::(|class| { + super::super::parent_static::class_object_pinned_parent(class as *const crate::ObjectHeader) + }); + assert_eq!( + first_pin_again.map(|v| v.to_bits()), + Some(class_ref(FIRST_PARENT).to_bits()), + "an earlier evaluation's pin must survive a LATER sibling evaluation's pin write", + ); + let last_pin = last_handle.with_mut_ptr::(|class| { + super::super::parent_static::class_object_pinned_parent(class as *const crate::ObjectHeader) + }); + assert_eq!( + last_pin.map(|v| v.to_bits()), + Some(class_ref(LAST_PARENT).to_bits()), + ); +} diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index b4813599ad..bedfd46ad9 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -66,6 +66,57 @@ pub extern "C" fn js_register_class_parent(class_id: u32, parent_class_id: u32) } } +/// Resolve a class_id from an arbitrary NaN-boxed runtime VALUE: an INT32 +/// `ClassRef` (the payload IS the class_id, verified registered) or a +/// POINTER-tagged object (its `ObjectHeader.class_id`, falling back to the +/// synthetic class id a plain closure's reassigned `.prototype` was given). +/// `0` for anything else (primitives, an unregistered closure, `undefined`, +/// `null`) — "no answer", never a wrong one. +/// +/// Shared by `js_register_class_parent_dynamic` (deriving the class_id to +/// register a NEW parent edge) and `object/instanceof.rs`'s +/// `class_chain_reaches_dynamic` (#10624, walking an EXISTING +/// per-evaluation pin chain) — both need the identical "what class_id does +/// this value denote" answer. +pub(crate) fn dynamic_value_class_id(value: f64) -> u32 { + let bits = value.to_bits(); + const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; + const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; + let tag = bits & 0xFFFF_0000_0000_0000; + if tag == INT32_TAG { + // ClassRef: lower 32 bits are the class id. Verify it's + // actually a registered class id before trusting it. + let payload = bits as u32; + if payload == 0 { + 0 + } else { + let guard = REGISTERED_CLASS_IDS.read().unwrap(); + match guard.as_ref() { + Some(set) if set.contains(&payload) => payload, + _ => 0, + } + } + } else if tag == POINTER_TAG { + // Object instance: read class_id from the ObjectHeader. + let ptr = crate::value::js_nanbox_get_pointer(value) as *const ObjectHeader; + let from_obj = js_object_get_class_id(ptr); + if from_obj != 0 { + from_obj + } else { + // Issue #711 part 2: the value might be a closure whose + // `.prototype` was assigned to an object via the + // `function Base() {}; Base.prototype = X` pattern. Look + // up the synthetic class id assigned at + // `js_set_function_prototype` time. Returns 0 if the + // closure has no registered prototype object — falls + // through to the parentless baseline. + function_class_id(value) + } + } else { + 0 + } +} + /// Issue #711: dynamic parent-class registration for /// `class X extends fn(...)` shapes where the parent class_id is only /// known at runtime. Called from codegen-emitted module-init code at @@ -239,41 +290,9 @@ pub extern "C" fn js_register_class_parent_dynamic(class_id: u32, mut parent_val let bits = parent_value.to_bits(); let tag = bits & 0xFFFF_0000_0000_0000; - const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; - let parent_cid: u32 = if tag == INT32_TAG { - // ClassRef: lower 32 bits are the class id. Verify it's - // actually a registered class id before trusting it. - let payload = bits as u32; - if payload == 0 { - 0 - } else { - let guard = REGISTERED_CLASS_IDS.read().unwrap(); - match guard.as_ref() { - Some(set) if set.contains(&payload) => payload, - _ => 0, - } - } - } else if tag == POINTER_TAG { - // Object instance: read class_id from the ObjectHeader. - let ptr = crate::value::js_nanbox_get_pointer(parent_value) as *const ObjectHeader; - let from_obj = js_object_get_class_id(ptr); - if from_obj != 0 { - from_obj - } else { - // Issue #711 part 2: the value might be a closure whose - // `.prototype` was assigned to an object via the - // `function Base() {}; Base.prototype = X` pattern. Look - // up the synthetic class id assigned at - // `js_set_function_prototype` time. Returns 0 if the - // closure has no registered prototype object — falls - // through to the parentless baseline. - function_class_id(parent_value) - } - } else { - 0 - }; + let parent_cid: u32 = dynamic_value_class_id(parent_value); if parent_cid != 0 && parent_cid != class_id { register_class(class_id, parent_cid); @@ -355,6 +374,12 @@ pub extern "C" fn js_class_object_pin_parent(obj: i64, template_class_id: u32) { if parent.to_bits() == TAG_UNDEFINED { return; } + // #10624: arm BEFORE the write it advertises (the ordering rule in + // `registry_latch.rs`) — everything the latch gates (this own-property + // write, and `pin_instance_constructing_class`'s later instance pin, + // which never fires without this one already having happened) follows + // in this thread's program order. + super::evaluation_heritage::CLASS_OBJECT_HERITAGE_PIN_LATCH.arm(); let key_bytes = CLASS_OBJECT_PARENT_KEY.as_bytes(); let key = crate::string::js_string_from_bytes(key_bytes.as_ptr(), key_bytes.len() as u32); crate::object::js_object_set_field_by_name( diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index 246fc28b84..393fa36466 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -1651,6 +1651,8 @@ pub(crate) fn is_internal_runtime_key_bytes(b: &[u8]) -> bool { b == crate::object::map_set_subclass::BACKING_KEY || b == crate::weakref::WEAK_ENTRIES_KEY || b == crate::object::parent_static::CLASS_OBJECT_PARENT_KEY.as_bytes() + || b == crate::object::class_registry::evaluation_heritage::INSTANCE_CONSTRUCTING_CLASS_KEY + .as_bytes() || b == b"__perry_ctor_caps" || is_class_capture_key(b) || b.starts_with(crate::node_stream::NATIVE_BASE_SUPER_PREFIX) diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index da9b4ba13b..6a638a6fe9 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -18,6 +18,12 @@ const CLASS_ID_CRYPTO_KEY: u32 = 0xFFFF00C2; /// `value instanceof Function` reserved id (see `js_instanceof`). const CLASS_ID_FUNCTION: u32 = 0xFFFF00F0; +mod dynamic_dispatch; +mod static_dispatch; + +pub use dynamic_dispatch::js_instanceof_dynamic; +pub use static_dispatch::js_instanceof; + /// Whether `value` is callable — the predicate behind `x instanceof Function` /// and `Function[Symbol.hasInstance]`. Covers every Perry function /// representation: heap closures (declarations / expressions / arrows / @@ -169,404 +175,6 @@ fn builtin_ctor_class_id_from_value(type_ref: f64) -> Option { Some(class_id) } -#[no_mangle] -pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - // `proxy instanceof C` uses the proxy's `[[GetPrototypeOf]]`, which (absent a - // trap) forwards to the target — so it is equivalent to `target instanceof - // C`. The proxy itself is a small registered id with no class chain, so - // without this it always returned false. Unwrap nested proxies (drizzle - // aliases columns as `new Proxy(column, …)` and its `is(value, type)` brand - // check relies on `value instanceof type`). Bounded to guard a cycle. - let mut value = value; - { - let mut depth = 0; - while depth < 16 && crate::proxy::js_proxy_is_proxy(value) != 0 { - value = crate::proxy::js_proxy_target(value); - depth += 1; - } - } - // `temporalValue instanceof Temporal.` — Temporal values dispatch via - // brand arms (not a real prototype chain), so resolve the constructor to - // its kind and compare against the value's brand. A non-Temporal value, or - // a Temporal value of a different kind, yields `false`. - if let Some(kind) = super::global_this::temporal_ctor_kind(type_ref) { - if crate::temporal::temporal_kind(value) == Some(kind) { - return f64::from_bits(crate::value::TAG_TRUE); - } - // `class X extends Temporal.` instance: a plain heap object whose - // [[Prototype]] chain reaches `Temporal..prototype`. It carries - // the brand via a stashed cell rather than the Temporal-cell tag, so - // recover that cell and compare its kind. The receiver reaches here both - // NaN-boxed (top16 == 0x7FFD) and as a raw-I64 heap pointer (top16 == 0, - // how module-level object vars are stored) — accept both. (#5587) - #[cfg(feature = "temporal")] - { - let bits = value.to_bits(); - let top16 = bits >> 48; - let raw = if top16 == 0x7FFD { - (bits & crate::value::POINTER_MASK) as usize - } else if top16 == 0 { - bits as usize - } else { - 0 - }; - if raw != 0 { - if let Some(cell) = unsafe { crate::object::temporal_subclass_cell(raw) } { - if crate::temporal::temporal_kind(cell) == Some(kind) { - return f64::from_bits(crate::value::TAG_TRUE); - } - } - } - } - return f64::from_bits(TAG_FALSE); - } - // Spec step (InstanceofOperator): an OWN user-defined `@@hasInstance` - // overrides even native constructor brand checks. The native generic hook - // lives on Function.prototype, so the own-property gate distinguishes an - // explicit override from that inherited default without recursion. - { - let hi_sym = crate::symbol::well_known_symbol("hasInstance"); - if !hi_sym.is_null() { - let hi_f64 = f64::from_bits(crate::value::JSValue::pointer(hi_sym as *const u8).bits()); - if unsafe { crate::symbol::js_object_has_own_symbol(type_ref, hi_f64) } { - let cb = unsafe { crate::symbol::js_object_get_symbol_property(type_ref, hi_f64) }; - if let HasInstanceOutcome::Result(result) = dispatch_own_has_instance(cb, value) { - return result; - } - } - } - } - // Native http(s).Agent handles have no heap prototype chain. After any own - // override above has had first refusal, retain their native brand check. - if let Some((module, method)) = unsafe { bound_native_callable_module_and_method(type_ref) } { - if matches!(module.as_str(), "http" | "https") && method == "Agent" { - let matched = small_native_handle_id(value) - .zip(crate::object::http_agent_handle_probe()) - .is_some_and(|(handle, probe)| unsafe { probe(handle) }); - return f64::from_bits(if matched { - crate::value::TAG_TRUE - } else { - TAG_FALSE - }); - } - } - let bits = type_ref.to_bits(); - // `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 - // remain distinct and a chain through earlier evaluations still matches. - if is_class_object_value(type_ref) { - // Static/forward `new C()` sites can still construct by template id - // without attaching an evaluated prototype. Retain that representation's - // class-id check; recorded individual chains are authoritative. - if !super::prototype_chain::object_has_prototype_divergence(value_addr(value)) { - let obj = crate::JSValue::from_bits(bits).as_pointer::(); - return js_instanceof(value, js_object_get_class_id(obj)); - } - return f64::from_bits(if ordinary_has_instance_prototype_walk(value, type_ref) { - crate::value::TAG_TRUE - } else { - TAG_FALSE - }); - } - // A builtin constructor held in a VARIABLE — `const RS = ReadableStream; body - // instanceof RS` — arrives here as the ClosureHeader-backed function installed - // on `globalThis`, so none of the class-id paths above match and the prototype - // walk below returns false. Codegen only special-cases the *static identifier* - // form (`body instanceof ReadableStream`), where it hands the builtin class id - // straight to `js_instanceof`, which brand-checks these natively-backed values - // via the stream / fetch kind probes (their instances are handles, not heap - // objects with a real prototype chain). - // - // Minified bundles almost always alias constructors into locals, so the - // variable form is the common one in the wild: `x instanceof ` for - // ReadableStream / Response / Headers silently returned `false` while Node - // returns `true`. That made a large esbuild-bundled CLI app mis-detect its - // `fetch()` body, throw "The first argument must be a Readable, a - // ReadableStream, or an async iterable", and abort its background - // tar-stream downloads entirely. - // - // Recover the builtin's name from the constructor closure (recorded by - // `set_bound_native_closure_name` when globalThis is populated) and reuse the - // static path's class id, so both spellings agree. - if let Some(class_id) = builtin_ctor_class_id_from_value(type_ref) { - return js_instanceof(value, class_id); - } - // #6558: `e instanceof WebAssembly.CompileError` (and LinkError / - // RuntimeError). These constructors live on the WebAssembly NAMESPACE — - // not on `globalThis`, so the builtin-name path above never resolves - // them — and their instances are ErrorHeader-backed values with no - // prototype chain reaching the namespace ctor's `.prototype`, so the - // ordinary prototype walk below can't brand them either. Identify the - // ctor by its dedicated thunk func_ptr (GC-move-safe) and brand-check - // the instance by its error `.name`. - if let Some(matches) = super::global_this::webassembly_error_ctor_instanceof(value, type_ref) { - return f64::from_bits(if matches { - crate::value::TAG_TRUE - } else { - crate::value::TAG_FALSE - }); - } - // #6558 sibling: `mod instanceof WebAssembly.Module` for the wasm-host - // module wrapper. Its `[[Prototype]]` does not reach the namespace ctor's - // `.prototype`, so brand-check its GC-aware internal wrapper identity. - // Only a positive match short-circuits here; a miss returns `None` so the - // value still flows to the prototype walk below (how `WebAssembly.Memory` - // instances resolve, and how a foreign object answers `false`). - if let Some(true) = super::global_this::webassembly_value_ctor_instanceof(value, type_ref) { - return f64::from_bits(crate::value::TAG_TRUE); - } - if let Some((module, method)) = unsafe { bound_native_callable_module_and_method(type_ref) } { - if module == "stream" - && matches!( - method.as_str(), - "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" - ) - && (crate::node_stream::is_classic_stream_instance_of(value, method.as_str()) - || super::tls_constructor_prototype_is_instance_of(value, method.as_str())) - { - 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 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 - } else { - TAG_FALSE - }, - ); - } - if module == "events" - && method == "EventEmitterAsyncResource" - && is_event_emitter_async_resource_instance_value(value) - { - return f64::from_bits(crate::value::TAG_TRUE); - } - if module == "async_hooks" - && matches!(method.as_str(), "AsyncLocalStorage" | "AsyncResource") - { - let raw = value_addr(value); - let matched = if method == "AsyncResource" { - crate::async_hooks::resolve_async_resource_handle(raw as i64).is_some() - || (crate::value::addr_class::is_plausible_heap_addr(raw) - && ordinary_has_instance_prototype_walk(value, type_ref)) - } else { - let candidate = small_native_handle_id(value).unwrap_or(raw as i64); - let native = (candidate != 0) && { - super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { - let property = b"getStore"; - let result = - unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; - value_is_callable(result) - }) - }; - native - || (crate::value::addr_class::is_plausible_heap_addr(raw) - && ordinary_has_instance_prototype_walk(value, type_ref)) - }; - return f64::from_bits(if matched { - crate::value::TAG_TRUE - } else { - TAG_FALSE - }); - } - if module == "tty" - && matches!(method.as_str(), "ReadStream" | "WriteStream") - && crate::tty::is_tty_stream_instance(value, method.as_str()) - { - return f64::from_bits(crate::value::TAG_TRUE); - } - if module == "fs" { - let matched = match method.as_str() { - "Stats" => crate::fs::is_fs_stats_instance_value(value), - "Dir" => crate::fs::is_fs_dir_instance_value(value), - "Dirent" => crate::fs::is_fs_dirent_instance_value(value), - "ReadStream" | "FileReadStream" | "WriteStream" | "FileWriteStream" - | "Utf8Stream" => crate::fs::is_fs_stream_instance_value(value, method.as_str()), - _ => false, - }; - if matched { - return f64::from_bits(crate::value::TAG_TRUE); - } - } - if module == "tls" - && method == "SecureContext" - && crate::tls::is_secure_context_instance(value) - { - return f64::from_bits(crate::value::TAG_TRUE); - } - if module == "tls" && matches!(method.as_str(), "Server" | "TLSSocket") { - let want = if method == "Server" { 1 } else { 2 }; - if let (Some(handle), Some(probe)) = ( - small_native_handle_id(value), - crate::object::tls_handle_kind_probe(), - ) { - return f64::from_bits(if unsafe { probe(handle) } == want { - crate::value::TAG_TRUE - } else { - TAG_FALSE - }); - } - } - if module == "wasi" && method == "WASI" && crate::wasi::is_wasi_instance(value) { - return f64::from_bits(crate::value::TAG_TRUE); - } - if module == "repl" { - let matched = match method.as_str() { - "Recoverable" => crate::node_repl::is_recoverable_value(value), - "REPLServer" => crate::node_repl::is_repl_server_value(value), - _ => false, - }; - if matched { - return f64::from_bits(crate::value::TAG_TRUE); - } - } - // #2689: `net.Stream` is an alias for `net.Socket`; both should match - // a live socket handle via the runtime probe. - if module == "net" && matches!(method.as_str(), "Socket" | "Stream") { - if let Some(handle) = small_native_handle_id(value) { - let net_socket = crate::object::net_socket_handle_probe() - .map(|probe| unsafe { probe(handle) }) - .unwrap_or(false); - let tls_socket = crate::object::tls_handle_kind_probe() - .map(|probe| unsafe { probe(handle) == 2 }) - .unwrap_or(false); - if net_socket || tls_socket { - return f64::from_bits(crate::value::TAG_TRUE); - } - } - } - if module == "console" - && method == "Console" - && crate::builtins::is_console_instance_value(value) - { - return f64::from_bits(crate::value::TAG_TRUE); - } - if module == "crypto" && method == "KeyObject" { - let addr = value_addr(value); - return if addr != 0 - && (crate::buffer::is_secret_key(addr) - || crate::buffer::asymmetric_key_meta(addr).is_some()) - { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - if module == "perf_hooks" { - let class_id = match method.as_str() { - "Performance" => crate::perf_hooks::CLASS_ID_PERFORMANCE, - "PerformanceEntry" => crate::perf_hooks::CLASS_ID_PERFORMANCE_ENTRY, - "PerformanceMark" => crate::perf_hooks::CLASS_ID_PERFORMANCE_MARK, - "PerformanceMeasure" => crate::perf_hooks::CLASS_ID_PERFORMANCE_MEASURE, - "PerformanceObserverEntryList" => { - crate::perf_hooks::CLASS_ID_PERFORMANCE_OBSERVER_ENTRY_LIST - } - "PerformanceResourceTiming" => { - crate::perf_hooks::CLASS_ID_PERFORMANCE_RESOURCE_TIMING - } - _ => 0, - }; - if class_id != 0 { - return js_instanceof(value, class_id); - } - } - } - if is_buffer_constructor_value(type_ref) { - return js_instanceof(value, crate::buffer::BUFFER_TYPE_ID); - } - if let Some(name) = identify_global_builtin_constructor(type_ref) { - match name { - "Crypto" => { - return if is_native_module_namespace_value(value, "crypto.webcrypto") { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - "SubtleCrypto" => { - return if is_native_module_namespace_value(value, "crypto.subtle") { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - "CryptoKey" => { - let addr = value_addr(value); - return if addr != 0 && crate::buffer::crypto_key_meta(addr).is_some() { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - _ => {} - } - let class_id = global_builtin_constructor_class_id(name); - if class_id != 0 { - let r = js_instanceof(value, class_id); - if r.to_bits() == crate::value::TAG_TRUE { - return r; - } - // #5989: an object that inherits a builtin's prototype via - // `Fn.prototype = Object.create(Builtin.prototype)` is `instanceof - // Builtin` per the spec even though it carries no builtin class id — - // react-server-dom's flight Chunk inherits `Promise.prototype` this - // way, so `chunk instanceof Promise` must be true. Walk the real - // [[Prototype]] chain against `Builtin.prototype` before answering - // false. - if ordinary_has_instance_prototype_walk(value, type_ref) { - return f64::from_bits(crate::value::TAG_TRUE); - } - return f64::from_bits(TAG_FALSE); - } - } - if crate::node_submodules::is_diagnostics_channel_constructor_value(type_ref) { - return if crate::node_submodules::diagnostics_channel_is_channel_instance_value(value) { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - // `inst instanceof Intl.`: Intl instances are plain heap objects whose - // `[[Prototype]]` is `Intl..prototype` but carry no class-id, so the - // arms above can't match them. Walk their static-prototype chain. - // `Intl.*` brand checks. Behind `intl-namespace`: with the feature off no - // Intl constructor value can exist (the namespace install is a no-op), so - // the probe could never match — and skipping it keeps this always-live - // dispatcher from statically pinning every Intl constructor thunk (~204 KB). - #[cfg(feature = "intl-namespace")] - if let Some(is_inst) = crate::intl::intl_instanceof(value, type_ref) { - return if is_inst { - f64::from_bits(crate::value::TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) - }; - } - js_instanceof_dynamic_tail(value, type_ref) -} - /// Runtime class id for a globalThis built-in constructor *name*. /// /// Reference-type global constructors used as runtime values (e.g. @@ -1036,799 +644,139 @@ fn class_chain_reaches_parents_only(start: u32, want: u32, depth0: usize) -> boo false } -/// Check if a value is an instance of a class with the given class_id -/// Walks the inheritance chain to check parent classes -/// Returns NaN-boxed TAG_TRUE / TAG_FALSE so the result identifies as a boolean. -#[no_mangle] -pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { - const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; - let true_val = f64::from_bits(TAG_TRUE); - let false_val = f64::from_bits(TAG_FALSE); - - if class_id == 0 { - return false_val; - } - // `proxy instanceof C` follows the proxy's prototype chain, which forwards - // to the target (absent a `getPrototypeOf` trap) — so unwrap to the target - // before walking the class chain. The proxy is a small id with no chain of - // its own. (drizzle's aliased-column proxies + `is(value, type)`.) - let mut value = value; - { - let mut depth = 0; - while depth < 16 && crate::proxy::js_proxy_is_proxy(value) != 0 { - value = crate::proxy::js_proxy_target(value); - depth += 1; - } - } - // User-defined `Symbol.hasInstance` takes precedence over the built-in - // prototype-chain walk — and over the ordinary class-chain fast path below. - // `new C() instanceof C` must run a class-level `@@hasInstance` rather than - // short-circuit on the chain (the hook can return `false` for a real - // instance), so both hook forms are consulted here, ahead of that walk. - // - // Form 1: the HIR lifts `static [Symbol.hasInstance](v)` to a top-level - // function `__perry_wk_hasinstance_` and the LLVM backend registers a - // pointer to it against the class id at module init. - if let Some(func_ptr) = lookup_has_instance_hook(class_id) { - let hook: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(func_ptr as *const u8) }; - let result = hook(value); - // Normalize: any truthy NaN-boxed bool stays as the TAG_TRUE/FALSE - // sentinel. User-written `return typeof v === "number" && ...` - // already returns a NaN-boxed bool, so this is usually a no-op. - let rbits = result.to_bits(); - if rbits == TAG_TRUE || rbits == TAG_FALSE { - return result; - } - // Fallback: treat as truthy → TRUE, zero/undefined → FALSE. - if result.is_nan() && rbits & 0xFFFF_0000_0000_0000 == 0x7FFC_0000_0000_0000 { - return false_val; - } - if result == 0.0 || result.is_nan() { - return false_val; - } - return true_val; - } - - // Form 2: the `Object.defineProperty(C, Symbol.hasInstance, { value: fn })` - // form (zod 4) stores the closure in the class static-symbol table. Read it - // off the class id (OWN lookup only — never resolves Function.prototype's - // default @@hasInstance thunk, so no recursion). A present-but-non-callable - // value throws; only `null`/`undefined` falls through to the chain. - // - // The latch check is what keeps `well_known_symbol("hasInstance")` — a - // string-keyed interning probe — off the path entirely in the (dominant) - // case where no class in the program declares any static Symbol member. - if crate::symbol::CLASS_STATIC_SYMBOLS_LATCH.is_armed() { - let hi_sym = crate::symbol::well_known_symbol("hasInstance"); - if !hi_sym.is_null() { - let hi_f64 = f64::from_bits(crate::value::JSValue::pointer(hi_sym as *const u8).bits()); - if let Some(vb) = crate::symbol::class_static_symbol_lookup(class_id, hi_f64) { - let cb = f64::from_bits(vb); - if let HasInstanceOutcome::Result(r) = dispatch_own_has_instance(cb, value) { - return r; - } - } - } - } - - // Subclass-of-built-in: `class S extends Array {}` produces a real - // ObjectHeader instance whose class-id chain reaches the built-in's - // reserved class id (a parent edge registered at module init). The - // per-built-in probes below short-circuit to `false` for such an - // instance (it isn't a *real* Array/Map/Error/…), so walk the object's - // own class chain up front. Only genuine `GC_TYPE_OBJECT` instances carry - // a `class_id` field — real Arrays/Maps/Errors have other GC types and - // fall through to their dedicated probes unchanged. Refs - // class/subclass-builtins/* and class/subclass/builtin-objects/*. - { - let jv = crate::JSValue::from_bits(value.to_bits()); - if jv.is_pointer() { - let obj = jv.as_pointer::(); - if crate::value::addr_class::is_above_handle_band(obj as usize) { - let gc_header = unsafe { - (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader - }; - if unsafe { (*gc_header).obj_type } == crate::gc::GC_TYPE_OBJECT { - let cur = unsafe { (*obj).class_id }; - if class_chain_reaches(cur, class_id) { - return true_val; - } - - // #9362: util.inherits(DerivedClass, BaseClass) links - // DerivedClass.prototype to BaseClass.prototype at - // runtime; it does not (and must not) create an extends - // edge between the constructor objects. The class-id fast - // path above therefore misses even though the observable - // prototype chain contains BaseClass.prototype. Only pay - // for the spec prototype walk when the candidate class's - // declaration prototype has a user-selected parent. - // The two `class_decl_prototype_object` probes are class - // registry reads (TLS + RwLock + map, ~130 instructions - // each) and they ran EAGERLY on every call that got this - // far — which is every MISS, the path this whole ladder - // exists to answer `false` on. They exist only to ask a - // question whose answer is `false` for every receiver in a - // process that never re-points an object's prototype, and - // the latch answers that for the whole process in one - // load. Set, never cleared, and published before the flag - // it guards, so it can only ever be conservatively true. - if super::prototype_chain::any_user_prototype_override() { - let candidate_proto = - super::class_registry::class_decl_prototype_object(cur); - let target_proto = - super::class_registry::class_decl_prototype_object(class_id); - if !candidate_proto.is_null() - && !target_proto.is_null() - && super::prototype_chain::object_has_user_prototype_override( - candidate_proto as usize, - ) - && ordinary_has_instance_prototype_walk( - value, - super::class_constructor_ref_value(class_id), - ) - { - return true_val; - } - } - } - } - } - } - // Temporal reference types (`d instanceof Temporal.Duration`, …). A Temporal - // value is a NaN-boxed pointer to a brand-tagged cell, not an ObjectHeader - // with a class chain, so probe the cell's brand kind directly. Keep the band - // in sync with perry-runtime/src/temporal/mod.rs. - if (crate::temporal::CLASS_ID_TEMPORAL_FIRST..=crate::temporal::CLASS_ID_TEMPORAL_LAST) - .contains(&class_id) - { - return if crate::temporal::temporal_value_matches_class_id(value, class_id) { - true_val - } else { - false_val - }; - } - // `value instanceof Function` — true for any callable value. Per - // `OrdinaryHasInstance`, every Perry function (declaration, expression, - // arrow, method, bound function, native handle, built-in constructor) - // has `Function.prototype` in its prototype chain. Keep `CLASS_ID_FUNCTION` - // in sync with perry-codegen/src/expr/instance_misc1.rs. - if class_id == CLASS_ID_FUNCTION { - return if value_is_callable(value) { - true_val - } else { - false_val - }; - } - // Keep in sync with perry-codegen/src/expr/instance_misc1.rs. - let classic_stream_name = match class_id { - 0xFFFF0070 => Some("Stream"), - 0xFFFF0071 => Some("Readable"), - 0xFFFF0072 => Some("Writable"), - 0xFFFF0073 => Some("Duplex"), - 0xFFFF0074 => Some("Transform"), - 0xFFFF0075 => Some("PassThrough"), - _ => None, - }; - if let Some(name) = classic_stream_name { - return if crate::node_stream::is_classic_stream_instance_of(value, name) - || super::tls_constructor_prototype_is_instance_of(value, name) - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_EVENT_EMITTER { - return if is_event_emitter_instance_value(value) - || super::tls_constructor_prototype_is_instance_of(value, "EventEmitter") - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_EVENT_EMITTER_ASYNC_RESOURCE { - return if is_event_emitter_async_resource_instance_value(value) { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_ASYNC_RESOURCE { - return if crate::async_hooks::resolve_async_resource_handle(value_addr(value) as i64) - .is_some() - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_ASYNC_LOCAL_STORAGE { - let candidate = small_native_handle_id(value).unwrap_or(value_addr(value) as i64); - let matched = candidate != 0 && { - super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { - let property = b"getStore"; - let result = unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; - value_is_callable(result) - }) - }; - return if matched { true_val } else { false_val }; - } - if class_id == CLASS_ID_NET_SOCKET { - return if let Some(handle) = small_native_handle_id(value) { - let net_socket = crate::object::net_socket_handle_probe() - .map(|probe| unsafe { probe(handle) }) - .unwrap_or(false); - let tls_socket = crate::object::tls_handle_kind_probe() - .map(|probe| unsafe { probe(handle) == 2 }) - .unwrap_or(false); - if net_socket || tls_socket { - true_val - } else { - false_val - } - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_STATS_EXPORT { - return if crate::fs::is_fs_stats_instance_value(value) { - true_val - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_DIR { - return if crate::fs::is_fs_dir_instance_value(value) { - true_val - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_DIRENT { - return if crate::fs::is_fs_dirent_instance_value(value) { - true_val - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_READ_STREAM { - return if crate::fs::is_fs_stream_instance_value(value, "ReadStream") { - true_val - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_WRITE_STREAM { - return if crate::fs::is_fs_stream_instance_value(value, "WriteStream") { - true_val - } else { - false_val - }; - } - if class_id == crate::fs::CLASS_ID_FS_UTF8_STREAM { - return if crate::fs::is_fs_stream_instance_value(value, "Utf8Stream") { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_CRYPTO { - return if is_native_module_namespace_value(value, "crypto.webcrypto") { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_SUBTLE_CRYPTO { - return if is_native_module_namespace_value(value, "crypto.subtle") { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_CRYPTO_KEY { - let addr = value_addr(value); - return if addr != 0 && crate::buffer::crypto_key_meta(addr).is_some() { - true_val - } else { - false_val - }; - } - - let bits = value.to_bits(); - let jsval = crate::JSValue::from_bits(bits); - - // Native/exotic subclass instances (typed arrays, ArrayBuffers, boxed - // primitives, Dates, …) do not carry a Perry `ObjectHeader.class_id`. - // Their constructor records the distinct newTarget prototype in the - // prototype side table instead. Honor that chain for user class ids. - if is_class_id_registered(class_id) { - let addr = value_addr(value); - if addr != 0 && super::prototype_chain::object_static_prototype(addr).is_some() { - let constructor = super::class_constructor_ref_value(class_id); - return if ordinary_has_instance_prototype_walk(value, constructor) { - true_val - } else { - false_val - }; - } - } +/// #10624: `subclass_of_builtin_reaches`'s armed-latch arm. +fn class_chain_reaches_dynamic_armed(cur: u32, obj: *const ObjectHeader, want: u32) -> bool { + let pin = super::class_registry::instance_pinned_constructing_class(obj); + class_chain_reaches_dynamic(cur, pin, want) +} - // Special handling for Uint8Array/Buffer (class_id 0xFFFF0004) - // Perry buffers are raw BufferHeader pointers bitcast to f64 (not NaN-boxed), - // so the normal POINTER_TAG check doesn't work for them. - // We use a thread-local buffer registry to identify buffer pointers. - if class_id == crate::buffer::BUFFER_TYPE_ID { - // Check if NaN-boxed pointer - if jsval.is_pointer() { - let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::buffer::is_registered_buffer(addr) { - return true_val; - } - } - // Check if raw pointer (buffer values are bitcast, not NaN-boxed) - let top16 = (bits >> 48) as u16; - if top16 == 0 && bits >= 0x1000 && crate::buffer::is_registered_buffer(bits as usize) { - return true_val; - } - return false_val; +/// Does the ancestry chain from `start_cid` reach `want`, walking by VALUE +/// while precision is available? `class_chain_reaches` walks purely by +/// class_id through the shared, last-write-wins `CLASS_REGISTRY` — +/// ambiguous once the SAME `ClassExprFresh` template has been evaluated more +/// than once. Each hop here instead prefers, in order: (1) `start_pin`/a +/// pinned VALUE on the current node (`class_object_pinned_parent`, the same +/// per-evaluation edge `super()`/captures already consult), (2) +/// `template_dynamic_parent_value`, the actual parent VALUE for any class_id +/// registered dynamically. Exhausting both degrades to exactly +/// `class_chain_reaches`'s answer — so an instance from an EARLIER +/// evaluation stays correct even after a LATER one overwrote the table. +fn class_chain_reaches_dynamic(start_cid: u32, start_pin: Option, want: u32) -> bool { + const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; + if start_cid == 0 || want == 0 { + return false; } - - // ArrayBuffer — Perry models ArrayBuffer storage with BufferHeader values - // marked in a side registry. They can arrive either NaN-boxed or as raw - // buffer pointers, matching the Buffer/Uint8Array path above. - const CLASS_ID_ARRAY_BUFFER: u32 = 0xFFFF0025; - const CLASS_ID_SHARED_ARRAY_BUFFER: u32 = 0xFFFF002E; - if class_id == CLASS_ID_ARRAY_BUFFER || class_id == CLASS_ID_SHARED_ARRAY_BUFFER { - 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 - } - }; - let matches_brand = if class_id == CLASS_ID_SHARED_ARRAY_BUFFER { - crate::buffer::is_shared_array_buffer(addr) - } else { - crate::buffer::is_array_buffer(addr) - }; - if addr != 0 && crate::buffer::is_registered_buffer(addr) && matches_brand { - return true_val; + let mut cur = start_cid; + let mut cur_value = start_pin; + let mut depth = 0usize; + loop { + if cur == want { + return true; } - return false_val; - } - - // #1545: Web Streams `instanceof ReadableStream` / `instanceof - // WritableStream`. Stream handles are numeric `id as f64`, so consult the - // stdlib kind-probe (1 = readable, 2 = writable) rather than the class - // chain. Covers `ts.readable instanceof ReadableStream`, - // `rs.pipeThrough(ts) instanceof ReadableStream`, etc. - // kind probe values: 1 = readable, 2 = writable, 5 = transform - // (3 = reader, 4 = writer — not user-facing instanceof targets here). - const CLASS_ID_READABLE_STREAM: u32 = 0xFFFF0060; - const CLASS_ID_WRITABLE_STREAM: u32 = 0xFFFF0061; - const CLASS_ID_TRANSFORM_STREAM: u32 = 0xFFFF0062; - if class_id == CLASS_ID_READABLE_STREAM - || class_id == CLASS_ID_WRITABLE_STREAM - || class_id == CLASS_ID_TRANSFORM_STREAM - { - if value.is_finite() && value > 0.0 && value.fract() == 0.0 { - if let Some(probe) = crate::object::stream_handle_kind_probe() { - let kind = unsafe { probe(value as usize) }; - let want = match class_id { - CLASS_ID_READABLE_STREAM => 1, - CLASS_ID_WRITABLE_STREAM => 2, - _ => 5, // CLASS_ID_TRANSFORM_STREAM - }; - if kind == want { - return true_val; - } - } + if depth > 64 { + return false; } - return false_val; - } - - // WHATWG fetch: `instanceof Response` / `Request` / `Headers` / `Blob` / - // `File`. - // These are pointer-tagged small-integer handles (stdlib fetch registries), - // not heap objects, so consult the stdlib fetch kind-probe rather than the - // class chain. Without this, Hono's `res instanceof Response` route-fallback - // guard sees `false` and skips the fallback, escaping a bare sentinel. - const CLASS_ID_RESPONSE: u32 = 0xFFFF0028; - const CLASS_ID_REQUEST: u32 = 0xFFFF0029; - const CLASS_ID_HEADERS: u32 = 0xFFFF002A; - const CLASS_ID_BLOB: u32 = 0xFFFF0026; - const CLASS_ID_FILE: u32 = 0xFFFF002F; - if class_id == CLASS_ID_RESPONSE - || class_id == CLASS_ID_REQUEST - || class_id == CLASS_ID_HEADERS - || class_id == CLASS_ID_BLOB - || class_id == CLASS_ID_FILE - { - let want = match class_id { - CLASS_ID_RESPONSE => 1u8, - CLASS_ID_REQUEST => 2, - CLASS_ID_HEADERS => 3, - CLASS_ID_BLOB => 4, - _ => 5, // CLASS_ID_FILE - }; - if let Some(handle) = small_native_handle_id(value) { - if let Some(probe) = crate::object::fetch_handle_kind_probe() { - let kind = unsafe { probe(handle as usize) }; - // File inherits Blob, so a File handle satisfies both brands. - if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { - return true_val; - } + if let Some(gid) = crate::object::class_generic_origin(cur) { + if gid == want || class_chain_reaches_parents_only(gid, want, depth + 1) { + return true; } } - // `class X extends Request/Response` instance: a heap object that - // stashes the underlying native fetch handle id under - // `__perry_fetch_handle__`. Unwrap and probe so `sub instanceof - // Request` is true, matching a bare handle. - if jsval.is_pointer() { - let raw = jsval.as_pointer::() as usize; - if let Some(id) = unsafe { crate::object::fetch_subclass_handle_id(raw) } { - if let Some(probe) = crate::object::fetch_handle_kind_probe() { - let kind = unsafe { probe(id as usize) }; - if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { - return true_val; - } - } - } + let pinned = cur_value + .filter(|v| is_class_object_value(*v)) + .and_then(|v| { + class_object_pinned_parent( + crate::value::js_nanbox_get_pointer(v) as *const ObjectHeader + ) + }); + let next_value = pinned.unwrap_or_else(|| { + super::class_registry::parent_static::template_dynamic_parent_value(cur) + }); + if next_value.to_bits() == TAG_UNDEFINED { + return false; } - // A Blob can also be a real heap object allocated with CLASS_ID_BLOB - // (e.g. `stream/consumers`.`blob()` and `blob_value_from_bytes`), not - // just a small fetch-registry handle. Match it by its own class id so - // `blob instanceof Blob` is true for that representation too. - if class_id == CLASS_ID_BLOB && jsval.is_pointer() { - let obj = jsval.as_pointer::(); - if crate::value::addr_class::is_above_handle_band(obj as usize) - && unsafe { (*obj).class_id } == CLASS_ID_BLOB - { - return true_val; - } + let next_cid = dynamic_value_class_id(next_value); + if next_cid == 0 || next_cid == cur { + return false; } - return false_val; + cur = next_cid; + cur_value = Some(next_value); + depth += 1; } +} - // Built-in JS types Map / Set / RegExp / Date — Perry doesn't define - // user classes for these, so we use reserved class IDs and detect via - // the per-type registries (MAP_REGISTRY / SET_REGISTRY / REGEX_POINTERS) - // or, for Date, by checking that the value is a finite f64 timestamp. - const CLASS_ID_DATE: u32 = 0xFFFF0020; - const CLASS_ID_REGEXP: u32 = 0xFFFF0021; - const CLASS_ID_MAP: u32 = 0xFFFF0022; - const CLASS_ID_SET: u32 = 0xFFFF0023; - if class_id == CLASS_ID_DATE { - // A Perry Date is a NaN-boxed pointer to a `DateCell` (#2089). Its - // identity is the cell's `GcHeader` type, so `new Date(NaN)` (an - // Invalid Date — a cell whose time value is NaN) matches just like - // any other Date, and a plain number never matches. - if crate::date::is_date_value(value) { - return true_val; - } - return false_val; - } - if class_id == CLASS_ID_MAP { - if jsval.is_pointer() { - let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::map::is_registered_map(addr) { - return true_val; - } - } - return false_val; - } - if class_id == CLASS_ID_SET { - if jsval.is_pointer() { - let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::set::is_registered_set(addr) { - return true_val; - } - } - return false_val; - } - // #5834: `x instanceof WeakMap`/`WeakSet` for a REAL instance. These - // reserved ids (kept in sync with perry-codegen/src/expr/instance_misc1.rs) - // are distinct from the runtime `CLASS_ID_WEAKMAP`/`CLASS_ID_WEAKSET` - // stamped on actual instances (weakref.rs) — the subclass-chain walk above - // only matches a `class S extends WeakMap {}` instance (whose chain reaches - // this reserved id), so a genuine `new WeakMap()` still needs its own probe - // here, same shape as Map/Set above. - const CLASS_ID_WEAKMAP_RESERVED: u32 = 0xFFFF002C; - const CLASS_ID_WEAKSET_RESERVED: u32 = 0xFFFF002D; - if class_id == CLASS_ID_WEAKMAP_RESERVED { - return if crate::object::weak_class_id_from_receiver(value) - == Some(crate::weakref::CLASS_ID_WEAKMAP) - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_WEAKSET_RESERVED { - return if crate::object::weak_class_id_from_receiver(value) - == Some(crate::weakref::CLASS_ID_WEAKSET) - { - true_val - } else { - false_val - }; - } - if class_id == CLASS_ID_REGEXP { - if jsval.is_pointer() { - let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; - if crate::regex::is_regex_pointer(addr as *const u8) { - return true_val; - } - } - return false_val; - } - if class_id == CLASS_ID_PROMISE { - if let Some(matches) = recorded_prototype_instanceof_builtin(value, "Promise") { - return if matches { true_val } else { false_val }; - } - return if crate::promise::js_value_is_promise(value) != 0 { - true_val - } else { - false_val - }; +/// `class S extends Array {}` produces a real `ObjectHeader` instance whose +/// class-id chain reaches the built-in's reserved class id (a parent edge +/// registered at module init). The per-built-in probes in `js_instanceof` +/// short-circuit to `false` for such an instance (it isn't a *real* +/// Array/Map/Error/…), so walk the object's own class chain up front. Only +/// genuine `GC_TYPE_OBJECT` instances carry a `class_id` field. Refs +/// class/subclass-builtins/* and class/subclass/builtin-objects/*. +/// +/// Split out of `js_instanceof` (#10624) so that function's own size, and +/// thus how well its unrelated, far more common paths optimize, does not +/// depend on this ladder's own latch-gated logic. +fn subclass_of_builtin_reaches(value: f64, class_id: u32) -> bool { + let jv = crate::JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; } - - // `Object` — ECMAScript spec: `x instanceof Object` is true for any - // non-primitive (every object/array/function/Map/Set/Buffer/RegExp/ - // Date/typed-array/Promise/etc.). The codegen maps `Object` to this - // reserved id (#585 follow-up: pre-#585 fix this case worked by - // accident because the codegen produced `class_id = 0` and the - // runtime returned true via `0 == 0` on the obj_class_id check). - const CLASS_ID_OBJECT: u32 = 0xFFFF0050; - if class_id == CLASS_ID_OBJECT { - if jsval.is_pointer() { - // A Symbol is a POINTER_TAG heap allocation but a PRIMITIVE, not an - // object, so `Symbol() instanceof Object` is false (the comment - // above says "any non-primitive"). Every other primitive is - // non-pointer-tagged and already falls through below. #6587 review. - if unsafe { crate::symbol::js_is_symbol(value) != 0 } { - return false_val; - } - // Covers every heap object, including a Date (now a NaN-boxed - // `DateCell` pointer — #2089) and an Invalid Date. - return true_val; - } - let top16 = (bits >> 48) as u16; - if top16 == 0 && bits >= 0x1000 { - let addr = bits as usize; - if crate::buffer::is_registered_buffer(addr) - || crate::set::is_registered_set(addr) - || crate::map::is_registered_map(addr) - || crate::typedarray::lookup_typed_array_kind(addr).is_some() - { - return true_val; - } - } - return false_val; + let obj = jv.as_pointer::(); + if !crate::value::addr_class::is_above_handle_band(obj as usize) { + return false; } - - // Array — Perry arrays are heap allocations with `GC_TYPE_ARRAY` in - // their gc_header (one byte at obj-8). Pointer can arrive NaN-boxed - // (POINTER_TAG) or as a raw bitcast f64; handle both. Lazy arrays - // (Phase 5 JSON.parse result) are also arrays from the user's - // perspective — must return true without force-materializing. - const CLASS_ID_ARRAY: u32 = 0xFFFF0024; - if class_id == CLASS_ID_ARRAY { - // 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 }; + let gc_header = + unsafe { (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader }; + if unsafe { (*gc_header).obj_type } != crate::gc::GC_TYPE_OBJECT { + return false; } - - // Typed arrays — Int8Array..Float16Array reserved IDs (0xFFFF0030..3B). - // The pointer can arrive as either a NaN-boxed POINTER_TAG value or a - // raw bitcast f64, so handle both forms. - if (0xFFFF0030..=0xFFFF003B).contains(&class_id) { - let addr = if jsval.is_pointer() { - (bits & 0x0000_FFFF_FFFF_FFFF) as usize + let cur = unsafe { (*obj).class_id }; + // #10624: only pay for the value-aware walk once something has pinned + // per-evaluation heritage. + let reaches = + if super::class_registry::evaluation_heritage::CLASS_OBJECT_HERITAGE_PIN_LATCH.is_idle() { + class_chain_reaches(cur, class_id) } else { - let top16 = (bits >> 48) as u16; - if top16 == 0 && bits >= 0x1000 { - bits as usize - } else { - 0 - } + class_chain_reaches_dynamic_armed(cur, obj, class_id) }; - if addr != 0 { - if let Some(actual_kind) = crate::typedarray::lookup_typed_array_kind(addr) { - let want_id = crate::typedarray::class_id_for_kind(actual_kind); - if want_id == class_id { - return true_val; - } - } - } - return false_val; - } - - // Only objects (pointers) can be instances of classes - if !jsval.is_pointer() { - return false_val; - } - - // Get the object pointer - let obj_ptr = jsval.as_pointer::(); - if obj_ptr.is_null() { - return false_val; - } - - // Refs #421: NaN-boxed POINTER_TAG values whose unboxed payload is a - // small registry id (Web Fetch handles, sockets, DB connections, etc.) - // are NOT real ObjectHeader pointers — reading the GC header at - // `obj_ptr - 8` would SIGSEGV on unmapped memory. They aren't instances - // of any user-defined class either, so return false unconditionally. - if crate::value::addr_class::is_handle_band(obj_ptr as usize) { - return false_val; + if reaches { + return true; } - unsafe { - // Special handling for built-in Error and its subclasses (TypeError, RangeError, etc.). - // ErrorHeader uses GC_TYPE_ERROR; we match by error_kind against the requested CLASS_ID_*. - let gc_header = - (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let gc_type = (*gc_header).obj_type; - if gc_type == crate::gc::GC_TYPE_ERROR { - let err_ptr = obj_ptr as *const crate::error::ErrorHeader; - let kind = (*err_ptr).error_kind; - if class_id == crate::event_target::CLASS_ID_DOM_EXCEPTION { - return if crate::event_target::is_dom_exception_error(err_ptr) { - true_val - } else { - false_val - }; - } - let builtin_name = match class_id { - crate::error::CLASS_ID_ERROR => Some("Error"), - crate::error::CLASS_ID_TYPE_ERROR => Some("TypeError"), - crate::error::CLASS_ID_RANGE_ERROR => Some("RangeError"), - crate::error::CLASS_ID_REFERENCE_ERROR => Some("ReferenceError"), - crate::error::CLASS_ID_SYNTAX_ERROR => Some("SyntaxError"), - crate::error::CLASS_ID_EVAL_ERROR => Some("EvalError"), - crate::error::CLASS_ID_URI_ERROR => Some("URIError"), - crate::error::CLASS_ID_AGGREGATE_ERROR => Some("AggregateError"), - _ => None, - }; - if let Some(name) = builtin_name { - if let Some(matches) = recorded_prototype_instanceof_builtin(value, name) { - return if matches { true_val } else { false_val }; - } - } - return match class_id { - crate::error::CLASS_ID_ERROR => true_val, - crate::error::CLASS_ID_TYPE_ERROR => { - if kind == crate::error::ERROR_KIND_TYPE_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_RANGE_ERROR => { - if kind == crate::error::ERROR_KIND_RANGE_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_REFERENCE_ERROR => { - if kind == crate::error::ERROR_KIND_REFERENCE_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_SYNTAX_ERROR => { - if kind == crate::error::ERROR_KIND_SYNTAX_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_EVAL_ERROR => { - if kind == crate::error::ERROR_KIND_EVAL_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_URI_ERROR => { - if kind == crate::error::ERROR_KIND_URI_ERROR { - true_val - } else { - false_val - } - } - crate::error::CLASS_ID_AGGREGATE_ERROR => { - if kind == crate::error::ERROR_KIND_AGGREGATE_ERROR { - true_val - } else { - false_val - } - } - _ => false_val, - }; - } - - if gc_type == crate::gc::GC_TYPE_OBJECT { - if let Some(matches) = - crate::perf_hooks::is_perf_hooks_shape_instance_of(value, class_id) - { - return if matches { true_val } else { false_val }; - } - if let Some(matches) = - crate::perf_hooks::is_perf_entry_object_instance_of(obj_ptr, class_id) - { - return if matches { true_val } else { false_val }; - } - } - - // For user-defined classes that extend Error: `myErr instanceof Error` should be true. - if class_id == crate::error::CLASS_ID_ERROR { - // #9940: a function-local class declaration gets a fresh class - // object on every evaluation, but all evaluations share its - // compile-time class id. A constructor factory can therefore - // evaluate `class Definition extends Error {}`, then later - // evaluate the same declaration with an Object parent. The class - // registry is keyed by the shared id and is necessarily - // last-wins; the instance's recorded evaluation prototype is the - // authoritative chain. Zod's `$constructor` has exactly this - // shape, and its later schema classes made an earlier ZodError - // fail `instanceof Error` even though getPrototypeOf still showed - // `ZodError -> Error -> Object`. - 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; - } - } - - // Check if the object's class_id matches directly - let obj_class_id = (*obj_ptr).class_id; - if class_id == crate::event_target::CLASS_ID_EVENT - && obj_class_id == crate::event_target::CLASS_ID_CUSTOM_EVENT + // #9362: util.inherits(DerivedClass, BaseClass) links DerivedClass.prototype + // to BaseClass.prototype at runtime; it does not (and must not) create an + // extends edge between the constructor objects. The class-id fast path + // above therefore misses even though the observable prototype chain + // contains BaseClass.prototype. Only pay for the spec prototype walk when + // the candidate class's declaration prototype has a user-selected parent. + // The two `class_decl_prototype_object` probes are class registry reads + // (TLS + RwLock + map, ~130 instructions each) and they ran EAGERLY on + // every call that got this far — which is every MISS, the path this whole + // ladder exists to answer `false` on. They exist only to ask a question + // whose answer is `false` for every receiver in a process that never + // re-points an object's prototype, and the latch answers that for the + // whole process in one load. Set, never cleared, and published before the + // flag it guards, so it can only ever be conservatively true. + if super::prototype_chain::any_user_prototype_override() { + let candidate_proto = super::class_registry::class_decl_prototype_object(cur); + let target_proto = super::class_registry::class_decl_prototype_object(class_id); + if !candidate_proto.is_null() + && !target_proto.is_null() + && super::prototype_chain::object_has_user_prototype_override(candidate_proto as usize) + && ordinary_has_instance_prototype_walk( + value, + super::class_constructor_ref_value(class_id), + ) { - return true_val; - } - // Walk up the inheritance chain using the class registry. #7575: the - // walk also follows the generic-origin edge, so a dynamic RHS holding a - // generic class (`const C = Gen; x instanceof C`) matches an instance of - // one of its specializations. - if class_chain_reaches(obj_class_id, class_id) { - return true_val; + return true; } - - false_val } + false } +/// Check if a value is an instance of a class with the given class_id +/// Walks the inheritance chain to check parent classes +/// Returns NaN-boxed TAG_TRUE / TAG_FALSE so the result identifies as a boolean. + #[cfg(test)] mod null_lhs_tests { use super::*; diff --git a/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs new file mode 100644 index 0000000000..f768a52cbb --- /dev/null +++ b/crates/perry-runtime/src/object/instanceof/dynamic_dispatch.rs @@ -0,0 +1,406 @@ +//! `js_instanceof_dynamic` — the dynamic (runtime-class-ref) form of +//! `instanceof`, resolving a value/class-ref RHS pair rather than a +//! compile-time-known class id. +//! +//! Split out of `instanceof.rs` for the file-size cap. Pure relocation — +//! no logic changes; see `super::*` for every helper this calls. + +use super::*; + +#[no_mangle] +pub extern "C" fn js_instanceof_dynamic(value: f64, type_ref: f64) -> f64 { + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + // `proxy instanceof C` uses the proxy's `[[GetPrototypeOf]]`, which (absent a + // trap) forwards to the target — so it is equivalent to `target instanceof + // C`. The proxy itself is a small registered id with no class chain, so + // without this it always returned false. Unwrap nested proxies (drizzle + // aliases columns as `new Proxy(column, …)` and its `is(value, type)` brand + // check relies on `value instanceof type`). Bounded to guard a cycle. + let mut value = value; + { + let mut depth = 0; + while depth < 16 && crate::proxy::js_proxy_is_proxy(value) != 0 { + value = crate::proxy::js_proxy_target(value); + depth += 1; + } + } + // `temporalValue instanceof Temporal.` — Temporal values dispatch via + // brand arms (not a real prototype chain), so resolve the constructor to + // its kind and compare against the value's brand. A non-Temporal value, or + // a Temporal value of a different kind, yields `false`. + if let Some(kind) = super::global_this::temporal_ctor_kind(type_ref) { + if crate::temporal::temporal_kind(value) == Some(kind) { + return f64::from_bits(crate::value::TAG_TRUE); + } + // `class X extends Temporal.` instance: a plain heap object whose + // [[Prototype]] chain reaches `Temporal..prototype`. It carries + // the brand via a stashed cell rather than the Temporal-cell tag, so + // recover that cell and compare its kind. The receiver reaches here both + // NaN-boxed (top16 == 0x7FFD) and as a raw-I64 heap pointer (top16 == 0, + // how module-level object vars are stored) — accept both. (#5587) + #[cfg(feature = "temporal")] + { + let bits = value.to_bits(); + let top16 = bits >> 48; + let raw = if top16 == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else if top16 == 0 { + bits as usize + } else { + 0 + }; + if raw != 0 { + if let Some(cell) = unsafe { crate::object::temporal_subclass_cell(raw) } { + if crate::temporal::temporal_kind(cell) == Some(kind) { + return f64::from_bits(crate::value::TAG_TRUE); + } + } + } + } + return f64::from_bits(TAG_FALSE); + } + // Spec step (InstanceofOperator): an OWN user-defined `@@hasInstance` + // overrides even native constructor brand checks. The native generic hook + // lives on Function.prototype, so the own-property gate distinguishes an + // explicit override from that inherited default without recursion. + { + let hi_sym = crate::symbol::well_known_symbol("hasInstance"); + if !hi_sym.is_null() { + let hi_f64 = f64::from_bits(crate::value::JSValue::pointer(hi_sym as *const u8).bits()); + if unsafe { crate::symbol::js_object_has_own_symbol(type_ref, hi_f64) } { + let cb = unsafe { crate::symbol::js_object_get_symbol_property(type_ref, hi_f64) }; + if let HasInstanceOutcome::Result(result) = dispatch_own_has_instance(cb, value) { + return result; + } + } + } + } + // Native http(s).Agent handles have no heap prototype chain. After any own + // override above has had first refusal, retain their native brand check. + if let Some((module, method)) = unsafe { bound_native_callable_module_and_method(type_ref) } { + if matches!(module.as_str(), "http" | "https") && method == "Agent" { + let matched = small_native_handle_id(value) + .zip(crate::object::http_agent_handle_probe()) + .is_some_and(|(handle, probe)| unsafe { probe(handle) }); + return f64::from_bits(if matched { + crate::value::TAG_TRUE + } else { + TAG_FALSE + }); + } + } + let bits = type_ref.to_bits(); + // `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 + // remain distinct and a chain through earlier evaluations still matches. + if is_class_object_value(type_ref) { + // Static/forward `new C()` sites can still construct by template id + // without attaching an evaluated prototype. Retain that representation's + // class-id check; recorded individual chains are authoritative. + if !super::prototype_chain::object_has_prototype_divergence(value_addr(value)) { + let obj = crate::JSValue::from_bits(bits).as_pointer::(); + return js_instanceof(value, js_object_get_class_id(obj)); + } + return f64::from_bits(if ordinary_has_instance_prototype_walk(value, type_ref) { + crate::value::TAG_TRUE + } else { + TAG_FALSE + }); + } + // A builtin constructor held in a VARIABLE — `const RS = ReadableStream; body + // instanceof RS` — arrives here as the ClosureHeader-backed function installed + // on `globalThis`, so none of the class-id paths above match and the prototype + // walk below returns false. Codegen only special-cases the *static identifier* + // form (`body instanceof ReadableStream`), where it hands the builtin class id + // straight to `js_instanceof`, which brand-checks these natively-backed values + // via the stream / fetch kind probes (their instances are handles, not heap + // objects with a real prototype chain). + // + // Minified bundles almost always alias constructors into locals, so the + // variable form is the common one in the wild: `x instanceof ` for + // ReadableStream / Response / Headers silently returned `false` while Node + // returns `true`. That made a large esbuild-bundled CLI app mis-detect its + // `fetch()` body, throw "The first argument must be a Readable, a + // ReadableStream, or an async iterable", and abort its background + // tar-stream downloads entirely. + // + // Recover the builtin's name from the constructor closure (recorded by + // `set_bound_native_closure_name` when globalThis is populated) and reuse the + // static path's class id, so both spellings agree. + if let Some(class_id) = builtin_ctor_class_id_from_value(type_ref) { + return js_instanceof(value, class_id); + } + // #6558: `e instanceof WebAssembly.CompileError` (and LinkError / + // RuntimeError). These constructors live on the WebAssembly NAMESPACE — + // not on `globalThis`, so the builtin-name path above never resolves + // them — and their instances are ErrorHeader-backed values with no + // prototype chain reaching the namespace ctor's `.prototype`, so the + // ordinary prototype walk below can't brand them either. Identify the + // ctor by its dedicated thunk func_ptr (GC-move-safe) and brand-check + // the instance by its error `.name`. + if let Some(matches) = super::global_this::webassembly_error_ctor_instanceof(value, type_ref) { + return f64::from_bits(if matches { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }); + } + // #6558 sibling: `mod instanceof WebAssembly.Module` for the wasm-host + // module wrapper. Its `[[Prototype]]` does not reach the namespace ctor's + // `.prototype`, so brand-check its GC-aware internal wrapper identity. + // Only a positive match short-circuits here; a miss returns `None` so the + // value still flows to the prototype walk below (how `WebAssembly.Memory` + // instances resolve, and how a foreign object answers `false`). + if let Some(true) = super::global_this::webassembly_value_ctor_instanceof(value, type_ref) { + return f64::from_bits(crate::value::TAG_TRUE); + } + if let Some((module, method)) = unsafe { bound_native_callable_module_and_method(type_ref) } { + if module == "stream" + && matches!( + method.as_str(), + "Readable" | "Writable" | "Duplex" | "Transform" | "PassThrough" | "Stream" + ) + && (crate::node_stream::is_classic_stream_instance_of(value, method.as_str()) + || super::tls_constructor_prototype_is_instance_of(value, method.as_str())) + { + 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 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 + } else { + TAG_FALSE + }, + ); + } + if module == "events" + && method == "EventEmitterAsyncResource" + && is_event_emitter_async_resource_instance_value(value) + { + return f64::from_bits(crate::value::TAG_TRUE); + } + if module == "async_hooks" + && matches!(method.as_str(), "AsyncLocalStorage" | "AsyncResource") + { + let raw = value_addr(value); + let matched = if method == "AsyncResource" { + crate::async_hooks::resolve_async_resource_handle(raw as i64).is_some() + || (crate::value::addr_class::is_plausible_heap_addr(raw) + && ordinary_has_instance_prototype_walk(value, type_ref)) + } else { + let candidate = small_native_handle_id(value).unwrap_or(raw as i64); + let native = (candidate != 0) && { + super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { + let property = b"getStore"; + let result = + unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; + value_is_callable(result) + }) + }; + native + || (crate::value::addr_class::is_plausible_heap_addr(raw) + && ordinary_has_instance_prototype_walk(value, type_ref)) + }; + return f64::from_bits(if matched { + crate::value::TAG_TRUE + } else { + TAG_FALSE + }); + } + if module == "tty" + && matches!(method.as_str(), "ReadStream" | "WriteStream") + && crate::tty::is_tty_stream_instance(value, method.as_str()) + { + return f64::from_bits(crate::value::TAG_TRUE); + } + if module == "fs" { + let matched = match method.as_str() { + "Stats" => crate::fs::is_fs_stats_instance_value(value), + "Dir" => crate::fs::is_fs_dir_instance_value(value), + "Dirent" => crate::fs::is_fs_dirent_instance_value(value), + "ReadStream" | "FileReadStream" | "WriteStream" | "FileWriteStream" + | "Utf8Stream" => crate::fs::is_fs_stream_instance_value(value, method.as_str()), + _ => false, + }; + if matched { + return f64::from_bits(crate::value::TAG_TRUE); + } + } + if module == "tls" + && method == "SecureContext" + && crate::tls::is_secure_context_instance(value) + { + return f64::from_bits(crate::value::TAG_TRUE); + } + if module == "tls" && matches!(method.as_str(), "Server" | "TLSSocket") { + let want = if method == "Server" { 1 } else { 2 }; + if let (Some(handle), Some(probe)) = ( + small_native_handle_id(value), + crate::object::tls_handle_kind_probe(), + ) { + return f64::from_bits(if unsafe { probe(handle) } == want { + crate::value::TAG_TRUE + } else { + TAG_FALSE + }); + } + } + if module == "wasi" && method == "WASI" && crate::wasi::is_wasi_instance(value) { + return f64::from_bits(crate::value::TAG_TRUE); + } + if module == "repl" { + let matched = match method.as_str() { + "Recoverable" => crate::node_repl::is_recoverable_value(value), + "REPLServer" => crate::node_repl::is_repl_server_value(value), + _ => false, + }; + if matched { + return f64::from_bits(crate::value::TAG_TRUE); + } + } + // #2689: `net.Stream` is an alias for `net.Socket`; both should match + // a live socket handle via the runtime probe. + if module == "net" && matches!(method.as_str(), "Socket" | "Stream") { + if let Some(handle) = small_native_handle_id(value) { + let net_socket = crate::object::net_socket_handle_probe() + .map(|probe| unsafe { probe(handle) }) + .unwrap_or(false); + let tls_socket = crate::object::tls_handle_kind_probe() + .map(|probe| unsafe { probe(handle) == 2 }) + .unwrap_or(false); + if net_socket || tls_socket { + return f64::from_bits(crate::value::TAG_TRUE); + } + } + } + if module == "console" + && method == "Console" + && crate::builtins::is_console_instance_value(value) + { + return f64::from_bits(crate::value::TAG_TRUE); + } + if module == "crypto" && method == "KeyObject" { + let addr = value_addr(value); + return if addr != 0 + && (crate::buffer::is_secret_key(addr) + || crate::buffer::asymmetric_key_meta(addr).is_some()) + { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + if module == "perf_hooks" { + let class_id = match method.as_str() { + "Performance" => crate::perf_hooks::CLASS_ID_PERFORMANCE, + "PerformanceEntry" => crate::perf_hooks::CLASS_ID_PERFORMANCE_ENTRY, + "PerformanceMark" => crate::perf_hooks::CLASS_ID_PERFORMANCE_MARK, + "PerformanceMeasure" => crate::perf_hooks::CLASS_ID_PERFORMANCE_MEASURE, + "PerformanceObserverEntryList" => { + crate::perf_hooks::CLASS_ID_PERFORMANCE_OBSERVER_ENTRY_LIST + } + "PerformanceResourceTiming" => { + crate::perf_hooks::CLASS_ID_PERFORMANCE_RESOURCE_TIMING + } + _ => 0, + }; + if class_id != 0 { + return js_instanceof(value, class_id); + } + } + } + if is_buffer_constructor_value(type_ref) { + return js_instanceof(value, crate::buffer::BUFFER_TYPE_ID); + } + if let Some(name) = identify_global_builtin_constructor(type_ref) { + match name { + "Crypto" => { + return if is_native_module_namespace_value(value, "crypto.webcrypto") { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + "SubtleCrypto" => { + return if is_native_module_namespace_value(value, "crypto.subtle") { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + "CryptoKey" => { + let addr = value_addr(value); + return if addr != 0 && crate::buffer::crypto_key_meta(addr).is_some() { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + _ => {} + } + let class_id = global_builtin_constructor_class_id(name); + if class_id != 0 { + let r = js_instanceof(value, class_id); + if r.to_bits() == crate::value::TAG_TRUE { + return r; + } + // #5989: an object that inherits a builtin's prototype via + // `Fn.prototype = Object.create(Builtin.prototype)` is `instanceof + // Builtin` per the spec even though it carries no builtin class id — + // react-server-dom's flight Chunk inherits `Promise.prototype` this + // way, so `chunk instanceof Promise` must be true. Walk the real + // [[Prototype]] chain against `Builtin.prototype` before answering + // false. + if ordinary_has_instance_prototype_walk(value, type_ref) { + return f64::from_bits(crate::value::TAG_TRUE); + } + return f64::from_bits(TAG_FALSE); + } + } + if crate::node_submodules::is_diagnostics_channel_constructor_value(type_ref) { + return if crate::node_submodules::diagnostics_channel_is_channel_instance_value(value) { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + // `inst instanceof Intl.`: Intl instances are plain heap objects whose + // `[[Prototype]]` is `Intl..prototype` but carry no class-id, so the + // arms above can't match them. Walk their static-prototype chain. + // `Intl.*` brand checks. Behind `intl-namespace`: with the feature off no + // Intl constructor value can exist (the namespace install is a no-op), so + // the probe could never match — and skipping it keeps this always-live + // dispatcher from statically pinning every Intl constructor thunk (~204 KB). + #[cfg(feature = "intl-namespace")] + if let Some(is_inst) = crate::intl::intl_instanceof(value, type_ref) { + return if is_inst { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(TAG_FALSE) + }; + } + js_instanceof_dynamic_tail(value, type_ref) +} diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs new file mode 100644 index 0000000000..7e512bb302 --- /dev/null +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -0,0 +1,738 @@ +//! `js_instanceof` — the static (compile-time-known-class-id) form of +//! `instanceof`, and the per-built-in dispatch ladder behind it. +//! +//! Split out of `instanceof.rs` for the file-size cap. Pure relocation — +//! no logic changes; see `super::*` for every helper this calls. + +use super::*; + +#[no_mangle] +pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { + const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; + const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; + let true_val = f64::from_bits(TAG_TRUE); + let false_val = f64::from_bits(TAG_FALSE); + + if class_id == 0 { + return false_val; + } + // `proxy instanceof C` follows the proxy's prototype chain, which forwards + // to the target (absent a `getPrototypeOf` trap) — so unwrap to the target + // before walking the class chain. The proxy is a small id with no chain of + // its own. (drizzle's aliased-column proxies + `is(value, type)`.) + let mut value = value; + { + let mut depth = 0; + while depth < 16 && crate::proxy::js_proxy_is_proxy(value) != 0 { + value = crate::proxy::js_proxy_target(value); + depth += 1; + } + } + // User-defined `Symbol.hasInstance` takes precedence over the built-in + // prototype-chain walk — and over the ordinary class-chain fast path below. + // `new C() instanceof C` must run a class-level `@@hasInstance` rather than + // short-circuit on the chain (the hook can return `false` for a real + // instance), so both hook forms are consulted here, ahead of that walk. + // + // Form 1: the HIR lifts `static [Symbol.hasInstance](v)` to a top-level + // function `__perry_wk_hasinstance_` and the LLVM backend registers a + // pointer to it against the class id at module init. + if let Some(func_ptr) = lookup_has_instance_hook(class_id) { + let hook: extern "C" fn(f64) -> f64 = unsafe { std::mem::transmute(func_ptr as *const u8) }; + let result = hook(value); + // Normalize: any truthy NaN-boxed bool stays as the TAG_TRUE/FALSE + // sentinel. User-written `return typeof v === "number" && ...` + // already returns a NaN-boxed bool, so this is usually a no-op. + let rbits = result.to_bits(); + if rbits == TAG_TRUE || rbits == TAG_FALSE { + return result; + } + // Fallback: treat as truthy → TRUE, zero/undefined → FALSE. + if result.is_nan() && rbits & 0xFFFF_0000_0000_0000 == 0x7FFC_0000_0000_0000 { + return false_val; + } + if result == 0.0 || result.is_nan() { + return false_val; + } + return true_val; + } + + // Form 2: the `Object.defineProperty(C, Symbol.hasInstance, { value: fn })` + // form (zod 4) stores the closure in the class static-symbol table. Read it + // off the class id (OWN lookup only — never resolves Function.prototype's + // default @@hasInstance thunk, so no recursion). A present-but-non-callable + // value throws; only `null`/`undefined` falls through to the chain. + // + // The latch check is what keeps `well_known_symbol("hasInstance")` — a + // string-keyed interning probe — off the path entirely in the (dominant) + // case where no class in the program declares any static Symbol member. + if crate::symbol::CLASS_STATIC_SYMBOLS_LATCH.is_armed() { + let hi_sym = crate::symbol::well_known_symbol("hasInstance"); + if !hi_sym.is_null() { + let hi_f64 = f64::from_bits(crate::value::JSValue::pointer(hi_sym as *const u8).bits()); + if let Some(vb) = crate::symbol::class_static_symbol_lookup(class_id, hi_f64) { + let cb = f64::from_bits(vb); + if let HasInstanceOutcome::Result(r) = dispatch_own_has_instance(cb, value) { + return r; + } + } + } + } + + // Subclass-of-built-in: see `subclass_of_builtin_reaches`. + if subclass_of_builtin_reaches(value, class_id) { + return true_val; + } + // Temporal reference types (`d instanceof Temporal.Duration`, …). A Temporal + // value is a NaN-boxed pointer to a brand-tagged cell, not an ObjectHeader + // with a class chain, so probe the cell's brand kind directly. Keep the band + // in sync with perry-runtime/src/temporal/mod.rs. + if (crate::temporal::CLASS_ID_TEMPORAL_FIRST..=crate::temporal::CLASS_ID_TEMPORAL_LAST) + .contains(&class_id) + { + return if crate::temporal::temporal_value_matches_class_id(value, class_id) { + true_val + } else { + false_val + }; + } + // `value instanceof Function` — true for any callable value. Per + // `OrdinaryHasInstance`, every Perry function (declaration, expression, + // arrow, method, bound function, native handle, built-in constructor) + // has `Function.prototype` in its prototype chain. Keep `CLASS_ID_FUNCTION` + // in sync with perry-codegen/src/expr/instance_misc1.rs. + if class_id == CLASS_ID_FUNCTION { + return if value_is_callable(value) { + true_val + } else { + false_val + }; + } + // Keep in sync with perry-codegen/src/expr/instance_misc1.rs. + let classic_stream_name = match class_id { + 0xFFFF0070 => Some("Stream"), + 0xFFFF0071 => Some("Readable"), + 0xFFFF0072 => Some("Writable"), + 0xFFFF0073 => Some("Duplex"), + 0xFFFF0074 => Some("Transform"), + 0xFFFF0075 => Some("PassThrough"), + _ => None, + }; + if let Some(name) = classic_stream_name { + return if crate::node_stream::is_classic_stream_instance_of(value, name) + || super::tls_constructor_prototype_is_instance_of(value, name) + { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_EVENT_EMITTER { + return if is_event_emitter_instance_value(value) + || super::tls_constructor_prototype_is_instance_of(value, "EventEmitter") + { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_EVENT_EMITTER_ASYNC_RESOURCE { + return if is_event_emitter_async_resource_instance_value(value) { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_ASYNC_RESOURCE { + return if crate::async_hooks::resolve_async_resource_handle(value_addr(value) as i64) + .is_some() + { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_ASYNC_LOCAL_STORAGE { + let candidate = small_native_handle_id(value).unwrap_or(value_addr(value) as i64); + let matched = candidate != 0 && { + super::class_handles::handle_property_dispatch().is_some_and(|dispatch| { + let property = b"getStore"; + let result = unsafe { dispatch(candidate, property.as_ptr(), property.len()) }; + value_is_callable(result) + }) + }; + return if matched { true_val } else { false_val }; + } + if class_id == CLASS_ID_NET_SOCKET { + return if let Some(handle) = small_native_handle_id(value) { + let net_socket = crate::object::net_socket_handle_probe() + .map(|probe| unsafe { probe(handle) }) + .unwrap_or(false); + let tls_socket = crate::object::tls_handle_kind_probe() + .map(|probe| unsafe { probe(handle) == 2 }) + .unwrap_or(false); + if net_socket || tls_socket { + true_val + } else { + false_val + } + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_STATS_EXPORT { + return if crate::fs::is_fs_stats_instance_value(value) { + true_val + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_DIR { + return if crate::fs::is_fs_dir_instance_value(value) { + true_val + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_DIRENT { + return if crate::fs::is_fs_dirent_instance_value(value) { + true_val + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_READ_STREAM { + return if crate::fs::is_fs_stream_instance_value(value, "ReadStream") { + true_val + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_WRITE_STREAM { + return if crate::fs::is_fs_stream_instance_value(value, "WriteStream") { + true_val + } else { + false_val + }; + } + if class_id == crate::fs::CLASS_ID_FS_UTF8_STREAM { + return if crate::fs::is_fs_stream_instance_value(value, "Utf8Stream") { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_CRYPTO { + return if is_native_module_namespace_value(value, "crypto.webcrypto") { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_SUBTLE_CRYPTO { + return if is_native_module_namespace_value(value, "crypto.subtle") { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_CRYPTO_KEY { + let addr = value_addr(value); + return if addr != 0 && crate::buffer::crypto_key_meta(addr).is_some() { + true_val + } else { + false_val + }; + } + + let bits = value.to_bits(); + let jsval = crate::JSValue::from_bits(bits); + + // Native/exotic subclass instances (typed arrays, ArrayBuffers, boxed + // primitives, Dates, …) do not carry a Perry `ObjectHeader.class_id`. + // Their constructor records the distinct newTarget prototype in the + // prototype side table instead. Honor that chain for user class ids. + if is_class_id_registered(class_id) { + let addr = value_addr(value); + if addr != 0 && super::prototype_chain::object_static_prototype(addr).is_some() { + let constructor = super::class_constructor_ref_value(class_id); + return if ordinary_has_instance_prototype_walk(value, constructor) { + true_val + } else { + false_val + }; + } + } + + // Special handling for Uint8Array/Buffer (class_id 0xFFFF0004) + // Perry buffers are raw BufferHeader pointers bitcast to f64 (not NaN-boxed), + // so the normal POINTER_TAG check doesn't work for them. + // We use a thread-local buffer registry to identify buffer pointers. + if class_id == crate::buffer::BUFFER_TYPE_ID { + // Check if NaN-boxed pointer + if jsval.is_pointer() { + let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::buffer::is_registered_buffer(addr) { + return true_val; + } + } + // Check if raw pointer (buffer values are bitcast, not NaN-boxed) + let top16 = (bits >> 48) as u16; + if top16 == 0 && bits >= 0x1000 && crate::buffer::is_registered_buffer(bits as usize) { + return true_val; + } + return false_val; + } + + // ArrayBuffer — Perry models ArrayBuffer storage with BufferHeader values + // marked in a side registry. They can arrive either NaN-boxed or as raw + // buffer pointers, matching the Buffer/Uint8Array path above. + const CLASS_ID_ARRAY_BUFFER: u32 = 0xFFFF0025; + const CLASS_ID_SHARED_ARRAY_BUFFER: u32 = 0xFFFF002E; + if class_id == CLASS_ID_ARRAY_BUFFER || class_id == CLASS_ID_SHARED_ARRAY_BUFFER { + 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 + } + }; + let matches_brand = if class_id == CLASS_ID_SHARED_ARRAY_BUFFER { + crate::buffer::is_shared_array_buffer(addr) + } else { + crate::buffer::is_array_buffer(addr) + }; + if addr != 0 && crate::buffer::is_registered_buffer(addr) && matches_brand { + return true_val; + } + return false_val; + } + + // #1545: Web Streams `instanceof ReadableStream` / `instanceof + // WritableStream`. Stream handles are numeric `id as f64`, so consult the + // stdlib kind-probe (1 = readable, 2 = writable) rather than the class + // chain. Covers `ts.readable instanceof ReadableStream`, + // `rs.pipeThrough(ts) instanceof ReadableStream`, etc. + // kind probe values: 1 = readable, 2 = writable, 5 = transform + // (3 = reader, 4 = writer — not user-facing instanceof targets here). + const CLASS_ID_READABLE_STREAM: u32 = 0xFFFF0060; + const CLASS_ID_WRITABLE_STREAM: u32 = 0xFFFF0061; + const CLASS_ID_TRANSFORM_STREAM: u32 = 0xFFFF0062; + if class_id == CLASS_ID_READABLE_STREAM + || class_id == CLASS_ID_WRITABLE_STREAM + || class_id == CLASS_ID_TRANSFORM_STREAM + { + if value.is_finite() && value > 0.0 && value.fract() == 0.0 { + if let Some(probe) = crate::object::stream_handle_kind_probe() { + let kind = unsafe { probe(value as usize) }; + let want = match class_id { + CLASS_ID_READABLE_STREAM => 1, + CLASS_ID_WRITABLE_STREAM => 2, + _ => 5, // CLASS_ID_TRANSFORM_STREAM + }; + if kind == want { + return true_val; + } + } + } + return false_val; + } + + // WHATWG fetch: `instanceof Response` / `Request` / `Headers` / `Blob` / + // `File`. + // These are pointer-tagged small-integer handles (stdlib fetch registries), + // not heap objects, so consult the stdlib fetch kind-probe rather than the + // class chain. Without this, Hono's `res instanceof Response` route-fallback + // guard sees `false` and skips the fallback, escaping a bare sentinel. + const CLASS_ID_RESPONSE: u32 = 0xFFFF0028; + const CLASS_ID_REQUEST: u32 = 0xFFFF0029; + const CLASS_ID_HEADERS: u32 = 0xFFFF002A; + const CLASS_ID_BLOB: u32 = 0xFFFF0026; + const CLASS_ID_FILE: u32 = 0xFFFF002F; + if class_id == CLASS_ID_RESPONSE + || class_id == CLASS_ID_REQUEST + || class_id == CLASS_ID_HEADERS + || class_id == CLASS_ID_BLOB + || class_id == CLASS_ID_FILE + { + let want = match class_id { + CLASS_ID_RESPONSE => 1u8, + CLASS_ID_REQUEST => 2, + CLASS_ID_HEADERS => 3, + CLASS_ID_BLOB => 4, + _ => 5, // CLASS_ID_FILE + }; + if let Some(handle) = small_native_handle_id(value) { + if let Some(probe) = crate::object::fetch_handle_kind_probe() { + let kind = unsafe { probe(handle as usize) }; + // File inherits Blob, so a File handle satisfies both brands. + if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { + return true_val; + } + } + } + // `class X extends Request/Response` instance: a heap object that + // stashes the underlying native fetch handle id under + // `__perry_fetch_handle__`. Unwrap and probe so `sub instanceof + // Request` is true, matching a bare handle. + if jsval.is_pointer() { + let raw = jsval.as_pointer::() as usize; + if let Some(id) = unsafe { crate::object::fetch_subclass_handle_id(raw) } { + if let Some(probe) = crate::object::fetch_handle_kind_probe() { + let kind = unsafe { probe(id as usize) }; + if kind == want || (class_id == CLASS_ID_BLOB && kind == 5) { + return true_val; + } + } + } + } + // A Blob can also be a real heap object allocated with CLASS_ID_BLOB + // (e.g. `stream/consumers`.`blob()` and `blob_value_from_bytes`), not + // just a small fetch-registry handle. Match it by its own class id so + // `blob instanceof Blob` is true for that representation too. + if class_id == CLASS_ID_BLOB && jsval.is_pointer() { + let obj = jsval.as_pointer::(); + if crate::value::addr_class::is_above_handle_band(obj as usize) + && unsafe { (*obj).class_id } == CLASS_ID_BLOB + { + return true_val; + } + } + return false_val; + } + + // Built-in JS types Map / Set / RegExp / Date — Perry doesn't define + // user classes for these, so we use reserved class IDs and detect via + // the per-type registries (MAP_REGISTRY / SET_REGISTRY / REGEX_POINTERS) + // or, for Date, by checking that the value is a finite f64 timestamp. + const CLASS_ID_DATE: u32 = 0xFFFF0020; + const CLASS_ID_REGEXP: u32 = 0xFFFF0021; + const CLASS_ID_MAP: u32 = 0xFFFF0022; + const CLASS_ID_SET: u32 = 0xFFFF0023; + if class_id == CLASS_ID_DATE { + // A Perry Date is a NaN-boxed pointer to a `DateCell` (#2089). Its + // identity is the cell's `GcHeader` type, so `new Date(NaN)` (an + // Invalid Date — a cell whose time value is NaN) matches just like + // any other Date, and a plain number never matches. + if crate::date::is_date_value(value) { + return true_val; + } + return false_val; + } + if class_id == CLASS_ID_MAP { + if jsval.is_pointer() { + let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::map::is_registered_map(addr) { + return true_val; + } + } + return false_val; + } + if class_id == CLASS_ID_SET { + if jsval.is_pointer() { + let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::set::is_registered_set(addr) { + return true_val; + } + } + return false_val; + } + // #5834: `x instanceof WeakMap`/`WeakSet` for a REAL instance. These + // reserved ids (kept in sync with perry-codegen/src/expr/instance_misc1.rs) + // are distinct from the runtime `CLASS_ID_WEAKMAP`/`CLASS_ID_WEAKSET` + // stamped on actual instances (weakref.rs) — the subclass-chain walk above + // only matches a `class S extends WeakMap {}` instance (whose chain reaches + // this reserved id), so a genuine `new WeakMap()` still needs its own probe + // here, same shape as Map/Set above. + const CLASS_ID_WEAKMAP_RESERVED: u32 = 0xFFFF002C; + const CLASS_ID_WEAKSET_RESERVED: u32 = 0xFFFF002D; + if class_id == CLASS_ID_WEAKMAP_RESERVED { + return if crate::object::weak_class_id_from_receiver(value) + == Some(crate::weakref::CLASS_ID_WEAKMAP) + { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_WEAKSET_RESERVED { + return if crate::object::weak_class_id_from_receiver(value) + == Some(crate::weakref::CLASS_ID_WEAKSET) + { + true_val + } else { + false_val + }; + } + if class_id == CLASS_ID_REGEXP { + if jsval.is_pointer() { + let addr = (bits & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::regex::is_regex_pointer(addr as *const u8) { + return true_val; + } + } + return false_val; + } + if class_id == CLASS_ID_PROMISE { + if let Some(matches) = recorded_prototype_instanceof_builtin(value, "Promise") { + return if matches { true_val } else { false_val }; + } + return if crate::promise::js_value_is_promise(value) != 0 { + true_val + } else { + false_val + }; + } + + // `Object` — ECMAScript spec: `x instanceof Object` is true for any + // non-primitive (every object/array/function/Map/Set/Buffer/RegExp/ + // Date/typed-array/Promise/etc.). The codegen maps `Object` to this + // reserved id (#585 follow-up: pre-#585 fix this case worked by + // accident because the codegen produced `class_id = 0` and the + // runtime returned true via `0 == 0` on the obj_class_id check). + const CLASS_ID_OBJECT: u32 = 0xFFFF0050; + if class_id == CLASS_ID_OBJECT { + if jsval.is_pointer() { + // A Symbol is a POINTER_TAG heap allocation but a PRIMITIVE, not an + // object, so `Symbol() instanceof Object` is false (the comment + // above says "any non-primitive"). Every other primitive is + // non-pointer-tagged and already falls through below. #6587 review. + if unsafe { crate::symbol::js_is_symbol(value) != 0 } { + return false_val; + } + // Covers every heap object, including a Date (now a NaN-boxed + // `DateCell` pointer — #2089) and an Invalid Date. + return true_val; + } + let top16 = (bits >> 48) as u16; + if top16 == 0 && bits >= 0x1000 { + let addr = bits as usize; + if crate::buffer::is_registered_buffer(addr) + || crate::set::is_registered_set(addr) + || crate::map::is_registered_map(addr) + || crate::typedarray::lookup_typed_array_kind(addr).is_some() + { + return true_val; + } + } + return false_val; + } + + // Array — Perry arrays are heap allocations with `GC_TYPE_ARRAY` in + // their gc_header (one byte at obj-8). Pointer can arrive NaN-boxed + // (POINTER_TAG) or as a raw bitcast f64; handle both. Lazy arrays + // (Phase 5 JSON.parse result) are also arrays from the user's + // perspective — must return true without force-materializing. + const CLASS_ID_ARRAY: u32 = 0xFFFF0024; + if class_id == CLASS_ID_ARRAY { + // 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). + // The pointer can arrive as either a NaN-boxed POINTER_TAG value or a + // raw bitcast f64, so handle both forms. + if (0xFFFF0030..=0xFFFF003B).contains(&class_id) { + 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 { + if let Some(actual_kind) = crate::typedarray::lookup_typed_array_kind(addr) { + let want_id = crate::typedarray::class_id_for_kind(actual_kind); + if want_id == class_id { + return true_val; + } + } + } + return false_val; + } + + // Only objects (pointers) can be instances of classes + if !jsval.is_pointer() { + return false_val; + } + + // Get the object pointer + let obj_ptr = jsval.as_pointer::(); + if obj_ptr.is_null() { + return false_val; + } + + // Refs #421: NaN-boxed POINTER_TAG values whose unboxed payload is a + // small registry id (Web Fetch handles, sockets, DB connections, etc.) + // are NOT real ObjectHeader pointers — reading the GC header at + // `obj_ptr - 8` would SIGSEGV on unmapped memory. They aren't instances + // of any user-defined class either, so return false unconditionally. + if crate::value::addr_class::is_handle_band(obj_ptr as usize) { + return false_val; + } + + unsafe { + // Special handling for built-in Error and its subclasses (TypeError, RangeError, etc.). + // ErrorHeader uses GC_TYPE_ERROR; we match by error_kind against the requested CLASS_ID_*. + let gc_header = + (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + let gc_type = (*gc_header).obj_type; + if gc_type == crate::gc::GC_TYPE_ERROR { + let err_ptr = obj_ptr as *const crate::error::ErrorHeader; + let kind = (*err_ptr).error_kind; + if class_id == crate::event_target::CLASS_ID_DOM_EXCEPTION { + return if crate::event_target::is_dom_exception_error(err_ptr) { + true_val + } else { + false_val + }; + } + let builtin_name = match class_id { + crate::error::CLASS_ID_ERROR => Some("Error"), + crate::error::CLASS_ID_TYPE_ERROR => Some("TypeError"), + crate::error::CLASS_ID_RANGE_ERROR => Some("RangeError"), + crate::error::CLASS_ID_REFERENCE_ERROR => Some("ReferenceError"), + crate::error::CLASS_ID_SYNTAX_ERROR => Some("SyntaxError"), + crate::error::CLASS_ID_EVAL_ERROR => Some("EvalError"), + crate::error::CLASS_ID_URI_ERROR => Some("URIError"), + crate::error::CLASS_ID_AGGREGATE_ERROR => Some("AggregateError"), + _ => None, + }; + if let Some(name) = builtin_name { + if let Some(matches) = recorded_prototype_instanceof_builtin(value, name) { + return if matches { true_val } else { false_val }; + } + } + return match class_id { + crate::error::CLASS_ID_ERROR => true_val, + crate::error::CLASS_ID_TYPE_ERROR => { + if kind == crate::error::ERROR_KIND_TYPE_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_RANGE_ERROR => { + if kind == crate::error::ERROR_KIND_RANGE_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_REFERENCE_ERROR => { + if kind == crate::error::ERROR_KIND_REFERENCE_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_SYNTAX_ERROR => { + if kind == crate::error::ERROR_KIND_SYNTAX_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_EVAL_ERROR => { + if kind == crate::error::ERROR_KIND_EVAL_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_URI_ERROR => { + if kind == crate::error::ERROR_KIND_URI_ERROR { + true_val + } else { + false_val + } + } + crate::error::CLASS_ID_AGGREGATE_ERROR => { + if kind == crate::error::ERROR_KIND_AGGREGATE_ERROR { + true_val + } else { + false_val + } + } + _ => false_val, + }; + } + + if gc_type == crate::gc::GC_TYPE_OBJECT { + if let Some(matches) = + crate::perf_hooks::is_perf_hooks_shape_instance_of(value, class_id) + { + return if matches { true_val } else { false_val }; + } + if let Some(matches) = + crate::perf_hooks::is_perf_entry_object_instance_of(obj_ptr, class_id) + { + return if matches { true_val } else { false_val }; + } + } + + // For user-defined classes that extend Error: `myErr instanceof Error` should be true. + if class_id == crate::error::CLASS_ID_ERROR { + // #9940: a function-local class declaration gets a fresh class + // object on every evaluation, but all evaluations share its + // compile-time class id. A constructor factory can therefore + // evaluate `class Definition extends Error {}`, then later + // evaluate the same declaration with an Object parent. The class + // registry is keyed by the shared id and is necessarily + // last-wins; the instance's recorded evaluation prototype is the + // authoritative chain. Zod's `$constructor` has exactly this + // shape, and its later schema classes made an earlier ZodError + // fail `instanceof Error` even though getPrototypeOf still showed + // `ZodError -> Error -> Object`. + 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; + } + } + + // Check if the object's class_id matches directly + let obj_class_id = (*obj_ptr).class_id; + if class_id == crate::event_target::CLASS_ID_EVENT + && obj_class_id == crate::event_target::CLASS_ID_CUSTOM_EVENT + { + return true_val; + } + // Walk up the inheritance chain using the class registry. #7575: the + // walk also follows the generic-origin edge, so a dynamic RHS holding a + // generic class (`const C = Gen; x instanceof C`) matches an instance of + // one of its specializations. + if class_chain_reaches(obj_class_id, class_id) { + return true_val; + } + + false_val + } +} diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index 4ab811e64c..dc1c5ed290 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -95,6 +95,7 @@ crates/perry-runtime/src/object/field_get_set.rs | * | pre-existing GcHeader pro crates/perry-runtime/src/object/field_set_by_name | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/object/global_this.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/object/instanceof.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up +crates/perry-runtime/src/object/instanceof/static_dispatch.rs | * | same pre-existing GcHeader probe, moved from instanceof.rs by the 2,000-line file split (js_instanceof); migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/object/mod.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/object/native_call_method.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/object/object_ops.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index 50de5d427e..4a9ee37fb0 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -125,7 +125,8 @@ 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 | 6 +handle-floor | crates/perry-runtime/src/object/instanceof.rs | 2 +handle-floor | crates/perry-runtime/src/object/instanceof/static_dispatch.rs | 4 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_10624_instanceof_classexprfresh_shared_id.ts b/test-files/test_gap_10624_instanceof_classexprfresh_shared_id.ts new file mode 100644 index 0000000000..a76d8688d4 --- /dev/null +++ b/test-files/test_gap_10624_instanceof_classexprfresh_shared_id.ts @@ -0,0 +1,82 @@ +// Gap test for #10624: `instanceof` against a `ClassExprFresh` parent must +// not resolve by shared (template) class id. Each per-evaluation class +// object's OWN pinned heritage must be honored even after a LATER +// evaluation of the same factory has overwritten the shared last-write-wins +// slot that `instanceof`'s class-chain walk otherwise reads. + +function extend(Base: any, tag: string) { + return class extends Base { + getTag() { + return tag; + } + }; +} + +class Root { + kind() { + return "root"; + } +} +class RootAlt { + kind() { + return "alt"; + } +} + +// Same call site invoked twice (a loop), so both evaluations share Perry's +// internal template class id - the shape #10624 is about. +const bases = [Root, RootAlt]; +const tags = ["r1", "r2"]; +const evaluations: any[] = []; +for (let i = 0; i < bases.length; i++) { + evaluations.push(extend(bases[i], tags[i])); +} +const A = evaluations[0]; // extends Root +const B = evaluations[1]; // extends RootAlt (later eval; overwrites the shared dynamic-parent slot) + +// Construction order interleaved: build from the EARLIER evaluation (A) +// *after* the LATER evaluation (B) has already run. +const earlyInstance = new A(); +console.log("earlyInstance instanceof Root:", earlyInstance instanceof Root); +console.log("earlyInstance instanceof RootAlt:", earlyInstance instanceof RootAlt); +console.log("earlyInstance instanceof A:", earlyInstance instanceof A); +console.log("earlyInstance instanceof B:", earlyInstance instanceof B); +console.log("earlyInstance.getTag():", earlyInstance.getTag()); + +const lateInstance = new B(); +console.log("lateInstance instanceof Root:", lateInstance instanceof Root); +console.log("lateInstance instanceof RootAlt:", lateInstance instanceof RootAlt); +console.log("lateInstance.getTag():", lateInstance.getTag()); + +// instanceof in both directions, re-checked after more evaluations ran. +console.log("earlyInstance instanceof RootAlt (again):", earlyInstance instanceof RootAlt); +console.log("lateInstance instanceof Root (again):", lateInstance instanceof Root); + +// A THIRD evaluation, constructed immediately (control - the "latest" +// evaluation was never the stale case, so this must always have worked). +const C = extend(Root, "r3"); +const freshInstance = new C(); +console.log("freshInstance instanceof Root:", freshInstance instanceof Root); +console.log("freshInstance instanceof RootAlt:", freshInstance instanceof RootAlt); + +// Two-level subclass: a SECOND dynamic factory evaluated against a specific +// evaluation of the FIRST one (A, not B), checked after yet another +// evaluation of the first factory has run and overwritten its shared slot +// again. Checked against Root/RootAlt only (distinct classes, distinct +// class ids) - not against A/D directly, which exercises a separate, +// pre-existing limitation (instanceof against a *specific* sibling +// evaluation of the same template, referenced directly as the RHS, is not +// this issue's mechanism). +function extendAgain(Base: any, mark: string) { + return class extends Base { + extra() { + return mark; + } + }; +} +const G = extendAgain(A, "grandchild"); // extends A specifically, i.e. transitively Root +const D = extend(RootAlt, "r4"); // yet another eval of `extend` - overwrites its shared slot again +const grandchildInstance = new G(); +console.log("grandchildInstance instanceof Root:", grandchildInstance instanceof Root); +console.log("grandchildInstance instanceof RootAlt:", grandchildInstance instanceof RootAlt); +console.log("grandchildInstance.extra():", grandchildInstance.extra());