diff --git a/changelog.d/11777-class-super-instanceof-relink.md b/changelog.d/11777-class-super-instanceof-relink.md new file mode 100644 index 0000000000..1480e6d326 --- /dev/null +++ b/changelog.d/11777-class-super-instanceof-relink.md @@ -0,0 +1,9 @@ +`super.m()`, `typeof super.m`, `super.m` as a value and `super.x` now read the +parent prototype as it is when they run, so a patched, deleted or getter-backed +parent method applies, and so does `Object.setPrototypeOf` on the class +prototype or, in a static method, on the class itself. A static `super.s()` no +longer calls an instance method of the same name. After a class prototype is +relinked, `instanceof` follows the new chain, including `instanceof Object` and +an instance whose own prototype was replaced. `C.prototype.__proto__ = X` now +relinks the prototype like `Object.setPrototypeOf` instead of creating an own +property named `__proto__`. diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 7bc2887216..81cd6d8d32 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -24,11 +24,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } return Ok(double_literal(0.0)); }; - // Walk parent chain starting from extends_name. - let mut parent = ctx - .classes - .get(¤t_class_name) - .and_then(|c| c.extends_name.clone()); + // Walk parent chain starting from extends_name. The method tables + // hold INSTANCE methods only: in a static member `super` is the + // parent constructor, which the runtime path resolves. + let mut parent = if ctx.in_static_member { + None + } else { + ctx.classes + .get(¤t_class_name) + .and_then(|c| c.extends_name.clone()) + }; let mut resolved_fn: Option = None; // #8040: the class the body actually lives on, so the trailing // parameter shape below is read off the callee we are calling. @@ -344,11 +349,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let Some(current_class_name) = ctx.class_stack.last().cloned() else { return Ok(undef); }; - let immediate_parent = ctx - .classes - .get(¤t_class_name) - .and_then(|c| c.extends_name.clone()); - let mut parent = immediate_parent.clone(); + // As for the call form: in a static member the instance method + // tables do not describe `super`. + let mut parent = if ctx.in_static_member { + None + } else { + ctx.classes + .get(¤t_class_name) + .and_then(|c| c.extends_name.clone()) + }; let mut resolved_fn: Option = None; while let Some(p) = parent { let key = (p.clone(), property.clone()); @@ -358,18 +367,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } parent = ctx.classes.get(&p).and_then(|c| c.extends_name.clone()); } - let Some(fn_name) = resolved_fn else { - // Not a method on the parent chain — `super.prop` then reads the - // property off the parent prototype with `this` as receiver: - // an accessor (getter) is INVOKED, a data property - // (`B.prototype.x = 42`) is returned. Route to the runtime, - // which walks the parent class chain. Refs - // class/super/in-{constructor,getter,methods,setter}. - let parent_cid = immediate_parent - .as_ref() - .and_then(|p| ctx.class_ids.get(p)) - .copied() - .unwrap_or(0); + // `super.prop` reads the property off the home object's current + // `[[Prototype]]` with `this` as receiver: an accessor (getter) is + // INVOKED, a data property (`B.prototype.x = 42`) is returned. The + // runtime reads that chain from the home class id. Refs + // class/super/in-{constructor,getter,methods,setter}. + let home_cid = ctx.class_ids.get(¤t_class_name).copied().unwrap_or(0); + let runtime_get = |ctx: &mut FnCtx<'_>| -> String { let recv_v = if let Some(this_slot) = ctx.this_stack.last().cloned() { ctx.block().load(DOUBLE, &this_slot) } else { @@ -378,12 +382,41 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let key_idx = ctx.strings.intern(property); let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); let key_box = ctx.block().load(DOUBLE, &key_handle_global); - let parent_cid_s = parent_cid.to_string(); - return Ok(ctx.block().call( + let home_cid_s = home_cid.to_string(); + ctx.block().call( DOUBLE, "js_super_accessor_get", - &[(I32, &parent_cid_s), (DOUBLE, &key_box), (DOUBLE, &recv_v)], - )); + &[(I32, &home_cid_s), (DOUBLE, &key_box), (DOUBLE, &recv_v)], + ) + }; + let Some(fn_name) = resolved_fn else { + return Ok(runtime_get(ctx)); + }; + // The method above was resolved along the declared `extends` + // chain, which holds while no prototype surgery touched this + // name (the guard bytes `super.m()` reads too). + let guarded = if home_cid != 0 { + let key_idx = ctx.strings.intern(property); + let slot = (ctx.strings.entry(key_idx).dispatch_hash & 0xffff).to_string(); + let direct_idx = ctx.new_block("super_get.direct"); + let dynamic_idx = ctx.new_block("super_get.dynamic"); + let merge_idx = ctx.new_block("super_get.merge"); + let direct_label = ctx.block_label(direct_idx); + let dynamic_label = ctx.block_label(dynamic_idx); + let merge_label = ctx.block_label(merge_idx); + let ok = crate::lower_call::method_override::emit_prototype_method_guard_ok( + ctx.block(), + &slot, + ); + ctx.block().cond_br(&ok, &direct_label, &dynamic_label); + ctx.current_block = dynamic_idx; + let dynamic_value = runtime_get(ctx); + let dynamic_end = ctx.block().label.clone(); + ctx.block().br(&merge_label); + ctx.current_block = direct_idx; + Some((merge_idx, merge_label, dynamic_value, dynamic_end)) + } else { + None }; // Mirror Expr::FuncRef: route through the singleton wrapper // so callers can invoke via the closure-call ABI. The @@ -413,7 +446,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let blk = ctx.block(); let wrap_info = blk.fn_info_ref(&wrap_name); let closure_handle = blk.call(I64, "js_closure_alloc_singleton", &[(PTR, &wrap_info)]); - Ok(nanbox_pointer_inline(blk, &closure_handle)) + let direct_value = nanbox_pointer_inline(blk, &closure_handle); + let Some((merge_idx, merge_label, dynamic_value, dynamic_end)) = guarded else { + return Ok(direct_value); + }; + let direct_end = ctx.block().label.clone(); + ctx.block().br(&merge_label); + ctx.current_block = merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[(&direct_value, &direct_end), (&dynamic_value, &dynamic_end)], + )) } Expr::SuperPropertySet { diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 1c1e0e2018..6a5cb67d4e 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -11,6 +11,10 @@ use std::collections::HashMap; use std::sync::RwLock; use super::class_registry::call_vtable_method; +use super::class_super_chain::{ + class_super_base, declared_chain_has_relinked_prototype, static_chain_relinked, + super_call_on_live_base, super_call_on_relinked_chain, super_home_owner, +}; use super::ObjectHeader; /// Replace the capture array carried by one heap class-expression value. @@ -881,11 +885,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( // Repeated evaluations can share a template id, so the template parent // table cannot represent their heritage edge. Resolve the method's own // evaluation first, then read its pinned parent. - let lexical_owner = super::field_get_set::current_private_lexical_brand_value(child_class_id) - .or_else(|| { - super::field_get_set::private_evaluation_brand_value(this_value) - .and_then(|owner| pinned_class_object_for_ancestor(owner, child_class_id)) - }); + let lexical_owner = super_home_owner(child_class_id, this_value); let parent_owner = lexical_owner.and_then(|owner| { let object = crate::value::JSValue::from_bits(owner.to_bits()).as_pointer::(); super::class_registry::class_object_pinned_parent(object) @@ -905,16 +905,67 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( }; let _parent_brand = super::field_get_set::PrivateHintBrandScope::new(parent_owner.map(f64::to_bits)); + // `super.name` is a property lookup on the home object's CURRENT + // `[[Prototype]]`, with `this` as the receiver: a patched, deleted or + // accessor parent member and a relinked home all apply. Where the runtime + // models that chain end to end, read it; the declared-chain lookups below + // serve the rest (native and builtin bases, per-evaluation classes). + let is_static = super::class_ref_id(this_value).is_some() + || super::class_registry::is_class_object_value(this_value); + // A static member's declared lookup reads the class function objects (an + // assigned or deleted static ends it), so it is the property lookup unless + // a constructor on the way was relinked. An instance member reaches here + // when the compiler could not resolve the name or a prototype-surgery + // guard byte is set: the declared vtable no longer describes the chain. + let static_entry = if is_static { + super::class_registry::parent_static::lookup_static_method_owner(parent_cid, name) + } else { + None + }; + let mut base = None; + // The probes below can allocate (materializing a prototype or a class + // value), so the receiver and the arguments ride across them in handles; + // the declared-chain paths below read the refreshed copies. + let refreshed_args: Vec; + let (this_value, args_ptr) = if static_entry.is_none() + || super::prototype_chain::any_user_prototype_override() + { + let live_scope = crate::gc::RuntimeHandleScope::new(); + let this_handle = live_scope.root_nanbox_f64(this_value); + let arg_handles = + live_scope.root_nanbox_f64_slice(if args_len > 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len) + } else { + &[] + }); + let live = match static_entry { + Some((owner, _)) => static_chain_relinked(child_class_id, owner), + None => true, + }; + if live { + base = class_super_base(child_class_id, parent_cid, lexical_owner, is_static); + } + refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + ( + this_handle.get_nanbox_f64(), + if refreshed_args.is_empty() { + args_ptr + } else { + refreshed_args.as_ptr() + }, + ) + } else { + (this_value, args_ptr) + }; + if let Some(base) = base { + return super_call_on_live_base(name, this_value, args_ptr, args_len, base); + } // Static-context super call (`super.m()` inside a `static` method): the // receiver is the class constructor (a ClassRef), so resolve the PARENT's // STATIC method (not an instance/prototype method) and invoke it with // `this` bound to the current class. Refs class/super/in-static-methods. - if super::class_ref_id(this_value).is_some() - || super::class_registry::is_class_object_value(this_value) - { - if let Some((func_ptr, param_count, has_rest)) = - super::class_registry::lookup_static_method_in_chain(parent_cid, name) - { + if is_static { + if let Some((_, (func_ptr, param_count, has_rest))) = static_entry { crate::object::static_this_arm_if_unarmed(this_value); let result = if has_rest { // Mirror `js_class_static_method_call`'s rest bundling: fixed @@ -1024,92 +1075,6 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( call_displaced_native_base_method(this_value, name, args_ptr, args_len, undef) } -/// Is the prototype of `cid` or of one of its declared ancestors relinked by a -/// user operation? Asked only after the declared-member lookups missed. -fn declared_chain_has_relinked_prototype(cid: u32) -> bool { - let mut cur = cid; - for _ in 0..32 { - if cur == 0 { - return false; - } - if super::class_registry::class_decl_prototype_relinked(cur) { - return true; - } - match crate::object::get_parent_class_id(cur) { - Some(p) if p != cur => cur = p, - _ => return false, - } - } - false -} - -/// `super.name(...args)` resolved by `read` on a relinked chain: call the value -/// with `this_value` as receiver, or throw the TypeError a call of a -/// non-callable `super.name` throws. -/// -/// # Safety -/// `args_ptr` must point to `args_len` valid `f64`s (or be null when -/// `args_len == 0`). -unsafe fn super_call_on_relinked_chain( - name: &str, - this_value: f64, - args_ptr: *const f64, - args_len: usize, - read: impl FnOnce(*const crate::StringHeader, f64) -> Option, -) -> f64 { - // The key allocation and the read (a getter on the new chain) can collect; - // the receiver and the arguments ride across them in handles. - let scope = crate::gc::RuntimeHandleScope::new(); - let this_handle = scope.root_nanbox_f64(this_value); - let args: Vec = if args_len > 0 && !args_ptr.is_null() { - std::slice::from_raw_parts(args_ptr, args_len).to_vec() - } else { - Vec::new() - }; - let arg_handles = scope.root_nanbox_f64_slice(&args); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let value = if key.is_null() { - None - } else { - read( - key as *const crate::StringHeader, - this_handle.get_nanbox_f64(), - ) - }; - let callable = value.filter(|v| { - let boxed = f64::from_bits(v.bits()); - v.is_pointer() - && ((crate::proxy::js_proxy_is_proxy(boxed) == 1 - && crate::proxy::proxy_wraps_callable(boxed)) - || crate::closure::is_closure_ptr( - crate::value::js_nanbox_get_pointer(boxed) as usize - )) - }); - let Some(method) = callable else { - crate::error::js_throw_type_error_not_a_function( - std::ptr::null(), - 0, - name.as_ptr(), - name.len(), - ) - }; - let method_handle = scope.root_nanbox_f64(f64::from_bits(method.bits())); - let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); - if crate::proxy::js_proxy_is_proxy(method_handle.get_nanbox_f64()) == 1 { - return crate::proxy::call_proxy_value_with_this( - method_handle.get_nanbox_f64(), - this_handle.get_nanbox_f64(), - &args, - ); - } - crate::closure::native_call_value_this( - method_handle.get_nanbox_f64(), - crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), - args.as_ptr(), - args.len(), - ) -} - /// Invoke the native base method a subclass override displaced (#6316), with /// `this` bound to the receiver. Falls back to `undef` when the receiver carries /// no such method — an ordinary `super.m()` miss stays `undefined`. diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index 210fb7cfd5..ab7caa88da 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -405,6 +405,27 @@ pub unsafe extern "C" fn js_register_prototype_method( if class_has_instance_getter(class_id, &name) { return; } + // `C.prototype.__proto__ = v` is not a method install: it is a `[[Set]]` + // that reaches `Object.prototype`'s `__proto__` accessor, whose setter + // relinks the prototype exactly like `Object.setPrototypeOf`. + if name == "__proto__" { + let proto = super::class_decl_prototype_value(class_id); + if crate::value::JSValue::from_bits(proto.to_bits()).is_pointer() { + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_nanbox_f64(proto); + let value = scope.root_nanbox_f64(value); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); + crate::proxy::js_put_value_set( + proto.get_nanbox_f64(), + key, + value.get_nanbox_f64(), + proto.get_nanbox_f64(), + 1, + ); + return; + } + } class_prototype_method_root_store(class_id, name, value.to_bits()); // Ensure the receiver class can be `typeof`-detected. Method-less // classes that only get extended via `Class.prototype.m = fn` diff --git a/crates/perry-runtime/src/object/class_super_chain.rs b/crates/perry-runtime/src/object/class_super_chain.rs new file mode 100644 index 0000000000..07b932b8d6 --- /dev/null +++ b/crates/perry-runtime/src/object/class_super_chain.rs @@ -0,0 +1,269 @@ +//! The live `[[Prototype]]` chain a `super` reference reads. +//! +//! `super.name` is a property lookup on the home object's CURRENT +//! `[[Prototype]]` (the class prototype for an instance member, the class +//! constructor for a static one), with `this` as the receiver. These helpers +//! find that base and call or read through it when a relink, a patch or a +//! non-modeled base means the declared class tables no longer describe it. +//! +//! Split out of `class_constructors.rs` to keep that file under the 2,000-line +//! CI gate. + +use super::class_constructors::pinned_class_object_for_ancestor; + +/// The class object of the evaluation a `super` reference in class +/// `home_cid`'s methods belongs to, when one is pinned: repeated evaluations +/// share a template id, so the template tables cannot name it. +#[inline] +pub(crate) fn super_home_owner(home_cid: u32, this_value: f64) -> Option { + super::field_get_set::current_private_lexical_brand_value(home_cid).or_else(|| { + super::field_get_set::private_evaluation_brand_value(this_value) + .and_then(|owner| pinned_class_object_for_ancestor(owner, home_cid)) + }) +} + +/// The object `super` reads from in a method whose home object belongs to class +/// `home_cid`: that home object's current `[[Prototype]]`. The home object is +/// the class prototype for an instance method and the class constructor for a +/// static one. +/// +/// `None` when the declared-chain lookups must answer instead: the home is not +/// this template's class value (a per-evaluation class object), or the home +/// still links to its declared parent and that parent's declared chain +/// reaches a builtin, native or function-valued base, whose members this +/// runtime does not model as properties of a prototype object. +/// +/// # Safety +/// Reads class registry state; `home_owner` must be a live value or `None`. +pub(crate) unsafe fn class_super_base( + home_cid: u32, + parent_cid: u32, + home_owner: Option, + is_static: bool, +) -> Option { + if home_cid == 0 || !super::is_class_id_registered(home_cid) { + return None; + } + let class_value = super::class_value::class_value(home_cid); + if home_owner.is_some_and(|owner| owner.to_bits() != class_value.to_bits()) { + return None; + } + let home = if is_static { + class_value + } else { + super::class_registry::class_decl_prototype_value(home_cid) + }; + if !crate::value::JSValue::from_bits(home.to_bits()).is_pointer() { + return None; + } + // Materializing the declared parent's prototype can allocate. + let scope = crate::gc::RuntimeHandleScope::new(); + let base = scope.root_nanbox_f64(super::js_object_get_prototype_of(home)); + if parent_cid == 0 { + return None; + } + let declared = if is_static { + super::class_value::class_value(parent_cid) + } else { + super::class_registry::class_decl_prototype_value(parent_cid) + }; + let base = base.get_nanbox_f64(); + (base.to_bits() != declared.to_bits() || declared_chain_is_user_classes(parent_cid)) + .then_some(base) +} + +/// Was the `[[Prototype]]` of a class constructor on the declared chain from +/// `home_cid` up to (not including) `owner_cid` relinked, so that the declared +/// static lookup no longer describes it? One latch load answers `false` in a +/// process that never set a user prototype. +pub(super) fn static_chain_relinked(home_cid: u32, owner_cid: u32) -> bool { + if !super::prototype_chain::any_user_prototype_override() { + return false; + } + let mut cur = home_cid; + for _ in 0..64 { + if cur == owner_cid { + return false; + } + let Some(parent) = crate::object::get_parent_class_id(cur).filter(|p| *p != 0 && *p != cur) + else { + return false; + }; + // A constructor nobody has seen as a value cannot have been relinked. + if let Some(ctor) = super::class_value::class_value_if_minted(cur) { + let proto = + super::js_object_get_prototype_of(crate::value::js_nanbox_pointer(ctor as i64)); + if proto.to_bits() != super::class_value::class_value(parent).to_bits() { + return true; + } + } + cur = parent; + } + false +} + +/// The live super base for `super.key` in a method whose home belongs to class +/// `home_cid`, when the declared lookups may answer differently: only after a +/// relink, because they read the prototype objects and class function +/// objects, so patched, deleted and accessor members already apply. Asked +/// once a user prototype override exists; may allocate. +#[cold] +#[inline(never)] +pub(crate) unsafe fn super_get_live_base( + home_cid: u32, + parent_cid: u32, + receiver: f64, +) -> Option { + let is_static = super::class_ref_id(receiver).is_some() + || super::class_registry::is_class_object_value(receiver); + let relinked = if is_static { + static_chain_relinked(home_cid, 0) + } else { + declared_chain_has_relinked_prototype(home_cid) + }; + if !relinked { + return None; + } + let owner = super_home_owner(home_cid, receiver); + class_super_base(home_cid, parent_cid, owner, is_static) +} + +/// Does the declared chain from `cid` consist of compiled user classes only, +/// ending in a class that extends nothing? Then every member on it is a +/// property of a prototype object (or class constructor) the runtime reads. +pub(super) fn declared_chain_is_user_classes(cid: u32) -> bool { + let mut cur = cid; + for _ in 0..64 { + if cur == 0 || cur >= 0xFFFF_0000 || !super::is_class_id_registered(cur) { + return false; + } + match crate::object::get_parent_class_id(cur) { + Some(p) if p != 0 && p != cur => cur = p, + _ => { + // No parent edge: a recorded heritage value means a function, + // native or builtin base. + return crate::value::JSValue::from_bits( + super::class_registry::parent_static::template_dynamic_parent_value(cur) + .to_bits(), + ) + .is_undefined(); + } + } + } + false +} + +/// `super.name(...args)` with `base` as the super base: `base.[[Get]](name, +/// this)`, called with `this_value` as receiver. A null base or a +/// non-callable value throws the call's TypeError. +/// +/// # Safety +/// `args_ptr` must point to `args_len` valid `f64`s (or be null when +/// `args_len == 0`). +pub(super) unsafe fn super_call_on_live_base( + name: &str, + this_value: f64, + args_ptr: *const f64, + args_len: usize, + base: f64, +) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let base = scope.root_nanbox_f64(base); + super_call_on_relinked_chain(name, this_value, args_ptr, args_len, |key, receiver| { + let base = base.get_nanbox_f64(); + let jv = crate::value::JSValue::from_bits(base.to_bits()); + if jv.is_null() || jv.is_undefined() { + return None; + } + let key = f64::from_bits(crate::value::JSValue::string_ptr(key as *mut _).bits()); + Some(crate::value::JSValue::from_bits( + crate::proxy::js_reflect_get(base, key, receiver).to_bits(), + )) + }) +} + +/// Is the prototype of `cid` or of one of its declared ancestors relinked by a +/// user operation? Asked only after the declared-member lookups missed. +pub(super) fn declared_chain_has_relinked_prototype(cid: u32) -> bool { + let mut cur = cid; + for _ in 0..32 { + if cur == 0 { + return false; + } + if super::class_registry::class_decl_prototype_relinked(cur) { + return true; + } + match crate::object::get_parent_class_id(cur) { + Some(p) if p != cur => cur = p, + _ => return false, + } + } + false +} + +/// `super.name(...args)` resolved by `read` on a relinked chain: call the value +/// with `this_value` as receiver, or throw the TypeError a call of a +/// non-callable `super.name` throws. +/// +/// # Safety +/// `args_ptr` must point to `args_len` valid `f64`s (or be null when +/// `args_len == 0`). +pub(super) unsafe fn super_call_on_relinked_chain( + name: &str, + this_value: f64, + args_ptr: *const f64, + args_len: usize, + read: impl FnOnce(*const crate::StringHeader, f64) -> Option, +) -> f64 { + // The key allocation and the read (a getter on the new chain) can collect; + // the receiver and the arguments ride across them in handles. + let scope = crate::gc::RuntimeHandleScope::new(); + let this_handle = scope.root_nanbox_f64(this_value); + let args: Vec = if args_len > 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len).to_vec() + } else { + Vec::new() + }; + let arg_handles = scope.root_nanbox_f64_slice(&args); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let value = if key.is_null() { + None + } else { + read( + key as *const crate::StringHeader, + this_handle.get_nanbox_f64(), + ) + }; + let callable = value.filter(|v| { + let boxed = f64::from_bits(v.bits()); + v.is_pointer() + && ((crate::proxy::js_proxy_is_proxy(boxed) == 1 + && crate::proxy::proxy_wraps_callable(boxed)) + || crate::closure::is_closure_ptr( + crate::value::js_nanbox_get_pointer(boxed) as usize + )) + }); + let Some(method) = callable else { + crate::error::js_throw_type_error_not_a_function( + std::ptr::null(), + 0, + name.as_ptr(), + name.len(), + ) + }; + let method_handle = scope.root_nanbox_f64(f64::from_bits(method.bits())); + let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles); + if crate::proxy::js_proxy_is_proxy(method_handle.get_nanbox_f64()) == 1 { + return crate::proxy::call_proxy_value_with_this( + method_handle.get_nanbox_f64(), + this_handle.get_nanbox_f64(), + &args, + ); + } + crate::closure::native_call_value_this( + method_handle.get_nanbox_f64(), + crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()), + args.as_ptr(), + args.len(), + ) +} diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 5ca1850f7d..f3d7de9cde 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -683,6 +683,62 @@ pub(crate) fn class_chain_reaches(start: u32, want: u32) -> bool { } } +/// `value instanceof ` for an instance of class `start` when a +/// user `[[Prototype]]` change sits on its way: the instance's own prototype +/// was replaced, or a class prototype on the declared chain from `start` was +/// relinked before that chain reaches `want`. Then the declared class ids no +/// longer describe the chain, and `OrdinaryHasInstance` walks the live one. +/// `None` means the declared walk answers: no such change was ever made (one +/// latch load), or `want` is reached first. +/// `want` is a compiled class, or `Object` (its reserved id), whose +/// constructor is the global one. +#[inline(always)] +pub(crate) fn relinked_instance_chain_answer(value: f64, start: u32, want: u32) -> Option { + if !super::prototype_chain::any_user_prototype_override() { + return None; + } + relinked_instance_chain_answer_armed(value, start, want) +} + +/// [`relinked_instance_chain_answer`] once a user prototype override exists. +/// Out of line so the callers' common path stays one latch load. +#[cold] +#[inline(never)] +fn relinked_instance_chain_answer_armed(value: f64, start: u32, want: u32) -> Option { + const CLASS_ID_OBJECT: u32 = 0xFFFF0050; + if want == 0 || (want != CLASS_ID_OBJECT && !super::is_class_id_registered(want)) { + return None; + } + let live = || { + let constructor = if want == CLASS_ID_OBJECT { + js_get_global_this_builtin_value(b"Object".as_ptr(), 6) + } else { + super::class_constructor_ref_value(want) + }; + ordinary_has_instance_prototype_walk(value, constructor) + }; + if super::prototype_chain::object_has_user_prototype_override(value_addr(value)) { + return Some(live()); + } + if start == 0 { + return None; + } + let mut cur = start; + for _ in 0..64 { + if cur == want || crate::object::class_generic_origin(cur) == Some(want) { + return None; + } + if super::class_registry::class_decl_prototype_relinked(cur) { + return Some(live()); + } + match get_parent_class_id(cur) { + Some(pid) if pid != 0 && pid != cur => cur = pid, + _ => return None, + } + } + None +} + /// The parent-only half of [`class_chain_reaches`], used to continue a walk that /// has already stepped onto a generic origin. Separate so the two edges cannot /// recurse into each other without bound. @@ -788,6 +844,9 @@ fn subclass_of_builtin_reaches(value: f64, class_id: u32) -> bool { return false; } let cur = unsafe { (*obj).class_id }; + if let Some(answer) = relinked_instance_chain_answer(value, cur, class_id) { + return answer; + } // #10624: only pay for the value-aware walk once something has pinned // per-evaluation heritage. let reaches = diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index 33794ffc8d..ae5d7b664d 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -578,6 +578,22 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { if unsafe { crate::symbol::js_is_symbol(value) != 0 } { return false_val; } + // A relinked class prototype (or a replaced instance prototype) + // can end the chain before `Object.prototype`. + if let Some(header) = super::super::prototype_chain::any_user_prototype_override() + .then(|| unsafe { crate::value::addr_class::try_read_gc_header(value_addr(value)) }) + .flatten() + { + if header.obj_type == crate::gc::GC_TYPE_OBJECT { + let obj_class_id = + unsafe { (*(value_addr(value) as *const ObjectHeader)).class_id }; + if let Some(answer) = + relinked_instance_chain_answer(value, obj_class_id, CLASS_ID_OBJECT) + { + return if answer { true_val } else { false_val }; + } + } + } // Covers every heap object, including a Date (now a NaN-boxed // `DateCell` pointer — #2089) and an Invalid Date. return true_val; @@ -804,6 +820,9 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { // 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 let Some(answer) = relinked_instance_chain_answer(value, obj_class_id, class_id) { + return if answer { true_val } else { false_val }; + } if class_chain_reaches(obj_class_id, class_id) { return true_val; } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 5096353482..f4e91b43fb 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -89,6 +89,7 @@ mod class_gc_roots; mod class_handles; pub mod class_image; mod class_registry; +mod class_super_chain; pub(crate) mod class_value; #[cfg(test)] mod zeroed_cache_tests; diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 0c03bcc90e..dbf436125e 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -328,18 +328,24 @@ pub unsafe extern "C" fn js_object_super_get(home: f64, key_value: f64, _receive js_object_get_property_key(proto, key_value) } -/// `super.prop` GET for class methods: walk the parent class chain from -/// `parent_class_id` for an accessor (getter) named `key` and invoke it with -/// `receiver` as `this` (lookup starts at the super prototype, but the getter -/// runs with the current `this`). If no getter is found, read a data property -/// off the parent prototype object (`B.prototype.x = 42` then `super.x`). -/// Refs class/super/in-{constructor,getter,methods,setter}. +/// `super.prop` GET for class methods whose home object belongs to class +/// `home_class_id`: `home.[[GetPrototypeOf]]().[[Get]](key, receiver)`. +/// +/// Where the runtime models that chain end to end (see +/// `class_super_base`), it reads it: a patched, deleted or accessor parent +/// member and a relinked home all apply. Otherwise it walks the declared +/// parent class chain for an accessor (getter) named `key` and invokes it +/// with `receiver` as `this` (lookup starts at the super prototype, but the +/// getter runs with the current `this`); if no getter is found, it reads a +/// data property off the parent prototype object. Refs +/// class/super/in-{constructor,getter,methods,setter}. #[no_mangle] -pub unsafe extern "C" fn js_super_accessor_get( - parent_class_id: u32, - key: f64, - receiver: f64, -) -> f64 { +pub unsafe extern "C" fn js_super_accessor_get(home_class_id: u32, key: f64, receiver: f64) -> f64 { + let parent_class_id = if home_class_id == 0 { + 0 + } else { + crate::object::get_parent_class_id(home_class_id).unwrap_or(0) + }; // #6935: `js_string_coerce` on an object key runs a user `toString` / // `valueOf` (and allocates even for primitive keys), so it can GC and // evacuate. `receiver` is dereferenced far below (`class_ref_id`, the @@ -359,6 +365,29 @@ pub unsafe extern "C" fn js_super_accessor_get( .ok() .map(|s| s.to_string()) }; + let base = if super::prototype_chain::any_user_prototype_override() { + super::class_super_chain::super_get_live_base(home_class_id, parent_class_id, receiver) + } else { + None + }; + if let Some(base) = base { + let base_bits = crate::value::JSValue::from_bits(base.to_bits()); + if base_bits.is_null() || base_bits.is_undefined() { + let name = key_name.as_deref().unwrap_or("").as_bytes(); + crate::error::js_throw_type_error_property_access( + base_bits.is_null() as u32, + name.as_ptr(), + name.len(), + ); + } + return crate::proxy::js_reflect_get( + base, + key_handle.get_nanbox_f64(), + f64::from_bits(receiver_handle.get_heap_word_u64()), + ); + } + // `class_super_base` can allocate. + let receiver = f64::from_bits(receiver_handle.get_heap_word_u64()); // Static-context super (`super.x` inside a `static` method/getter): the // receiver is the class constructor (a ClassRef), so resolve against the // PARENT's static side — a static getter, then a static data field — diff --git a/crates/perry/tests/class_super_relink.rs b/crates/perry/tests/class_super_relink.rs new file mode 100644 index 0000000000..1b927edbf8 --- /dev/null +++ b/crates/perry/tests/class_super_relink.rs @@ -0,0 +1,114 @@ +//! `super.m` is a property lookup on the home object's CURRENT +//! `[[Prototype]]` (`C.prototype`'s in an instance method, `C`'s in a static +//! one) with `this` as the receiver, and `inst instanceof K` walks inst's live +//! chain for `K.prototype`. The program and node's output are the +//! `one_shape_class_super_relink` fixture. +//! +//! `super.m()` called the body the compiler resolved along the declared +//! `extends` chain, and its runtime fallback resolved through the declared +//! vtable, so a patched, deleted or accessor parent method never applied +//! (#11760). `typeof super.m` and `super.m` as a value were the declared +//! method's wrapper, `super.x` read the declared parent's prototype even +//! after a relink, and a static `super.s()` resolved an instance method of +//! the same name. `instanceof` walked declared class ids, and +//! `C.prototype.__proto__ = X` was compiled as a prototype-method install +//! named `__proto__` (#11765). + +use std::path::PathBuf; +use std::process::Command; + +const SOURCE: &str = include_str!("../../../tests/fixtures/one_shape_class_super_relink/main.ts"); +const EXPECTED: &str = + include_str!("../../../tests/fixtures/one_shape_class_super_relink/expected.txt"); + +const STATIC_SOURCE: &str = + include_str!("../../../tests/fixtures/one_shape_class_super_relink/static_super.ts"); +const STATIC_EXPECTED: &str = + include_str!("../../../tests/fixtures/one_shape_class_super_relink/static_super.expected.txt"); + +/// Compiles `source` into `dir` and returns the executable. +fn compile(dir: &std::path::Path, source: &str) -> PathBuf { + let entry = dir.join("main.ts"); + let output = dir.join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + let compile = Command::new(PathBuf::from(env!("CARGO_BIN_EXE_perry"))) + .current_dir(dir) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .env("PERRY_NO_CACHE", "1") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstderr:\n{}", + String::from_utf8_lossy(&compile.stderr) + ); + output +} + +#[test] +fn static_super_is_not_resolved_from_instance_methods() { + let dir = tempfile::tempdir().expect("tempdir"); + let output = compile(dir.path(), STATIC_SOURCE); + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + let stdout = String::from_utf8_lossy(&run.stdout); + assert!( + run.status.success(), + "binary failed ({:?})\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stderr) + ); + let wrong: Vec = STATIC_EXPECTED + .lines() + .zip(stdout.lines()) + .filter(|(want, got)| want != got) + .map(|(want, got)| format!("got `{got}`, node `{want}`")) + .collect(); + assert!( + wrong.is_empty() && STATIC_EXPECTED.lines().count() == stdout.lines().count(), + "{wrong:#?}\n{stdout}" + ); +} + +#[test] +fn super_and_instanceof_follow_the_live_prototype_chain() { + let dir = tempfile::tempdir().expect("tempdir"); + let output = compile(dir.path(), SOURCE); + // Runtime trip counts: the primed cases change the chain mid-loop, and a + // fixed small loop would be unrolled. The output does not depend on n. + for n in ["40", "41"] { + let run = Command::new(&output) + .arg(n) + .current_dir(dir.path()) + .env("PERRY_GC_FORCE_EVACUATE", "1") + .env("PERRY_GC_POISON_FROMSPACE", "1") + .output() + .expect("run compiled binary"); + let stdout = String::from_utf8_lossy(&run.stdout); + assert!( + run.status.success(), + "n={n}: binary failed ({:?})\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stderr) + ); + let mut wrong: Vec = EXPECTED + .lines() + .zip(stdout.lines()) + .filter(|(want, got)| want != got) + .map(|(want, got)| format!("got `{got}`, node `{want}`")) + .collect(); + if EXPECTED.lines().count() != stdout.lines().count() { + wrong.push(format!( + "{} lines, node {}", + stdout.lines().count(), + EXPECTED.lines().count() + )); + } + assert!(wrong.is_empty(), "n={n}: {wrong:#?}\n{stdout}"); + } +} diff --git a/tests/fixtures/one_shape_class_super_relink/check.sh b/tests/fixtures/one_shape_class_super_relink/check.sh new file mode 100755 index 0000000000..7d11a3896f --- /dev/null +++ b/tests/fixtures/one_shape_class_super_relink/check.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# `super.m` / `super.x` read the home object's current [[Prototype]], +# `instanceof` walks the live chain, and `C.prototype.__proto__ = X` relinks +# like Object.setPrototypeOf, after patches, deletes, accessors and relinks +# (expected.txt is node 26.5.1's output). Two trip counts, under forced +# evacuation. +set -euo pipefail +binary=$(realpath "${1:?pass the compiled fixture executable}") +fixture_dir=$(cd "$(dirname "$0")" && pwd) +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT +for n in 40 41; do + PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 "$binary" "$n" > "$tmp_dir/out" 2> "$tmp_dir/err" + cmp "$fixture_dir/expected.txt" "$tmp_dir/out" +done +echo ok diff --git a/tests/fixtures/one_shape_class_super_relink/expected.txt b/tests/fixtures/one_shape_class_super_relink/expected.txt new file mode 100644 index 0000000000..878fe1e438 --- /dev/null +++ b/tests/fixtures/one_shape_class_super_relink/expected.txt @@ -0,0 +1,60 @@ +call-base ok:PB.m +call-after-patch ok:patched:true +own-m-unchanged PC.m +call-primed-patch ok:QB.m / ok:QB.patched +call-via-getter ok:getter:true +typeof-via-getter function +call-before-delete ok:DB.m +call-after-delete TypeError +typeof-after-delete undefined +call-primed-relink ok:RB.m / ok:RX.m +call-missing TypeError +call-private-tripped ok:VB.m7 +typeof-base function +value-is-proto-m true +typeof-primed-patch ok:function / ok:number +value-after-patch 5 +data-primed ok:undefined / ok:42 +getter-receiver XB.g:c +getter-redefined XB.g2:c +getter-after-relink XY.g:c +data-after-relink undefined +get-on-null-home TypeError +static-base ok:SB.s:SC +static-after-patch ok:SB.s-patched:SC +static-data SB.k +static-after-relink ok:SD.s +static-typeof-after-relink function +static-data-after-relink SD.k +static-inst-only-call TypeError +static-inst-only-typeof undefined +static-inst-only-value undefined +static-prefers-static ok:StB.static-s +inst-before true,false +inst-old-parent false +inst-new-chain true,true,true,true +inst-subclass true,true,false,true +inst-dynamic-rhs false +inst-fresh-instance false,true +inst-hasInstance true,false +inst-null false,true,false +inst-plain false,true +inst-relinked-back true +inst-primed ok:true,false / ok:false,true +inst-own-proto false,false,true +inst-via-plain-mid false,true,true,true +inst-to-null-proto-object false,true,false +inst-mid-relink false,true,true,true +inst-mid-relink-dynamic false,true +inst-own-proto-plain false,true,true +inst-own-proto-null false,false,false +inst-primed-object ok:true,false,true / ok:false,true,true +dunder-direct true,false,UE,UE,false,true +dunder-alias true,UE +dunder-computed true +dunder-null ,undefined,false +dunder-ignored true,UB +dunder-primed ok:UE:false / ok:UB:true +plain-super 8 +native-base got:5 +map-base 101 diff --git a/tests/fixtures/one_shape_class_super_relink/main.ts b/tests/fixtures/one_shape_class_super_relink/main.ts new file mode 100644 index 0000000000..e9393dae64 --- /dev/null +++ b/tests/fixtures/one_shape_class_super_relink/main.ts @@ -0,0 +1,309 @@ +// `super.name` is a property lookup on the home object's CURRENT +// [[Prototype]] (`C.prototype`'s for an instance method, `C`'s for a static +// one) with `this` as the receiver. A patched, deleted or accessor parent +// member, a relinked `C.prototype` and a relinked `C` all apply, for calls, +// `typeof super.m`, `super.m` as a value and `super.x` reads. +// `inst instanceof K` is OrdinaryHasInstance (or K[Symbol.hasInstance]): it +// walks inst's LIVE chain for K.prototype. And `C.prototype.__proto__ = X` +// is the Object.prototype accessor's setter: a relink exactly like +// `Object.setPrototypeOf(C.prototype, X)`. +// +// Class names are unique per block. Each line is `name value`, compared with +// node 26.5.1. "Primed" cases change the chain mid-loop at a parameter +// receiver with an argv trip count, so the site ran before the change. +import { EventEmitter } from "events"; +const N = Number(process.argv[2] ?? "40"); +const H = N >> 1; +function show(name: string, v: any): void { + console.log(name + " " + String(v)); +} +function tryIt(f: () => any): string { + try { + return "ok:" + String(f()); + } catch (e) { + return (e instanceof TypeError) ? "TypeError" : "other:" + String(e); + } +} +// Runs `f(o)` n times, applying `change` before iteration `at`; returns the +// first and last results. +function primed(o: any, n: number, at: number, change: () => void, f: (o: any) => any): string { + let before = ""; + let after = ""; + for (let i = 0; i < n; i++) { + if (i === at) change(); + const v = tryIt(() => f(o)); + if (i < at) before = v; else after = v; + } + return before + " / " + after; +} + +// ---- super.m() calls ------------------------------------------------------- +{ + class PB { m() { return "PB.m"; } } + class PC extends PB { m() { return "PC.m"; } callM() { return super.m(); } } + const c = new PC(); + show("call-base", tryIt(() => c.callM())); + (PB.prototype as any).m = function (this: any) { return "patched:" + (this === c); }; + show("call-after-patch", tryIt(() => c.callM())); + show("own-m-unchanged", c.m()); +} +{ + class QB { m() { return "QB.m"; } } + class QC extends QB { callM() { return super.m(); } } + show("call-primed-patch", primed(new QC(), N, H, + () => { (QB.prototype as any).m = () => "QB.patched"; }, (o) => o.callM())); +} +{ + class GB { m() { return "GB.m"; } } + class GC extends GB { callM() { return super.m(); } typeM() { return typeof super.m; } } + const g = new GC(); + Object.defineProperty(GB.prototype, "m", { + get(this: any) { const self = this; return () => "getter:" + (self === g); }, + configurable: true, + }); + show("call-via-getter", tryIt(() => g.callM())); + show("typeof-via-getter", g.typeM()); +} +{ + class DB { m() { return "DB.m"; } } + class DC extends DB { callM() { return super.m(); } typeM() { return typeof super.m; } } + const d = new DC(); + show("call-before-delete", tryIt(() => d.callM())); + delete (DB.prototype as any).m; + show("call-after-delete", tryIt(() => d.callM())); + show("typeof-after-delete", d.typeM()); +} +{ + class RB { m() { return "RB.m"; } } + class RX { m() { return "RX.m"; } } + class RC extends RB { callM() { return super.m(); } } + show("call-primed-relink", primed(new RC(), N, H, + () => { Object.setPrototypeOf(RC.prototype, RX.prototype); }, (o) => o.callM())); +} +{ + class MB { m() { return "MB.m"; } } + class MC extends MB { nope() { return super.zz(); } } + show("call-missing", tryIt(() => new MC().nope())); +} +{ + // A patch of the same name on an unrelated class trips the per-name guard; + // the parent's private state must still work through the runtime path. + class VB { #v = 7; m() { return "VB.m" + this.#v; } } + class VC extends VB { m() { return "VC"; } callM() { return super.m(); } } + class Unrelated { m() { return 0; } } + (Unrelated.prototype as any).m = () => 1; + show("call-private-tripped", tryIt(() => new VC().callM())); +} + +// ---- super property gets --------------------------------------------------- +{ + class TB { m() { return 1; } } + class TC extends TB { typeM() { return typeof super.m; } valueM() { return super.m; } } + const t = new TC(); + show("typeof-base", t.typeM()); + show("value-is-proto-m", t.valueM() === TB.prototype.m); + show("typeof-primed-patch", primed(t, N, H, + () => { (TB.prototype as any).m = 5; }, (o) => o.typeM())); + show("value-after-patch", String(t.valueM())); +} +{ + class XB { get g() { return "XB.g:" + (this as any).tag; } } + class XC extends XB { tag = "c"; readX() { return super.x; } readG() { return super.g; } } + const x = new XC(); + show("data-primed", primed(x, N, H, () => { (XB.prototype as any).x = 42; }, (o) => o.readX())); + show("getter-receiver", x.readG()); + Object.defineProperty(XB.prototype, "g", { get(this: any) { return "XB.g2:" + this.tag; }, configurable: true }); + show("getter-redefined", x.readG()); + class XY { get g() { return "XY.g:" + (this as any).tag; } } + Object.setPrototypeOf(XC.prototype, XY.prototype); + show("getter-after-relink", x.readG()); + show("data-after-relink", String(x.readX())); + Object.setPrototypeOf(XC.prototype, null); + show("get-on-null-home", tryIt(() => x.readG())); +} + +// ---- static super ---------------------------------------------------------- +{ + class SB { static s() { return "SB.s:" + (this as any).name; } s() { return "SB.inst-s"; } } + class SC extends SB { + static callS() { return super.s(); } + static typeS() { return typeof super.s; } + static readK() { return super.k; } + } + show("static-base", tryIt(() => SC.callS())); + (SB as any).s = function (this: any) { return "SB.s-patched:" + this.name; }; + show("static-after-patch", tryIt(() => SC.callS())); + (SB as any).k = "SB.k"; + show("static-data", SC.readK()); + class SD { static s() { return "SD.s"; } static k = "SD.k"; } + Object.setPrototypeOf(SC, SD); + show("static-after-relink", tryIt(() => SC.callS())); + show("static-typeof-after-relink", SC.typeS()); + show("static-data-after-relink", SC.readK()); +} + +{ + // The parent has an INSTANCE method of the name and no static one: in a + // static member, super.m reads the parent constructor, so it is absent. + class StB { onlyInst() { return "StB.inst-m"; } static both() { return "StB.static-s"; } both() { return "StB.inst-s"; } } + class StC extends StB { + static callOnly() { return super.onlyInst(); } + static callBoth() { return super.both(); } + static typeOnly() { return typeof super.onlyInst; } + static valueOnly() { return super.onlyInst; } + } + show("static-inst-only-call", tryIt(() => StC.callOnly())); + show("static-inst-only-typeof", StC.typeOnly()); + show("static-inst-only-value", StC.valueOnly()); + show("static-prefers-static", tryIt(() => StC.callBoth())); +} + +// ---- instanceof ------------------------------------------------------------ +{ + class IB { m() { return "IB"; } } + class ID { m() { return "ID"; } } + class IE extends ID {} + class IC extends IB {} + class ISub extends IC {} + const c = new IC(); + const s = new ISub(); + show("inst-before", [c instanceof IB, c instanceof ID].join(",")); + Object.setPrototypeOf(IC.prototype, IE.prototype); + show("inst-old-parent", c instanceof IB); + show("inst-new-chain", [c instanceof IE, c instanceof ID, c instanceof IC, c instanceof Object].join(",")); + show("inst-subclass", [s instanceof ISub, s instanceof IC, s instanceof IB, s instanceof ID].join(",")); + const K: any = Math.random() < 2 ? IB : ID; + show("inst-dynamic-rhs", c instanceof K); + show("inst-fresh-instance", [new IC() instanceof IB, new IC() instanceof IE].join(",")); +} +{ + class HB {} + class HC extends HB {} + const h = new HC(); + class HK { static [Symbol.hasInstance](v: any) { return v === h; } } + Object.setPrototypeOf(HC.prototype, HK.prototype); + show("inst-hasInstance", [h instanceof HK, new HC() instanceof HK].join(",")); +} +{ + class NB {} + class NC extends NB {} + const n = new NC(); + Object.setPrototypeOf(NC.prototype, null); + show("inst-null", [n instanceof NB, n instanceof NC, n instanceof Object].join(",")); + const plain = { tag: 1 }; + class OC extends NB {} + const o = new OC(); + Object.setPrototypeOf(OC.prototype, plain); + show("inst-plain", [o instanceof NB, o instanceof Object].join(",")); + Object.setPrototypeOf(OC.prototype, NB.prototype); + show("inst-relinked-back", o instanceof NB); +} +{ + class LB {} + class LD {} + class LC extends LB {} + const l = new LC(); + show("inst-primed", primed(l, N, H, () => { Object.setPrototypeOf(LC.prototype, LD.prototype); }, + (o) => [o instanceof LB, o instanceof LD].join(","))); +} +{ + class WB {} + class WD {} + class WC extends WB {} + const w = new WC(); + Object.setPrototypeOf(w, WD.prototype); + show("inst-own-proto", [w instanceof WC, w instanceof WB, w instanceof WD].join(",")); +} + +{ + class MA {} + class MB extends MA {} + class MD {} + const mid = Object.create(MD.prototype); + const m = new MB(); + Object.setPrototypeOf(MB.prototype, mid); + show("inst-via-plain-mid", [m instanceof MA, m instanceof MD, m instanceof MB, m instanceof Object].join(",")); +} +{ + class QA {} + class QB extends QA {} + const q = new QB(); + Object.setPrototypeOf(QB.prototype, Object.create(null)); + show("inst-to-null-proto-object", [q instanceof QA, q instanceof QB, q instanceof Object].join(",")); +} +{ + class RA {} + class RB extends RA {} + class RC extends RB {} + class RX {} + const r = new RC(); + Object.setPrototypeOf(RB.prototype, RX.prototype); + show("inst-mid-relink", [r instanceof RA, r instanceof RB, r instanceof RC, r instanceof RX].join(",")); + const dyn: any = Math.random() < 2 ? RA : RX; + const dyn2: any = Math.random() < 2 ? RX : RA; + show("inst-mid-relink-dynamic", [r instanceof dyn, r instanceof dyn2].join(",")); +} +{ + class YA {} + class YB extends YA {} + const y = new YB(); + Object.setPrototypeOf(y, Object.create(YA.prototype)); + show("inst-own-proto-plain", [y instanceof YB, y instanceof YA, y instanceof Object].join(",")); + const y2 = new YB(); + Object.setPrototypeOf(y2, null); + show("inst-own-proto-null", [y2 instanceof YB, y2 instanceof YA, y2 instanceof Object].join(",")); +} +{ + class ZA {} + class ZB extends ZA {} + class ZX {} + const z = new ZB(); + show("inst-primed-object", primed(z, N, H, () => { Object.setPrototypeOf(ZB.prototype, ZX.prototype); }, + (o) => [o instanceof ZA, o instanceof ZX, o instanceof Object].join(","))); +} + +// ---- the __proto__ setter on a class prototype ------------------------------ +{ + class UB { m() { return "UB"; } } + class UE { m() { return "UE"; } } + class U1 extends UB { callM() { return super.m(); } } + class U2 extends UB {} + class U3 extends UB {} + class U4 extends UB {} + class U5 extends UB {} + const u1 = new U1(); + (U1.prototype as any).__proto__ = UE.prototype; + show("dunder-direct", [Object.getPrototypeOf(U1.prototype) === UE.prototype, + Object.prototype.hasOwnProperty.call(U1.prototype, "__proto__"), u1.m(), u1.callM(), + u1 instanceof UB, u1 instanceof UE].join(",")); + const p2: any = U2.prototype; + p2.__proto__ = UE.prototype; + show("dunder-alias", [Object.getPrototypeOf(U2.prototype) === UE.prototype, (new U2() as any).m()].join(",")); + (U3.prototype as any)["__proto__"] = UE.prototype; + show("dunder-computed", Object.getPrototypeOf(U3.prototype) === UE.prototype); + const u4 = new U4(); + (U4.prototype as any).__proto__ = null; + show("dunder-null", [Object.getPrototypeOf(U4.prototype), typeof (u4 as any).m, u4 instanceof UB].join(",")); + (U5.prototype as any).__proto__ = 5; + show("dunder-ignored", [Object.getPrototypeOf(U5.prototype) === UB.prototype, (new U5() as any).m()].join(",")); + show("dunder-primed", primed(new U2(), N, H, () => { (U2.prototype as any).__proto__ = UB.prototype; }, + (o) => o.m() + ":" + (o instanceof UB))); +} + +// ---- untouched and native bases still resolve ------------------------------- +{ + class AB { m(x: number) { return x + 1; } } + class AC extends AB { m(x: number) { return super.m(x) * 2; } } + show("plain-super", new AC().m(3)); + class Bus extends EventEmitter { emit(ev: string, ...a: any[]) { return super.emit(ev, ...a); } } + class Logged extends Bus { emit(ev: string, x: any) { return super.emit(ev, x); } } + const l = new Logged(); + let got = ""; + l.on("x", (v: any) => { got = "got:" + v; }); + l.emit("x", 5); + show("native-base", got); + class MM extends Map { get(k: string) { return (super.get(k) ?? 0) + 100; } } + const mm = new MM(); + mm.set("a", 1); + show("map-base", mm.get("a")); +} diff --git a/tests/fixtures/one_shape_class_super_relink/static_super.expected.txt b/tests/fixtures/one_shape_class_super_relink/static_super.expected.txt new file mode 100644 index 0000000000..95ed2672cc --- /dev/null +++ b/tests/fixtures/one_shape_class_super_relink/static_super.expected.txt @@ -0,0 +1,9 @@ +call-inst-only TypeError +call-both ok:StB.static-both:StC +call-only-static ok:StB.static-only:StC +typeof-inst-only undefined +typeof-both function +value-inst-only undefined +value-both true +instance-side StB.inst-both +instance-typeof-static-only undefined diff --git a/tests/fixtures/one_shape_class_super_relink/static_super.ts b/tests/fixtures/one_shape_class_super_relink/static_super.ts new file mode 100644 index 0000000000..86f586f87b --- /dev/null +++ b/tests/fixtures/one_shape_class_super_relink/static_super.ts @@ -0,0 +1,42 @@ +// A program with no prototype surgery: `super.name` in a static member names +// the parent CONSTRUCTOR's member, which the compiler must not resolve from +// the parent's INSTANCE method tables, for calls, `typeof super.m` and +// `super.m` as a value. (A program that relinks or patches a prototype sends +// these through the runtime lookup, so one_shape_class_super_relink cannot +// see the compile-time route.) Expected output is node 26.5.1's. +function show(name: string, v: any): void { + console.log(name + " " + String(v)); +} +function tryIt(f: () => any): string { + try { + return "ok:" + String(f()); + } catch (e) { + return (e instanceof TypeError) ? "TypeError" : "other:" + String(e); + } +} +class StB { + onlyInst() { return "StB.inst-only"; } + static both() { return "StB.static-both:" + (this as any).name; } + both() { return "StB.inst-both"; } + static onlyStatic() { return "StB.static-only:" + (this as any).name; } +} +class StC extends StB { + static callOnlyInst() { return super.onlyInst(); } + static callBoth() { return super.both(); } + static callOnlyStatic() { return super.onlyStatic(); } + static typeOnlyInst() { return typeof super.onlyInst; } + static typeBoth() { return typeof super.both; } + static valueOnlyInst() { return super.onlyInst; } + static valueBoth() { return super.both === StB.both; } + callBothInst() { return super.both(); } + typeOnlyStatic() { return typeof super.onlyStatic; } +} +show("call-inst-only", tryIt(() => StC.callOnlyInst())); +show("call-both", tryIt(() => StC.callBoth())); +show("call-only-static", tryIt(() => StC.callOnlyStatic())); +show("typeof-inst-only", StC.typeOnlyInst()); +show("typeof-both", StC.typeBoth()); +show("value-inst-only", StC.valueOnlyInst()); +show("value-both", StC.valueBoth()); +show("instance-side", new StC().callBothInst()); +show("instance-typeof-static-only", new StC().typeOnlyStatic());