diff --git a/changelog.d/11764-class-relink-method-visibility.md b/changelog.d/11764-class-relink-method-visibility.md new file mode 100644 index 0000000000..4b03ce80e8 --- /dev/null +++ b/changelog.d/11764-class-relink-method-visibility.md @@ -0,0 +1,11 @@ +After `Object.setPrototypeOf(C.prototype, X)`, methods, getters and setters +declared in C's old parent class are no longer visible on C's instances. +Property reads, `in`, calls and `super.m()` follow the live prototype chain, so +the new chain's members resolve. A wide dispatch tower also stops running a +parent method's old body after `Object.defineProperty` replaces it on the +parent prototype. + +Callable proxies returned by a getter on the replacement super chain use the rooted proxy-call helper. Object-target proxies remain non-callable, and nested/revoked proxy calls and getter exceptions retain their normal behavior. + +Include the immutable thread-global IR suite in scoped test selection after +integrating current main, so the complete suite map accepts this branch. diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 2cc5462910..7bc2887216 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -106,11 +106,67 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .ok_or_else(|| anyhow!("super.{}() outside any method body", method))?; let this_box = ctx.block().load(DOUBLE, &this_slot); let mut lowered: Vec = Vec::with_capacity(args.len() + 1); - lowered.push(this_box); + lowered.push(this_box.clone()); let mut user_vals: Vec = Vec::with_capacity(args.len()); for a in args { user_vals.push(lower_expr(ctx, a)?); } + // The body above was resolved along the declared `extends` chain. + // `super` is the home object's CURRENT `[[Prototype]]`, so a + // relinked class prototype (or other prototype surgery on this + // name) sends the call to the runtime, which reads that chain. + let home_cid = ctx.class_ids.get(¤t_class_name).copied().unwrap_or(0); + let guarded_merge = if home_cid != 0 { + let key_idx = ctx.strings.intern(method); + let slot = (ctx.strings.entry(key_idx).dispatch_hash & 0xffff).to_string(); + let direct_idx = ctx.new_block("super_m.direct"); + let dynamic_idx = ctx.new_block("super_m.dynamic"); + let merge_idx = ctx.new_block("super_m.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 (args_ptr, args_len) = if user_vals.is_empty() { + ("null".to_string(), "0".to_string()) + } else { + let n = user_vals.len(); + let buf = ctx.func.alloca_entry_array(DOUBLE, n); + for (i, v) in user_vals.iter().enumerate() { + let slot = ctx.block().gep(DOUBLE, &buf, &[(I64, &i.to_string())]); + ctx.block().store(DOUBLE, v, &slot); + } + let ptr_reg = ctx.block().next_reg(); + ctx.block().emit_raw(format!( + "{} = getelementptr [{} x double], ptr {}, i64 0, i64 0", + ptr_reg, n, buf + )); + (ptr_reg, n.to_string()) + }; + let name_global = emit_string_literal_global(ctx, method); + let dynamic_value = ctx.block().call( + DOUBLE, + "js_super_method_call_dynamic", + &[ + (I32, &home_cid.to_string()), + (PTR, &name_global), + (I64, &method.len().to_string()), + (DOUBLE, &this_box), + (PTR, &args_ptr), + (I64, &args_len), + ], + ); + 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 + }; // #8040: this arm passed every argument POSITIONALLY, which is wrong // whenever the resolved parent method ends in an array-shaped slot. // A body reading `arguments` gets one synthesized trailing parameter @@ -185,7 +241,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } let arg_slices: Vec<(crate::types::LlvmType, &str)> = lowered.iter().map(|s| (DOUBLE, s.as_str())).collect(); - Ok(ctx.block().call(DOUBLE, &fn_name, &arg_slices)) + let direct_value = ctx.block().call(DOUBLE, &fn_name, &arg_slices); + let Some((merge_idx, merge_label, dynamic_value, dynamic_end)) = guarded_merge 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)], + )) } // -------- super.method(...spread) -------- diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index ab5d665343..69ac6d7eae 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -227,6 +227,33 @@ fn method_inline_probe_enabled() -> bool { }) } +/// `i1`: no prototype surgery has retired direct-method arms for the name +/// whose guard slot is `method_guard_slot` (the low 16 bits of its dispatch +/// hash) — neither the all-names byte nor that name's byte is set. +/// +/// A compiler-resolved method body for an INHERITED name (the dispatch tower, +/// `super.m()`) assumes the declared `extends` chain is the instance chain. +/// Assigning, deleting or redefining a prototype member, or relinking a class +/// prototype (`Object.setPrototypeOf(C.prototype, X)`), sets these bytes +/// (`perry-runtime` `class_registry/prototype_methods.rs`); a set byte sends +/// the site to its runtime dispatch. +pub(crate) fn emit_prototype_method_guard_ok( + blk: &mut crate::block::LlBlock, + method_guard_slot: &str, +) -> String { + let invalidated = + blk.load_atomic_acquire(I8, "@PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED", 1); + let all_methods_ok = blk.icmp_eq(I8, &invalidated, "0"); + let method_slot_ptr = blk.gep( + I8, + "@PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED_BY_METHOD", + &[(I64, method_guard_slot)], + ); + let method_invalidated = blk.load_atomic_acquire(I8, &method_slot_ptr, 1); + let method_ok = blk.icmp_eq(I8, &method_invalidated, "0"); + blk.and(I1, &all_methods_ok, &method_ok) +} + pub(crate) fn emit_inline_direct_method_shape_guard( ctx: &mut FnCtx<'_>, recv_box: &str, diff --git a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs index 5c47220f40..e8f83fd666 100644 --- a/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs +++ b/crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs @@ -585,8 +585,22 @@ pub(crate) fn try_lower_instance_method_call( // fallback instead of re-entering this hard-coded tower. probed_cid } else { - ctx.block() - .call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]) + // A tower too wide for the shape probe still hard-codes the + // body each class id inherits along the declared `extends` + // chain. Prototype surgery on that name (an assignment, + // delete or redefinition, or a relinked class prototype) + // retires the arms: class id 0 matches no case and takes the + // runtime default, as the shape probe's miss does. + let raw_cid = + ctx.block() + .call(I32, "js_object_get_class_id", &[(I64, &recv_handle)]); + let blk = ctx.block(); + let prototype_ok = + crate::lower_call::method_override::emit_prototype_method_guard_ok( + blk, + &method_guard_slot_str, + ); + blk.select(I1, &prototype_ok, I32, &raw_cid, "0") }; for (i, (case_cid, _)) in implementors.iter().enumerate() { diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 75ec63c074..1c1e0e2018 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -949,6 +949,22 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( return result; } } + // `super` is the home object's CURRENT `[[Prototype]]`. Once a user + // relinked the home class's prototype (`Object.setPrototypeOf(C.prototype, + // X)`), that is the recorded link: the declared parent's members are off + // the chain, and a name the link does not carry is not callable. + if super::class_registry::class_decl_prototype_relinked(child_class_id) { + return super_call_on_relinked_chain( + name, + this_value, + args_ptr, + args_len, + |key, receiver| { + super::class_registry::relinked_class_prototype_read(child_class_id, key, receiver) + .flatten() + }, + ); + } // `lookup_class_method_in_chain` resolves under the registry read lock and // DROPS it before returning — the invoked method body may take the registry // write lock (a lazy `require()` registering a module class), so we must not @@ -984,6 +1000,22 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( args_len, ); } + // An ANCESTOR's prototype was relinked: the declared-member walks above + // stop there, and the rest of the chain is its recorded link, which the + // generic class-chain read follows. + if declared_chain_has_relinked_prototype(parent_cid) { + return super_call_on_relinked_chain( + name, + this_value, + args_ptr, + args_len, + |key, receiver| { + super::class_registry::resolve_proto_chain_field_with_receiver( + parent_cid, key, receiver, + ) + }, + ); + } // #6316: the parent chain is real (an intermediate user class) but bottoms // out in a NATIVE base whose surface perry stamps onto the instance — // `class Logged extends Bus`, `class Bus extends EventEmitter`. Neither the @@ -992,6 +1024,92 @@ 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.rs b/crates/perry-runtime/src/object/class_registry.rs index 125e476a4d..4824891ea0 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -112,7 +112,7 @@ pub use state::{ pub(crate) use prototype_objects::{ class_decl_prototype_relinked, class_prototype_object, ensure_function_prototype_object, function_class_id, function_value_for_class_id, instance_class_prototype_object, - object_proto_chain_symbol_slot, resolve_proto_chain_field, + object_proto_chain_symbol_slot, relinked_class_prototype_read, resolve_proto_chain_field, resolve_proto_chain_field_noting_miss, resolve_proto_chain_field_with_receiver, resolve_proto_chain_symbol, synthetic_class_prototype_object, SYNTHETIC_CLASS_ID_BASE, }; @@ -152,8 +152,9 @@ pub(crate) use prototype_methods::{ pub(crate) use prototype_methods::{ class_prototype_fast_guard_invalidated_for_method, class_prototype_method_guard_slot, class_prototype_method_root_remove, class_prototype_method_root_store, - invalidate_class_prototype_fast_guards, invalidate_class_prototype_fast_guards_for_method, - mirror_prototype_method_on_object, synthetic_class_id_for_function, + class_prototype_relinked, invalidate_class_prototype_fast_guards, + invalidate_class_prototype_fast_guards_for_method, mirror_prototype_method_on_object, + synthetic_class_id_for_function, }; pub use prototype_methods::{ js_class_register_static_field, js_get_function_prototype_method, @@ -230,9 +231,9 @@ pub(crate) use parent_static::{ 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, dynamic_value_class_id, - get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, - lookup_static_method_owner, register_class, register_class_dynamic_static_accessor, - static_accessor_in_chain, + get_parent_class_id, instance_chain_parent_class_id, lookup_class_symbol_method_in_chain, + lookup_static_method_in_chain, lookup_static_method_owner, register_class, + register_class_dynamic_static_accessor, static_accessor_in_chain, }; 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/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 0503f9644c..49964f336e 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -1922,14 +1922,34 @@ pub fn lookup_class_method_in_chain(class_id: u32, name: &str) -> Option<(usize, return Some(entry); } } - match get_parent_class_id(cur) { - Some(pid) if pid != 0 => cur = pid, - _ => return None, + match instance_chain_parent_class_id(cur) { + Some(pid) => cur = pid, + None => return None, } } None } +/// The next class on an INSTANCE chain after `cid`: the declared parent, +/// unless a user operation (`Object.setPrototypeOf(C.prototype, X)`, +/// `C.prototype.__proto__ = X`) replaced the `[[Prototype]]` of `cid`'s +/// prototype object. That prototype's recorded link is then the chain, and a +/// walk over declared class members must stop at `cid`: the parent's methods, +/// getters and setters are off the chain (the generic read continues on the +/// recorded link). The static side (`C.__proto__`) is a different object and +/// keeps the declared parent. +/// +/// The relink check runs only when a declared parent exists, so a walk that +/// answers from the receiver's own class, or reaches a root class, pays +/// nothing for it. +#[inline] +pub(crate) fn instance_chain_parent_class_id(cid: u32) -> Option { + match get_parent_class_id(cid) { + Some(pid) if pid != 0 && !super::class_decl_prototype_relinked(cid) => Some(pid), + _ => None, + } +} + /// True when `ptr` is the prototype OBJECT of some registered class. Class /// methods are installed as own fields on the prototype object, so a method-as- /// value read whose receiver *is* the prototype must return the shared canonical @@ -1959,9 +1979,9 @@ pub fn method_owner_class_id(class_id: u32, name: &str) -> Option { return Some(cur); } } - match get_parent_class_id(cur) { - Some(pid) if pid != 0 => cur = pid, - _ => return None, + match instance_chain_parent_class_id(cur) { + Some(pid) => cur = pid, + None => return None, } } None 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 e808ddf10a..210fb7cfd5 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -200,6 +200,66 @@ pub(crate) fn invalidate_class_prototype_fast_guards_for_method(name: &str) { retire_prototype_dependent_caches(); } +/// A user operation (`Object.setPrototypeOf(C.prototype, X)`, +/// `C.prototype.__proto__ = X`) replaced the `[[Prototype]]` of `proto`. +/// +/// When `proto` is a declared class's prototype object, every member that +/// class's instances inherited from its DECLARED ancestors is off their chain +/// from now on, and whatever `X` carries is on it. The runtime walks over +/// declared members stop at the relinked class +/// (`instance_chain_parent_class_id`); what remains are the resolutions made +/// before the relink or ahead of time: +/// +/// * compiler-emitted direct-method arms (the dispatch tower, `super.m()`) +/// resolved an inherited name to an ancestor's body along the declared +/// `extends` chain. They are guarded by the per-name invalidation bytes, so +/// the relink retires the slot of every method, getter and setter name an +/// ancestor declares — exactly the names whose resolution it can change. A +/// name the class itself declares still resolves to its own body, and a +/// name no declared class carries never had a direct arm; +/// * the `(class_id, method name)` dispatch caches (`VTABLE_IC`, +/// `OBJ_DISPATCH_IC`) are keyed on `VTABLE_GEN`, which the retirement bumps. +/// +/// The receiver-word site memos need nothing: the relink restamps `proto`'s +/// shape, which their hop facts compare. +/// +/// # Safety +/// `proto` must point to a live, meta-capable object. +pub(crate) unsafe fn class_prototype_relinked(proto: *mut crate::object::ObjectHeader) { + let cid = (*proto).class_id; + if cid == 0 || super::class_decl_prototype_object(cid) != proto { + return; + } + let mut names: Vec = Vec::new(); + if let Ok(registry) = super::CLASS_VTABLE_REGISTRY.read() { + if let Some(reg) = registry.as_ref() { + let mut cur = cid; + for _ in 0..32 { + match super::get_parent_class_id(cur) { + Some(pid) if pid != 0 && pid != cur => cur = pid, + _ => break, + } + if let Some(vt) = reg.get(&cur) { + names.extend(vt.methods.keys().cloned()); + names.extend(vt.accessors.keys().cloned()); + } + } + } + } + for name in &names { + let slot = class_prototype_method_guard_slot(name) as usize; + #[cfg(not(test))] + PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED_BY_METHOD[slot] + .store(1, std::sync::atomic::Ordering::Release); + #[cfg(test)] + CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED_BY_METHOD + .write() + .unwrap() + .insert(slot as u16); + } + retire_prototype_dependent_caches(); +} + pub(crate) fn invalidate_class_prototype_fast_guards() { #[cfg(not(test))] PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(1, std::sync::atomic::Ordering::Release); diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index 541ea78ef8..f00ba97df3 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -641,6 +641,32 @@ pub(crate) fn class_decl_prototype_relinked(cid: u32) -> bool { && super::super::prototype_chain::object_has_user_prototype_override(decl_proto as usize) } +/// `key` read on the rest of class `cid`'s instance chain past its declared +/// prototype, with `receiver` as the accessor receiver: `None` while that +/// prototype stands on its class default (the parent class id names the next +/// hop), `Some(None)` when the recorded link ends without `key`. +/// +/// This is `super.key` for a method whose home object is `cid`'s prototype: +/// `super` is the home object's current `[[Prototype]]`. +/// +/// # Safety +/// `key` must be a live string header; `receiver` a value the caller roots. +pub(crate) unsafe fn relinked_class_prototype_read( + cid: u32, + key: *const crate::StringHeader, + receiver: f64, +) -> Option> { + let decl_proto = class_decl_prototype_object(cid); + if decl_proto.is_null() { + return None; + } + match relinked_decl_prototype_field(decl_proto, key, receiver) { + RelinkedRead::NotRelinked => None, + RelinkedRead::Answered(value) => Some(Some(value)), + RelinkedRead::Missed => Some(None), + } +} + /// What the rest of a declared prototype's chain answers once a user /// operation replaced that prototype's `[[Prototype]]`. enum RelinkedRead { diff --git a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs index 85c214e4bb..8f3d04bd3f 100644 --- a/crates/perry-runtime/src/object/native_call_method/collection_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/collection_methods.rs @@ -512,7 +512,9 @@ pub(super) unsafe fn dispatch_raw_pointer( )); } } - match get_parent_class_id(cur_cid) { + match crate::object::class_registry::instance_chain_parent_class_id( + cur_cid, + ) { Some(pid) if pid != 0 => { cur_cid = pid; depth += 1; diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index f61fa77a8d..2011a8e0e1 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -1165,7 +1165,9 @@ pub(super) unsafe fn dispatch_handle( break; } } - match get_parent_class_id(cur_cid) { + match crate::object::class_registry::instance_chain_parent_class_id( + cur_cid, + ) { Some(pid) if pid != 0 => { cur_cid = pid; depth += 1; diff --git a/crates/perry-runtime/src/object/native_module/class_ref_values.rs b/crates/perry-runtime/src/object/native_module/class_ref_values.rs index eb90b4be4a..dfafc6064b 100644 --- a/crates/perry-runtime/src/object/native_module/class_ref_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_ref_values.rs @@ -87,7 +87,7 @@ pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { /// class metadata ("may this chain resolve `name`?") for paths that must not /// materialize a prototype; it never answers a property query itself. pub(crate) fn class_instance_has_member(class_id: u32, name: &str) -> bool { - class_chain_declares(class_id, name, true) + class_chain_declares(class_id, name, true, false) } /// Wall 10 — `name in instance` for a class instance whose walk found nothing: @@ -97,11 +97,20 @@ pub(crate) fn class_instance_has_member(class_id: u32, name: &str) -> bool { /// see, which made `'method' in instance` wrongly `false` (NestJS's app Proxy /// gates routing on `'listen' in receiver`). Accessors are not consulted: they /// are real properties of the class prototype, which that walk visits. +/// +/// It answers for an instance's chain, so it stops where that chain leaves the +/// declared classes: past a class whose prototype a user relinked, the parent's +/// methods are not inherited (`instance_chain_parent_class_id`), and the +/// ordinary walk has already read the recorded link. pub(crate) fn class_instance_has_method(class_id: u32, name: &str) -> bool { - class_chain_declares(class_id, name, false) + class_chain_declares(class_id, name, false, true) } -fn class_chain_declares(class_id: u32, name: &str, accessors: bool) -> bool { +/// `instance_chain`: the walk answers for an instance's `[[Prototype]]` chain +/// and stops at a relinked class prototype. The member filter keeps the +/// declared chain: it serves constructor-side reads, whose chain is the +/// constructor's own and does not change when `C.prototype` is relinked. +fn class_chain_declares(class_id: u32, name: &str, accessors: bool, instance_chain: bool) -> bool { if class_id == 0 { return false; } @@ -125,7 +134,12 @@ fn class_chain_declares(class_id: u32, name: &str, accessors: bool) -> bool { return true; } } - match super::class_registry::get_parent_class_id(cid) { + let parent = if instance_chain { + super::class_registry::instance_chain_parent_class_id(cid) + } else { + super::class_registry::get_parent_class_id(cid) + }; + match parent { Some(p) if p != 0 && p != cid => { cid = p; depth += 1; diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 7b7e45fe3d..b6a512fd02 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -554,6 +554,7 @@ fn object_set_static_prototype_impl(obj_ptr: usize, proto_bits: u64, link_kind: // already observe the latch (see `USER_PROTO_OVERRIDE_EVER`). USER_PROTO_OVERRIDE_EVER.store(true, Ordering::Release); (*meta).flags |= crate::object::OBJECT_META_FLAG_USER_PROTO_OVERRIDE; + crate::object::class_registry::class_prototype_relinked(obj); } if link_kind == PrototypeLinkKind::ClassEvaluation { (*meta).flags |= crate::object::OBJECT_META_FLAG_CLASS_EVALUATION_PROTO; diff --git a/crates/perry/tests/class_relink_methods.rs b/crates/perry/tests/class_relink_methods.rs new file mode 100644 index 0000000000..efb02e0161 --- /dev/null +++ b/crates/perry/tests/class_relink_methods.rs @@ -0,0 +1,168 @@ +//! `Object.setPrototypeOf(C.prototype, X)` on a declared class: the methods, +//! getters and setters declared in C's old parent classes are off the +//! instance chain. Reads, `in`, `Reflect.has`, calls (any-typed, class-typed, +//! `this.m()`) and `super.m()` inside C's methods must all answer from the new +//! chain, at sites that ran before the relink and at fresh ones; C's own +//! members and X's (including another class's declared methods) stay visible; +//! static members are unaffected. The program and node's output are the +//! `one_shape_class_relink_methods` fixture. +//! +//! Every walk over declared class members (the vtable lookups behind reads, +//! `in` and call dispatch) followed the parent class id registered at +//! declaration, and the compiler's dispatch tower and `super.m()` call the +//! ancestor's body it resolved along the declared `extends` chain. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +const SOURCE: &str = include_str!("../../../tests/fixtures/one_shape_class_relink_methods/main.ts"); +const EXPECTED: &str = + include_str!("../../../tests/fixtures/one_shape_class_relink_methods/expected.txt"); + +fn stat(stderr: &str, name: &str) -> u64 { + let line = stderr + .lines() + .find(|l| l.starts_with("[method-site]")) + .unwrap_or_else(|| panic!("no [method-site] line in:\n{stderr}")); + line.split_whitespace() + .find_map(|w| w.strip_prefix(name).and_then(|v| v.strip_prefix('='))) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| panic!("no {name} in {line}")) +} + +fn compile(dir: &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 +} + +fn mismatches(expected: &str, stdout: &str) -> Vec { + 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() + )); + } + wrong +} + +#[test] +fn old_parent_members_leave_a_relinked_class_chain() { + let dir = tempfile::tempdir().expect("tempdir"); + let output = compile(dir.path(), SOURCE); + // Runtime trip counts: a fixed small loop is unrolled and its site never + // reached. The output does not depend on the count. + for n in ["40", "41"] { + let run = Command::new(&output) + .arg(n) + .current_dir(dir.path()) + .env("PERRY_METHOD_SITE_STATS", "1") + .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); + let stderr = String::from_utf8_lossy(&run.stderr); + assert!( + run.status.success(), + "n={n}: binary failed ({:?})\nstderr:\n{stderr}", + run.status + ); + let wrong = mismatches(EXPECTED, &stdout); + assert!(wrong.is_empty(), "n={n}: stale members {wrong:?}\n{stdout}"); + // The class read sites must have primed, and hit, before the relinks, + // or nothing here tested what they validate. + assert!( + stat(&stderr, "class_read_primes") > 0 && stat(&stderr, "class_read_hits") > 0, + "n={n}: no class read entry primed and hit\n{stderr}" + ); + } +} + +/// The dispatch tower a call on an untyped receiver compiles to hard-codes, per +/// class id, the body that class inherits. Past eight implementors it has no +/// shape probe, and it ignored prototype surgery entirely: a redefined parent +/// method kept running the old body. Its arms now consult the same per-name +/// invalidation bytes as the narrow tower. +#[test] +fn wide_dispatch_tower_sees_a_redefined_parent_method() { + let mut source = String::from("function callM(o: any): any { return o.m(); }\n"); + for i in 1..=10 { + source.push_str(&format!( + "class B{i} {{ m() {{ return {i}; }} }}\nclass C{i} extends B{i} {{}}\n" + )); + } + source.push_str( + "const i: any = new C1();\n\ + console.log(\"a\", callM(i));\n\ + Object.defineProperty(B1.prototype, \"m\", { value: function () { return 99; }, writable: true, configurable: true });\n\ + console.log(\"b\", callM(i), i.m());\n\ + const j: any = new C2();\n\ + console.log(\"c\", callM(j));\n\ + Object.setPrototypeOf(C2.prototype, { k: 7 });\n\ + try { console.log(\"d\", callM(j)); } catch (e) { console.log(\"d\", e instanceof TypeError); }\n", + ); + let dir = tempfile::tempdir().expect("tempdir"); + let output = compile(dir.path(), &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 ({:?})\n{stdout}", + run.status + ); + // node 26.5.1 + let wrong = mismatches("a 1\nb 99 99\nc 2\nd true\n", &stdout); + assert!(wrong.is_empty(), "{wrong:?}\n{stdout}"); +} + +/// A getter on the replacement super chain can return a Proxy with [[Call]]. +/// Object-target proxies stay non-callable even if they carry an apply trap. +#[test] +fn relinked_super_calls_callable_proxies() { + let source = + include_str!("../../../tests/fixtures/one_shape_class_relink_methods/proxy_super.ts"); + let expected = include_str!( + "../../../tests/fixtures/one_shape_class_relink_methods/proxy_super.expected.txt" + ); + let dir = tempfile::tempdir().expect("tempdir"); + let output = compile(dir.path(), source); + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled proxy super fixture"); + let stdout = String::from_utf8_lossy(&run.stdout); + let stderr = String::from_utf8_lossy(&run.stderr); + assert!( + run.status.success(), + "proxy super fixture failed ({:?})\n{stdout}\n{stderr}", + run.status + ); + let wrong = mismatches(expected, &stdout); + assert!(wrong.is_empty(), "{wrong:?}\n{stdout}\n{stderr}"); +} diff --git a/tests/fixtures/one_shape_class_relink_methods/check.sh b/tests/fixtures/one_shape_class_relink_methods/check.sh new file mode 100755 index 0000000000..ac9076496c --- /dev/null +++ b/tests/fixtures/one_shape_class_relink_methods/check.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# A relinked class prototype (Object.setPrototypeOf(C.prototype, X)): members +# declared in C's old parent classes are off the chain for reads, `in`, calls +# and super calls; C's own members and X's stay visible (expected.txt is node +# 26.5.1's output). The class read sites must have primed and hit, or the +# fixture proves nothing about their facts. +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 +sum_counter() { + awk -v key="$2" 'index($0,key"="){split($0,a,key"="); split(a[2],b,/[^0-9]/); sum+=b[1]} END{print sum+0}' "$1" +} +for n in 40 41; do + PERRY_METHOD_SITE_STATS=1 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" + primes=$(sum_counter "$tmp_dir/err" class_read_primes) + hits=$(sum_counter "$tmp_dir/err" class_read_hits) + test "$primes" -gt 0 + test "$hits" -gt 0 +done +printf 'class_read_primes=%s class_read_hits=%s\n' "$primes" "$hits" diff --git a/tests/fixtures/one_shape_class_relink_methods/expected.txt b/tests/fixtures/one_shape_class_relink_methods/expected.txt new file mode 100644 index 0000000000..be023dc12b --- /dev/null +++ b/tests/fixtures/one_shape_class_relink_methods/expected.txt @@ -0,0 +1,50 @@ +unprimed-typeof undefined +unprimed-in false +unprimed-call TypeError +unprimed-k 7 +primed-before-typeof function +primed-before-call ok:2 +primed-before-fn function,ok:2,true +primed-after-typeof undefined +primed-after-in false +primed-after-call TypeError +primed-after-fn undefined,TypeError,false +primed-new-instance undefined,TypeError +same-site-read function/undefined +same-site-call 3/TypeError +same-site-in true/false +own-before own1 +own-after own1,function,true +own-inherited-gone undefined +null-before function,function +null-after undefined,false,undefined,false +null-call TypeError +null-own own5 +to-class-before B6,undefined +to-class-call ok:D6:t +to-class-dOnly ok:dOnly +to-class-only-gone undefined,false +to-class-fn function,ok:D6:t,true +to-class-identity true +accessor-before getB,true,true +accessor-getter undefined,false +accessor-setter undefined,true,9 +super-before C8>B8 +super-to-class ok:C8>D8 +super-to-plain TypeError +static-call ok:sB +static-in true,function +static-proto-of-C9 true +deep-before A10,b,a +deep-after ok:X10,ok:b,undefined,false +back-gone undefined +back-restored ok:B11,true +reflect-get undefined +reflect-has false +own-names-proto constructor|own +for-in k +typed-before ok:13,ok:13,ok:13,ok:13,g13 +typed-after TypeError,TypeError,TypeError,undefined,false +typed-subclass TypeError,TypeError,undefined +typed-this-call TypeError +typed-getter undefined,undefined diff --git a/tests/fixtures/one_shape_class_relink_methods/main.ts b/tests/fixtures/one_shape_class_relink_methods/main.ts new file mode 100644 index 0000000000..7653d11e75 --- /dev/null +++ b/tests/fixtures/one_shape_class_relink_methods/main.ts @@ -0,0 +1,218 @@ +// `Object.setPrototypeOf(C.prototype, X)` replaces the chain a class instance +// inherits through. A method, getter or setter DECLARED in C's old parent class +// body is then off the chain: it must not be read, found by `in`, or called. +// C's own declared members stay, X's members (including another class's +// declared methods) become visible, `super.m()` in C's methods follows the +// relink (the home object's [[Prototype]] is the new link), and static members +// (C's constructor still inherits from B) are unaffected. +// +// Class names are unique per block: a block-scoped duplicate class name is a +// separate bug, and this file must not depend on it. +// +// Each line is `name value`, compared with node. Same-site cases relink +// mid-loop at a parameter receiver with an argv trip count (a fixed small loop +// is unrolled and a literal receiver refined, so neither would reach the site). +const N = Number(process.argv[2] ?? "40"); +const H = N >> 1; +function show(name: string, v: any): void { + console.log(name + " " + String(v)); +} +function tryCall(f: () => any): string { + try { + return "ok:" + String(f()); + } catch (e) { + return (e instanceof TypeError) ? "TypeError" : "other:" + String(e); + } +} +function readM(o: any): any { return typeof o.m; } +function callM(o: any): string { return tryCall(() => o.m()); } +function hasM(o: any): boolean { return "m" in o; } +function loopRead(o: any, n: number, at: number, relink: () => void): string { + let before: any = undefined; + let after: any = undefined; + for (let i = 0; i < n; i++) { + if (i === at) relink(); + const v = typeof o.m; + if (i < at) before = v; else after = v; + } + return String(before) + "/" + String(after); +} +function loopCall(o: any, n: number, at: number, relink: () => void): string { + let before: any = undefined; + let after: any = undefined; + for (let i = 0; i < n; i++) { + if (i === at) relink(); + let v: any; + try { v = o.m(); } catch (e) { v = (e instanceof TypeError) ? "TypeError" : "other"; } + if (i < at) before = v; else after = v; + } + return String(before) + "/" + String(after); +} +function loopIn(o: any, n: number, at: number, relink: () => void): string { + let before: any = undefined; + let after: any = undefined; + for (let i = 0; i < n; i++) { + if (i === at) relink(); + const v = "m" in o; + if (i < at) before = v; else after = v; + } + return String(before) + "/" + String(after); +} + +// 1. unprimed: the old parent's declared method is gone +{ + class B1 { m() { return 1; } } + class C1 extends B1 {} + const inst: any = new C1(); + Object.setPrototypeOf(C1.prototype, { k: 7 }); + show("unprimed-typeof", typeof inst.m); + show("unprimed-in", "m" in inst); + show("unprimed-call", tryCall(() => inst.m())); + show("unprimed-k", inst.k); +} +// 2. primed at other sites before the relink +{ + class B2 { m() { return 2; } } + class C2 extends B2 {} + const inst: any = new C2(); + show("primed-before-typeof", typeof inst.m); + show("primed-before-call", tryCall(() => inst.m())); + show("primed-before-fn", readM(inst) + "," + callM(inst) + "," + hasM(inst)); + Object.setPrototypeOf(C2.prototype, { k: 7 }); + show("primed-after-typeof", typeof inst.m); + show("primed-after-in", "m" in inst); + show("primed-after-call", tryCall(() => inst.m())); + show("primed-after-fn", readM(inst) + "," + callM(inst) + "," + hasM(inst)); + show("primed-new-instance", readM(new C2()) + "," + callM(new C2())); +} +// 3. same site, relink mid-loop +{ + class B3 { m() { return 3; } } + class C3a extends B3 {} + class C3b extends B3 {} + class C3c extends B3 {} + show("same-site-read", loopRead(new C3a(), N, H, () => Object.setPrototypeOf(C3a.prototype, { k: 7 }))); + show("same-site-call", loopCall(new C3b(), N, H, () => Object.setPrototypeOf(C3b.prototype, { k: 7 }))); + show("same-site-in", loopIn(new C3c(), N, H, () => Object.setPrototypeOf(C3c.prototype, { k: 7 }))); +} +// 4. methods declared on C4 itself stay visible, and `this` is the instance +{ + class B4 { m() { return 4; } } + class C4 extends B4 { own() { return "own" + this.v; } v = 1; } + const inst: any = new C4(); + show("own-before", inst.own()); + Object.setPrototypeOf(C4.prototype, { k: 7 }); + show("own-after", inst.own() + "," + typeof inst.own + "," + ("own" in inst)); + show("own-inherited-gone", typeof inst.m); +} +// 5. relink to null: the old parent and Object.prototype are both off the chain +{ + class B5 { m() { return 5; } } + class C5 extends B5 { own() { return "own5"; } } + const inst: any = new C5(); + show("null-before", typeof inst.m + "," + typeof inst.toString); + Object.setPrototypeOf(C5.prototype, null); + show("null-after", typeof inst.m + "," + ("m" in inst) + "," + typeof inst.toString + "," + ("toString" in inst)); + show("null-call", tryCall(() => inst.m())); + show("null-own", inst.own()); +} +// 6. relink onto another class's prototype: that class's methods appear +{ + class B6 { m() { return "B6"; } only() { return "onlyB"; } } + class D6 { m() { return "D6:" + this.tag; } dOnly() { return "dOnly"; } } + class C6 extends B6 { tag = "t"; } + const inst: any = new C6(); + show("to-class-before", inst.m() + "," + typeof inst.dOnly); + Object.setPrototypeOf(C6.prototype, D6.prototype); + show("to-class-call", tryCall(() => inst.m())); + show("to-class-dOnly", tryCall(() => inst.dOnly())); + show("to-class-only-gone", typeof inst.only + "," + ("only" in inst)); + show("to-class-fn", readM(inst) + "," + callM(inst) + "," + hasM(inst)); + show("to-class-identity", inst.m === D6.prototype.m); +} +// 7. getters and setters declared in the old parent +{ + class B7 { + get g() { return "getB"; } + set s(v: any) { (this as any).fromSetter = v; } + } + class C7 extends B7 {} + const inst: any = new C7(); + show("accessor-before", inst.g + "," + ("g" in inst) + "," + ("s" in inst)); + Object.setPrototypeOf(C7.prototype, { k: 7 }); + show("accessor-getter", String(inst.g) + "," + ("g" in inst)); + inst.s = 9; + show("accessor-setter", String(inst.fromSetter) + "," + Object.prototype.hasOwnProperty.call(inst, "s") + "," + inst.s); +} +// 8. super.m() in C8's own methods follows the home object's new [[Prototype]] +{ + class B8 { m() { return "B8"; } } + class D8 { m() { return "D8"; } } + class C8 extends B8 { + m() { return "C8>" + super.m(); } + } + const inst: any = new C8(); + show("super-before", inst.m()); + Object.setPrototypeOf(C8.prototype, D8.prototype); + show("super-to-class", tryCall(() => inst.m())); + Object.setPrototypeOf(C8.prototype, { k: 7 }); + show("super-to-plain", tryCall(() => inst.m())); +} +// 9. static members: C9's constructor still inherits from B9 +{ + class B9 { static sm() { return "sB"; } m() { return 9; } } + class C9 extends B9 {} + Object.setPrototypeOf(C9.prototype, { k: 7 }); + show("static-call", tryCall(() => (C9 as any).sm())); + show("static-in", ("sm" in C9) + "," + typeof (C9 as any).sm); + show("static-proto-of-C9", Object.getPrototypeOf(C9) === B9); +} +// 10. deeper: an intermediate class prototype relinked +{ + class A10 { m() { return "A10"; } a() { return "a"; } } + class B10 extends A10 { b() { return "b"; } } + class C10 extends B10 {} + const inst: any = new C10(); + show("deep-before", inst.m() + "," + inst.b() + "," + inst.a()); + Object.setPrototypeOf(B10.prototype, { m() { return "X10"; } }); + show("deep-after", tryCall(() => inst.m()) + "," + tryCall(() => inst.b()) + "," + typeof inst.a + "," + ("a" in inst)); +} +// 11. relinking back to the old parent restores the method +{ + class B11 { m() { return "B11"; } } + class C11 extends B11 {} + const inst: any = new C11(); + Object.setPrototypeOf(C11.prototype, { k: 7 }); + show("back-gone", typeof inst.m); + Object.setPrototypeOf(C11.prototype, B11.prototype); + show("back-restored", tryCall(() => inst.m()) + "," + ("m" in inst)); +} +// 12. reflection on the relinked chain +{ + class B12 { m() { return 12; } } + class C12 extends B12 { own() { return 1; } } + const inst: any = new C12(); + Object.setPrototypeOf(C12.prototype, { k: 7 }); + show("reflect-get", typeof Reflect.get(inst, "m")); + show("reflect-has", Reflect.has(inst, "m")); + show("own-names-proto", Object.getOwnPropertyNames(C12.prototype).join("|")); + const keys: string[] = []; + for (const k in inst) keys.push(k); + show("for-in", keys.join("|")); +} +// 13. statically typed receivers (a class-typed parameter, a subclass, `this`) +class B13 { m(): number { return 13; } get g(): string { return "g13"; } } +class C13 extends B13 { own(): number { return this.m(); } } +class E13 extends C13 {} +function viaTyped13(c: C13): string { return tryCall(() => c.m()); } +function viaBase13(b: B13): string { return tryCall(() => b.m()); } +{ + const c = new C13(); + const e = new E13(); + show("typed-before", viaTyped13(c) + "," + viaBase13(c) + "," + viaTyped13(e) + "," + tryCall(() => c.own()) + "," + c.g); + Object.setPrototypeOf(C13.prototype, { k: 7 }); + show("typed-after", tryCall(() => c.m()) + "," + viaTyped13(c) + "," + viaBase13(c) + "," + typeof c.m + "," + ("m" in c)); + show("typed-subclass", viaTyped13(e) + "," + tryCall(() => e.m()) + "," + typeof e.m); + show("typed-this-call", tryCall(() => c.own())); + show("typed-getter", String(c.g) + "," + String(e.g)); +} diff --git a/tests/fixtures/one_shape_class_relink_methods/proxy_super.expected.txt b/tests/fixtures/one_shape_class_relink_methods/proxy_super.expected.txt new file mode 100644 index 0000000000..1f0157a689 --- /dev/null +++ b/tests/fixtures/one_shape_class_relink_methods/proxy_super.expected.txt @@ -0,0 +1,8 @@ +proxy 17 1 +function 18 +noncallable true +default-proxy 19 +nested-proxy 20 1 +object-proxy true 0 +revoked-proxy true +getter-throw getter-sentinel diff --git a/tests/fixtures/one_shape_class_relink_methods/proxy_super.ts b/tests/fixtures/one_shape_class_relink_methods/proxy_super.ts new file mode 100644 index 0000000000..8905b16a9c --- /dev/null +++ b/tests/fixtures/one_shape_class_relink_methods/proxy_super.ts @@ -0,0 +1,55 @@ +class Base { + m(value: { n: number }): number { return value.n; } +} +class Child extends Base { + tag = 10; + forward(value: { n: number }): number { return super.m(value); } +} +const child = new Child(); +let reads = 0; +const target = function (this: { tag: number }, value: { n: number }): number { + return this.tag + value.n; +}; +const callable = new Proxy(target, { + apply(fn, receiver, args) { return Reflect.apply(fn, receiver, args); }, +}); +Object.setPrototypeOf(Child.prototype, { + get m() { reads++; return callable; }, +}); +console.log("proxy", child.forward({ n: 7 }), reads); +Object.setPrototypeOf(Child.prototype, { m: target }); +console.log("function", child.forward({ n: 8 })); +Object.setPrototypeOf(Child.prototype, { m: 123 }); +try { child.forward({ n: 9 }); } catch (e) { + console.log("noncallable", e instanceof TypeError); +} + +const directProxy = new Proxy(target, {}); +Object.setPrototypeOf(Child.prototype, { m: directProxy }); +console.log("default-proxy", child.forward({ n: 9 })); +let outerCalls = 0; +const nestedProxy = new Proxy(callable, { + apply(fn, receiver, args) { outerCalls++; return Reflect.apply(fn, receiver, args); }, +}); +Object.setPrototypeOf(Child.prototype, { m: nestedProxy }); +console.log("nested-proxy", child.forward({ n: 10 }), outerCalls); +let invalidApplies = 0; +const objectProxy = new Proxy({ x: 1 }, { + apply() { invalidApplies++; return 999; }, +}); +Object.setPrototypeOf(Child.prototype, { m: objectProxy }); +try { child.forward({ n: 11 }); } catch (e) { + console.log("object-proxy", e instanceof TypeError, invalidApplies); +} +const revocable = Proxy.revocable(target, {}); +Object.setPrototypeOf(Child.prototype, { m: revocable.proxy }); +revocable.revoke(); +try { child.forward({ n: 12 }); } catch (e) { + console.log("revoked-proxy", e instanceof TypeError); +} +Object.setPrototypeOf(Child.prototype, { + get m() { throw new Error("getter-sentinel"); }, +}); +try { child.forward({ n: 13 }); } catch (e) { + console.log("getter-throw", (e as Error).message); +}