diff --git a/changelog.d/9785-9786-array-prototype-chains.md b/changelog.d/9785-9786-array-prototype-chains.md new file mode 100644 index 0000000000..efe6f10461 --- /dev/null +++ b/changelog.d/9785-9786-array-prototype-chains.md @@ -0,0 +1,3 @@ +Array indexed reads and membership checks now follow the full custom prototype chain, stop at explicit null prototypes, and invoke Proxy traps with the original receiver. Strict indexed writes also find inherited accessors and readonly properties beyond an array prototype, while writable own properties on intermediate prototypes continue to shadow ancestors. Fixes #9785 and #9786. + +Regression fixtures cover the reported chain-depth and Proxy cases plus accessor receivers, grown prototypes, undefined shadows, negative Proxy membership checks, nested reads inside traps, and trapless Proxy targets. No version bump. diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 530ac70e32..8b1a2638d4 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -1221,8 +1221,7 @@ fn js_array_set_f64_extend_strict_impl( // the inherited [[Set]] walk. This includes both a retargeted receiver and // the default chain after an index is installed on `Array.prototype` or // `Object.prototype`. `array_custom_prototype` is the #9219 classification - // shared with reads/HasProperty and deliberately returns None for a Proxy - // prototype, whose dedicated dispatch must remain single-shot. Existing + // shared with reads/HasProperty, including a Proxy prototype. Existing // own elements have already had every applicable dense lane above; the // fallback still needs the ownership check for descriptor/restricted // shapes that correctly declined those lanes. @@ -1676,9 +1675,11 @@ pub(crate) fn array_spec_set( inherited_owner = array_object_proto_index_owner(bits, &key); } Some(ArrayCustomProto::Array(proto_arr)) => { - if array_has_own_index(proto_arr, index) { - inherited_owner = proto_arr as usize; - } + default_chain = false; + inherited_owner = array_object_proto_index_owner( + crate::value::js_nanbox_pointer(proto_arr as i64).to_bits(), + &key, + ); } None => {} } diff --git a/crates/perry-runtime/src/array/indexing_proto_chain.rs b/crates/perry-runtime/src/array/indexing_proto_chain.rs index 1f612c8774..d16b310044 100644 --- a/crates/perry-runtime/src/array/indexing_proto_chain.rs +++ b/crates/perry-runtime/src/array/indexing_proto_chain.rs @@ -33,12 +33,19 @@ pub(super) unsafe fn array_oob_prototype_get(receiver: usize, index: u32) -> f64 match array_custom_prototype(arr) { Some(ArrayCustomProto::Null) => return TAG_UNDEFINED_F64, Some(ArrayCustomProto::Other(bits)) => { - return array_object_proto_index_get(arr, bits, index).unwrap_or(TAG_UNDEFINED_F64) + return array_object_proto_index_get( + crate::value::js_nanbox_pointer(receiver as i64), + bits, + index, + ) + .unwrap_or(TAG_UNDEFINED_F64) } Some(ArrayCustomProto::Array(proto_arr)) => { - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return js_array_get_f64(proto_arr, index); - } + return array_spec_get_with_receiver( + proto_arr, + index, + crate::value::js_nanbox_pointer(receiver as i64), + ); } None => {} } @@ -74,20 +81,15 @@ pub(crate) fn array_spec_has_index(arr: *const ArrayHeader, index: u32) -> bool return true; } // An explicit `Object.setPrototypeOf(arr, p)` REPLACES the default - // chain. A real-array `p` keeps the original lane (its own indices - // first, then the implicit `Array.prototype` tail below — test262 - // copyWithin/coerced-values-start-change-*). #9192: any other `p` - // answers the whole question by itself, so the default-chain tail must - // not run after it. + // chain. Every custom prototype answers the whole lookup, including + // an array whose own prototype may be retargeted or null (#9785). match array_custom_prototype(arr) { Some(ArrayCustomProto::Null) => return false, Some(ArrayCustomProto::Other(bits)) => { return array_object_proto_index_has(bits, index) } Some(ArrayCustomProto::Array(proto_arr)) => { - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return true; - } + return array_spec_has_index(proto_arr, index); } None => {} } @@ -120,8 +122,8 @@ pub(crate) enum ArrayCustomProto { /// `Object.setPrototypeOf(arr, null)`: nothing is inherited, and the /// implicit `Array.prototype` → `Object.prototype` chain is gone too. Null, - /// The recorded prototype is itself a real array — the original lane, kept - /// bit-for-bit (test262 copyWithin/coerced-values-start-change-*). + /// The recorded prototype is itself a real array. Its own prototype is + /// authoritative after an own-index miss, just as for any other object. Array(*const ArrayHeader), /// Any other object: resolved through the generic object machinery with the /// array as the receiver, so prototype accessors see the right `this` and @@ -140,11 +142,11 @@ pub(crate) unsafe fn array_custom_prototype(arr: *const ArrayHeader) -> Option Option { - // The caller may still hold a pre-grow forwarding stub; the receiver an - // inherited accessor observes must be the live head. - let arr = clean_arr_ptr(arr); - if arr.is_null() { - return None; - } +unsafe fn array_object_proto_index_get(receiver: f64, proto_bits: u64, index: u32) -> Option { let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(arr as i64)); + let receiver = scope.root_nanbox_f64(receiver); let proto = scope.root_heap_word_u64(proto_bits); let key = index.to_string(); let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); @@ -246,60 +238,46 @@ unsafe fn array_object_proto_index_get( .map(|v| f64::from_bits(v.bits())) } -/// #9192: the first object in a NON-array custom `[[Prototype]]` chain that -/// owns `key` with a descriptor — the owner whose accessor / attributes the -/// spec `Set` must observe before creating an own element on the array. A plain -/// writable data property carries no side-table entry and correctly reports no -/// owner: the Set then creates the own element, as the spec requires. +/// Find the first own indexed property in the actual custom prototype chain. +/// Stop at writable data too: it shadows a non-writable ancestor. The runtime's +/// GetPrototypeOf handles real arrays and synthetic Object.create prototypes +/// without interpreting an ArrayHeader as an ObjectHeader (#9785). pub(crate) unsafe fn array_object_proto_index_owner(proto_bits: u64, key: &str) -> usize { - let mut bits = proto_bits; + let scope = crate::gc::RuntimeHandleScope::new(); + let proto = scope.root_heap_word_u64(proto_bits); + let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + if key_ptr.is_null() { + return 0; + } + let key_handle = scope.root_nanbox_f64(crate::value::nanbox_string_key(key_ptr)); for _ in 0..64 { - if bits == crate::value::TAG_NULL { - return 0; - } - if crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 { + let bits = proto.get_heap_word_u64(); + if bits == crate::value::TAG_NULL + || crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 + { return 0; } let Some(addr) = pointer_bits_of_recorded_prototype(bits) else { return 0; }; - // Pair the band predicate with the validity check (#6279): a handle - // value sits below HANDLE_BAND_MAX and would otherwise be dereferenced - // as if it were an object pointer. - if !crate::value::addr_class::is_above_handle_band(addr as usize) + if !crate::value::addr_class::is_above_handle_band(addr) || !crate::object::is_valid_obj_ptr(addr as *const u8) { return 0; } - if crate::object::get_accessor_descriptor(addr, key).is_some() - || crate::object::get_property_attrs(addr, key).is_some() - { - return addr; + let addr = crate::value::resolve_forwarding(addr); + let value = crate::value::js_nanbox_pointer(addr as i64); + proto.set_heap_word_u64(value.to_bits()); + if crate::object::obj_value_has_own_key(value, key_handle.get_nanbox_f64()) { + return crate::value::js_nanbox_get_pointer(f64::from_bits(proto.get_heap_word_u64())) + as usize; } - match crate::object::prototype_chain::object_static_prototype(addr) { - Some(next) => bits = next, - // #9220: `Object.create(p)` does NOT record `p` in the observable - // prototype side table — `js_object_create` models the link with a - // SYNTHETIC CLASS ID whose `class_prototype_object` entry is `p` - // (#809). The recorded-prototype hop alone therefore stops one link - // short, and an inherited accessor / non-writable index that the - // READ side already resolves (`js_object_get_field_by_name`'s - // `class_id != 0` branch, reached through - // `resolve_inherited_field_from_prototype`) was silently replaced by - // a new own element on the array. Take the same hop the read walk - // takes so `[[Set]]` and `[[Get]]` agree on the chain. - None => { - let class_id = (*(addr as *const crate::ObjectHeader)).class_id; - if class_id == 0 { - return 0; - } - let synth = crate::object::class_prototype_object(class_id); - if synth.is_null() || synth as usize == addr { - return 0; - } - bits = crate::value::js_nanbox_pointer(synth as i64).to_bits(); - } + let next = + crate::object::js_object_get_prototype_of(f64::from_bits(proto.get_heap_word_u64())); + if next.to_bits() == proto.get_heap_word_u64() { + return 0; } + proto.set_heap_word_u64(next.to_bits()); } 0 } @@ -323,29 +301,32 @@ unsafe fn array_object_proto_index_has(proto_bits: u64, index: u32) -> bool { /// (firing index accessors via `js_array_get_f64`) or, for an absent own index, /// the inherited `Array.prototype[index]`. Returns `undefined` when absent. pub(crate) fn array_spec_get(arr: *const ArrayHeader, index: u32) -> f64 { + let arr = clean_arr_ptr(arr); + array_spec_get_with_receiver(arr, index, crate::value::js_nanbox_pointer(arr as i64)) +} + +fn array_spec_get_with_receiver(arr: *const ArrayHeader, index: u32, receiver: f64) -> f64 { const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); let arr = clean_arr_ptr(arr); if arr.is_null() { return TAG_UNDEFINED_F64; } unsafe { - let receiver = crate::value::js_nanbox_pointer(arr as i64); let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); if array_has_own_index(arr, index) { - return js_array_get_f64(arr, index); + return array_inherited_index_get(arr, index, receiver.get_nanbox_f64()); } // #9192: see `array_spec_has_index` — a non-array custom prototype // replaces the default chain outright. match array_custom_prototype(arr) { Some(ArrayCustomProto::Null) => return TAG_UNDEFINED_F64, Some(ArrayCustomProto::Other(bits)) => { - return array_object_proto_index_get(arr, bits, index).unwrap_or(TAG_UNDEFINED_F64) + return array_object_proto_index_get(receiver.get_nanbox_f64(), bits, index) + .unwrap_or(TAG_UNDEFINED_F64) } Some(ArrayCustomProto::Array(proto_arr)) => { - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return array_inherited_index_get(proto_arr, index, receiver.get_nanbox_f64()); - } + return array_spec_get_with_receiver(proto_arr, index, receiver.get_nanbox_f64()); } None => {} } diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 088f9d70d1..37247daf70 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -256,9 +256,10 @@ impl FieldLookupCaches { pub use accessors::js_object_get_field; pub(crate) use accessors::{ accessor_receiver_override_begin, accessor_receiver_override_end, - array_prototype_property_value, builtin_reflection_accessor_read, class_getter_this, - invoke_accessor_getter, invoke_accessor_setter, is_typed_array_prototype, - object_field_at_with_live, ordinary_object_prototype_property_value, own_data_field_by_name, + accessor_receiver_override_take, array_prototype_property_value, + builtin_reflection_accessor_read, class_getter_this, invoke_accessor_getter, + invoke_accessor_setter, is_typed_array_prototype, object_field_at_with_live, + ordinary_object_prototype_property_value, own_data_field_by_name, primitive_builtin_prototype_property, primitive_object_prototype_accessor, string_index_value, }; pub(crate) use class_object_props::class_object_prototype_value; diff --git a/crates/perry-runtime/src/object/field_get_set/accessors.rs b/crates/perry-runtime/src/object/field_get_set/accessors.rs index 8c477cdb50..fbc65b3cb4 100644 --- a/crates/perry-runtime/src/object/field_get_set/accessors.rs +++ b/crates/perry-runtime/src/object/field_get_set/accessors.rs @@ -408,6 +408,12 @@ pub(crate) fn accessor_receiver_override_begin(receiver: f64) -> Option { }) } +/// Consume the original receiver before entering user code through a Proxy, +/// just as invoke_accessor_getter does before entering a getter body. +pub(crate) fn accessor_receiver_override_take() -> Option { + ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.take()) +} + pub(crate) fn accessor_receiver_override_end(prev: Option) { ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.set(prev)); } diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index a81839c513..e107abafc7 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -874,14 +874,8 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { // Issue #233: resolve a grow forwarding pointer so `index in arr` // / `arr.hasOwnProperty(i)` stay correct after `arr.length = N`. let arr = crate::array::clean_arr_ptr(obj_ptr as *const crate::array::ArrayHeader); - let length = (*arr).length; - // A Proxy installed as the array's `[[Prototype]]` - // (`Object.setPrototypeOf(arr, proxy)`) — `array_spec_has_index` - // only recognizes a *real array* custom prototype, so a Proxy - // hop is silently treated as absent. Recover it here so the - // idx/string-key misses below can fall back to the proxy's - // `[[HasProperty]]` instead of a bare `false` (ECMA-262 10.1.7.1 - // step 5). + // Named keys still need Proxy dispatch below. Indexed keys use + // array_spec_has_index, which owns the complete prototype walk. let proxy_proto = super::super::prototype_chain::object_static_prototype(obj_ptr as usize) .filter(|&b| (b >> 48) == 0x7FFD) @@ -908,38 +902,11 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { None }; if let Some(idx) = idx { - let _ = length; - // Spec HasProperty: own (dense slot / sparse named prop / - // accessor descriptor) OR inherited — a custom array - // [[Prototype]], `Array.prototype[i]`, or an - // `Object.prototype` index (data or accessor; test262 - // sort/precise-comparefn-throws checks `'2' in array` - // against an Object.prototype accessor). - if crate::array::array_spec_has_index(arr, idx) { - return nanbox_true; - } - if crate::array::object_prototype_has_index_prop(idx) { - return nanbox_true; - } - if let Some(proxy) = proxy_proto { - let idx_str = idx.to_string(); - let key_ptr = crate::string::js_string_from_bytes( - idx_str.as_ptr(), - idx_str.len() as u32, - ); - let key_val = f64::from_bits( - crate::value::js_nanbox_string(key_ptr as i64).to_bits(), - ); - return if crate::value::js_is_truthy(crate::proxy::js_proxy_has( - proxy, key_val, - )) != 0 - { - nanbox_true - } else { - nanbox_false - }; - } - return nanbox_false; + return if crate::array::array_spec_has_index(arr, idx) { + nanbox_true + } else { + nanbox_false + }; } if key_val.is_any_string() { let key_str = crate::value::js_get_string_pointer_unified(key) @@ -952,17 +919,11 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { return nanbox_true; } if let Some(idx) = super::super::canonical_array_index(key_name) { - // Same spec HasProperty protocol as the - // numeric-key arm above: own + inherited - // (custom array proto / Array.prototype / - // Object.prototype data-or-accessor index; - // test262 sort/precise-comparefn-throws does - // `'2' in array`). - if crate::array::array_spec_has_index(arr, idx) - || crate::array::object_prototype_has_index_prop(idx) - { - return nanbox_true; - } + return if crate::array::array_spec_has_index(arr, idx) { + nanbox_true + } else { + nanbox_false + }; } else if array_prototype_property_value(key_name, obj_ptr as usize) .is_some() { diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index ba3b1d3ca9..29a9ce8084 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -571,16 +571,9 @@ pub(crate) fn resolve_inherited_field_from_prototype( return None; } let key_val = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj_ptr as i64).to_bits()); - let scope = crate::gc::RuntimeHandleScope::new(); - let previous_this = super::js_implicit_this_set(receiver); - let previous_this_handle = scope.root_nanbox_f64(previous_this); - let v = crate::proxy::js_proxy_get(proto_val, key_val); - super::js_implicit_this_set(previous_this_handle.get_nanbox_f64()); - if v.to_bits() == crate::value::TAG_UNDEFINED { - return None; - } + let receiver = super::field_get_set::accessor_receiver_override_take() + .unwrap_or_else(|| crate::value::js_nanbox_pointer(obj_ptr as i64)); + let v = crate::proxy::proxy_get_with_receiver(proto_val, key_val, receiver); return Some(crate::value::JSValue::from_bits(v.to_bits())); } } diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 66cc23fc2b..849c0c32d4 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -25,6 +25,9 @@ use crate::closure::{js_closure_call0, js_closure_call1, js_closure_call2, js_cl mod apply_construct; pub use apply_construct::{call_proxy_value_with_this, js_proxy_apply, js_proxy_construct}; pub(crate) use apply_construct::{is_callable_function, is_constructor_function}; +mod get; +pub use get::js_proxy_get; +pub(crate) use get::proxy_get_with_receiver; mod has_delete; pub(crate) use has_delete::reflect_ordinary_delete_property_key; pub use has_delete::{js_proxy_delete, js_proxy_has}; @@ -912,109 +915,6 @@ fn call_with_this_and_args(f: f64, this_arg: f64, args: &[f64]) -> f64 { result } -/// Detect the runtime's "null object" sentinel returned by -/// `js_native_call_method` when a method lookup falls off the end. -/// `proxy[key]` — if handler.get exists, call it with (target, key); -/// otherwise fetch the field from the target directly via the generic path. -#[no_mangle] -pub extern "C" fn js_proxy_get(proxy_boxed: f64, key: f64) -> f64 { - let _proxy_pin = pin_proxy_for_native_call(proxy_boxed); - let id = match lookup(proxy_boxed) { - Some(id) => id, - None => return f64::from_bits(TAG_UNDEFINED), - }; - // `[[Get]] ( P, Receiver )` receives an already-computed property key P, but - // codegen calls this helper with the raw index value for a computed read on - // a statically-known proxy (`proxy[10]` lowers to - // `js_proxy_get(proxy, 10.0)`). Apply `ToPropertyKey` so a numeric index is - // seen by the trap as the canonical string key (`10` -> `"10"`) and the - // forward-to-target path below stringifies consistently. Symbols and - // strings pass through unchanged. Without this the get trap received a raw - // number and key-equality checks (`key === "10"`) silently failed (test262 - // Proxy/get/trap-is-{null,undefined}-target-is-proxy `proxy[10]`). A key - // that is already a string (the overwhelmingly common `proxy.foo` case) or - // a symbol is left untouched, so this only pays `ToPropertyKey` for the - // numeric / object-index forms. - let key = { - let tag = key.to_bits() & 0xFFFF_0000_0000_0000; - let is_string_key = - tag == crate::value::STRING_TAG || tag == crate::value::SHORT_STRING_TAG; - if is_string_key || unsafe { crate::symbol::js_is_symbol(key) } != 0 { - key - } else { - unsafe { crate::object::js_to_property_key(key) } - } - }; - let (target, handler, revoked) = PROXIES.with(|p| { - p.borrow() - .get(id as usize) - .and_then(|o| o.as_ref()) - .map(|e| (e.target, e.handler, e.revoked)) - .unwrap_or(( - f64::from_bits(TAG_UNDEFINED), - f64::from_bits(TAG_UNDEFINED), - false, - )) - }); - if revoked { - return revoked_return(); - } - let trap = handler_trap(handler, "get"); - if is_callable(trap) { - let scope = crate::gc::RuntimeHandleScope::new(); - let target_h = scope.root_nanbox_f64(target); - let key_h = scope.root_nanbox_f64(key); - let result = call_trap( - handler, - trap, - &[ - target_h.get_nanbox_f64(), - key_h.get_nanbox_f64(), - proxy_boxed, - ], - ); - let result_h = scope.root_nanbox_f64(result); - invariants::enforce_get_invariant( - target_h.get_nanbox_f64(), - key_h.get_nanbox_f64(), - result_h.get_nanbox_f64(), - ); - return result_h.get_nanbox_f64(); - } - // No get trap — forward to the target's `[[Get]]`. A proxy target must - // recurse through proxy dispatch rather than `target_get`, which would deref - // the fake pointer. - if lookup(target).is_some() { - return js_proxy_get(target, key); - } - // `p.apply` / `p.call` / `p.bind` VALUE reads on a callable-wrapping - // proxy resolve to Function.prototype's methods with the PROXY as the - // receiver — reify a bound method so a later invocation dispatches - // `js_native_call_method(proxy, "call", …)` and routes through the - // proxy's [[Call]] (apply trap). Reading off the target instead would - // bypass the trap. (Test262 proxy-toString reads `.apply` as a value; - // Function.prototype.toString on the reified method is the - // NativeFunction form.) - if crate::object::value_is_callable(target) { - if let Some(name) = key_to_rust_string(key) { - let method: Option<&'static [u8]> = match name.as_str() { - "apply" => Some(b"apply"), - "call" => Some(b"call"), - "bind" => Some(b"bind"), - _ => None, - }; - if let Some(m) = method { - // Only when the target has no OWN override of the slot. - let t_ptr = extract_pointer(target.to_bits()) as usize; - if !crate::closure::closure_has_own_dynamic_prop(t_ptr, &name) { - return unsafe { crate::closure::reify_function_method_value(proxy_boxed, m) }; - } - } - } - } - target_get(target, key) -} - /// Resolve the ultimate target when a Proxy wraps a class constructor. Used /// by method-call dispatch to bind a static method's visible `this` to the /// Proxy receiver while retaining the target class as its lexical owner. @@ -1165,18 +1065,6 @@ fn target_get_property_key(target: f64, property_key: f64) -> f64 { crate::object::js_object_get_field_by_name_f64(obj_ptr, key_ptr) } -fn target_get(target: f64, key: f64) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let target_handle = scope.root_nanbox_f64(target); - let key_handle = scope.root_nanbox_f64(key); - let property_key_handle = scope - .root_nanbox_f64(unsafe { crate::object::js_to_property_key(key_handle.get_nanbox_f64()) }); - target_get_property_key( - target_handle.get_nanbox_f64(), - property_key_handle.get_nanbox_f64(), - ) -} - /// `Reflect.set` with an explicit receiver: OrdinarySet(target, P, V, /// receiver), boolean result NaN-boxed. pub(crate) fn reflect_ordinary_set_with_receiver( diff --git a/crates/perry-runtime/src/proxy/get.rs b/crates/perry-runtime/src/proxy/get.rs new file mode 100644 index 0000000000..08c1ab2e3c --- /dev/null +++ b/crates/perry-runtime/src/proxy/get.rs @@ -0,0 +1,126 @@ +//! Proxy [[Get]] carries the original Receiver through traps and target hops. +use super::*; + +/// `proxy[key]` uses the proxy itself as Receiver. Prototype and Reflect reads +/// use the explicit-receiver entry below. +#[no_mangle] +pub extern "C" fn js_proxy_get(proxy_boxed: f64, key: f64) -> f64 { + proxy_get_with_receiver(proxy_boxed, key, proxy_boxed) +} + +pub(crate) fn proxy_get_with_receiver(proxy_boxed: f64, key: f64, receiver: f64) -> f64 { + let _proxy_pin = pin_proxy_for_native_call(proxy_boxed); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let id = match lookup(proxy_boxed) { + Some(id) => id, + None => return f64::from_bits(TAG_UNDEFINED), + }; + // `[[Get]] ( P, Receiver )` receives an already-computed property key P, but + // codegen calls this helper with the raw index value for a computed read on + // a statically-known proxy (`proxy[10]` lowers to + // `js_proxy_get(proxy, 10.0)`). Apply `ToPropertyKey` so a numeric index is + // seen by the trap as the canonical string key (`10` -> `"10"`) and the + // forward-to-target path below stringifies consistently. Symbols and + // strings pass through unchanged. Without this the get trap received a raw + // number and key-equality checks (`key === "10"`) silently failed (test262 + // Proxy/get/trap-is-{null,undefined}-target-is-proxy `proxy[10]`). A key + // that is already a string (the overwhelmingly common `proxy.foo` case) or + // a symbol is left untouched, so this only pays `ToPropertyKey` for the + // numeric / object-index forms. + let key = { + let tag = key.to_bits() & 0xFFFF_0000_0000_0000; + let is_string_key = + tag == crate::value::STRING_TAG || tag == crate::value::SHORT_STRING_TAG; + if is_string_key || unsafe { crate::symbol::js_is_symbol(key) } != 0 { + key + } else { + unsafe { crate::object::js_to_property_key(key) } + } + }; + let (target, handler, revoked) = PROXIES.with(|p| { + p.borrow() + .get(id as usize) + .and_then(|o| o.as_ref()) + .map(|e| (e.target, e.handler, e.revoked)) + .unwrap_or(( + f64::from_bits(TAG_UNDEFINED), + f64::from_bits(TAG_UNDEFINED), + false, + )) + }); + if revoked { + return revoked_return(); + } + // Looking up handler.get can itself run a getter and move the heap. + let target_h = scope.root_nanbox_f64(target); + let handler_h = scope.root_nanbox_f64(handler); + let key_h = scope.root_nanbox_f64(key); + let trap = handler_trap(handler_h.get_nanbox_f64(), "get"); + if is_callable(trap) { + let result = call_trap( + handler_h.get_nanbox_f64(), + trap, + &[ + target_h.get_nanbox_f64(), + key_h.get_nanbox_f64(), + receiver.get_nanbox_f64(), + ], + ); + let result_h = scope.root_nanbox_f64(result); + invariants::enforce_get_invariant( + target_h.get_nanbox_f64(), + key_h.get_nanbox_f64(), + result_h.get_nanbox_f64(), + ); + return result_h.get_nanbox_f64(); + } + // No get trap — forward to the target's `[[Get]]`. A proxy target must + // recurse through proxy dispatch rather than ordinary target dispatch, which would deref + // the fake pointer. + let target = target_h.get_nanbox_f64(); + let key = key_h.get_nanbox_f64(); + if lookup(target).is_some() { + return proxy_get_with_receiver(target, key, receiver.get_nanbox_f64()); + } + // `p.apply` / `p.call` / `p.bind` VALUE reads on a callable-wrapping + // proxy resolve to Function.prototype's methods with the PROXY as the + // receiver — reify a bound method so a later invocation dispatches + // `js_native_call_method(proxy, "call", …)` and routes through the + // proxy's [[Call]] (apply trap). Reading off the target instead would + // bypass the trap. (Test262 proxy-toString reads `.apply` as a value; + // Function.prototype.toString on the reified method is the + // NativeFunction form.) + if crate::object::value_is_callable(target) { + if let Some(name) = key_to_rust_string(key) { + let method: Option<&'static [u8]> = match name.as_str() { + "apply" => Some(b"apply"), + "call" => Some(b"call"), + "bind" => Some(b"bind"), + _ => None, + }; + if let Some(m) = method { + // Only when the target has no OWN override of the slot. + let t_ptr = extract_pointer(target.to_bits()) as usize; + if !crate::closure::closure_has_own_dynamic_prop(t_ptr, &name) { + return unsafe { crate::closure::reify_function_method_value(proxy_boxed, m) }; + } + } + } + } + // Ordinary target getters and further prototype hops must keep Receiver. + // Clear/restore the override around the operation; getters and Proxy hops + // consume it before entering user code so nested reads bind independently. + let previous_this = scope.root_nanbox_f64(crate::object::js_implicit_this_set( + receiver.get_nanbox_f64(), + )); + let previous_override = + crate::object::accessor_receiver_override_begin(receiver.get_nanbox_f64()) + .map(|value| scope.root_nanbox_f64(value)); + let result = target_get_property_key(target_h.get_nanbox_f64(), key_h.get_nanbox_f64()); + crate::object::accessor_receiver_override_end( + previous_override.map(|value| value.get_nanbox_f64()), + ); + crate::object::js_implicit_this_set(previous_this.get_nanbox_f64()); + result +} diff --git a/crates/perry-runtime/src/proxy/reflect.rs b/crates/perry-runtime/src/proxy/reflect.rs index b653b195f4..cecf2299cd 100644 --- a/crates/perry-runtime/src/proxy/reflect.rs +++ b/crates/perry-runtime/src/proxy/reflect.rs @@ -10,9 +10,7 @@ use super::{ /// /// - throws `TypeError` for a non-object target, /// - uses `receiver` as the `this` binding for accessor getters, -/// - dispatches proxy `get` traps (forwarding `(target, key)` to the existing -/// proxy path; the three-argument trap receiver is out of scope - Perry's -/// proxy traps are two-argument). +/// - dispatches proxy `get` traps with `(target, key, receiver)`. /// /// `receiver` is the optional third argument; codegen passes `target` when the /// call site omits it (matching the spec default), and `undefined` is treated @@ -30,9 +28,6 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { .root_nanbox_f64(unsafe { crate::object::js_to_property_key(key_handle.get_nanbox_f64()) }); let target = target_handle.get_nanbox_f64(); let property_key = property_key_handle.get_nanbox_f64(); - if lookup(target).is_some() { - return js_proxy_get(target, property_key); - } // Default receiver to target when undefined. let receiver = receiver_handle.get_nanbox_f64(); let recv = if receiver.to_bits() == TAG_UNDEFINED { @@ -40,6 +35,9 @@ pub extern "C" fn js_reflect_get(target: f64, key: f64, receiver: f64) -> f64 { } else { receiver }; + if lookup(target).is_some() { + return super::proxy_get_with_receiver(target, property_key, recv); + } // #2766: if `key` resolves to an accessor *getter* on `target`, rebind its // `this` to the receiver and invoke it - object-literal getters capture // `this` in a reserved closure slot (not `IMPLICIT_THIS`), so plain diff --git a/test-files/test_gap_9785_array_prototype_chain_depth.ts b/test-files/test_gap_9785_array_prototype_chain_depth.ts new file mode 100644 index 0000000000..cf79a18ca1 --- /dev/null +++ b/test-files/test_gap_9785_array_prototype_chain_depth.ts @@ -0,0 +1,74 @@ +// #9785: indexed Get, HasProperty, and strict Set follow every custom array +// prototype link, stop at null, and preserve ordinary-array inheritance. + +function show(label: string, value: unknown): void { + console.log(label, JSON.stringify(value === undefined ? "undefined" : value)); +} + +// ── 1. a middle link that must be consulted ────────────────────────────────── +const mid: any = { 5: "from-mid", 7: "mid-seven" }; +const protoArr: any = []; +protoArr[3] = "from-protoArr"; +Object.setPrototypeOf(protoArr, mid); + +const arr: any = []; +arr[0] = "own-zero"; +Object.setPrototypeOf(arr, protoArr); + +show("depth.own", arr[0]); +show("depth.viaProtoArr", arr[3]); +show("depth.viaMid", arr[5]); // spec: "from-mid" — the skipped link +show("depth.viaMid7", arr[7]); // spec: "mid-seven" +show("depth.absent", arr[9]); +show("depth.hasMid", 5 in arr); +show("depth.hasAbsent", 9 in arr); + +// ── 2. a chain terminated with null must stop ──────────────────────────────── +(Array.prototype as any)[42] = "default-array-proto"; +(Object.prototype as any)[43] = "default-object-proto"; + +const cutProto: any = []; +cutProto[1] = "cut-one"; +Object.setPrototypeOf(cutProto, null); + +const cut: any = []; +Object.setPrototypeOf(cut, cutProto); + +show("cut.viaCutProto", cut[1]); +show("cut.arrayProtoLeak", cut[42]); // spec: undefined +show("cut.objectProtoLeak", cut[43]); // spec: undefined +show("cut.has42", 42 in cut); +show("cut.has43", 43 in cut); + +// A plain array still inherits both, which pins that the leak above is about +// chain termination and not about the indices being absent altogether. +const plain: any = []; +show("plain.arrayProto", plain[42]); +show("plain.objectProto", plain[43]); + +// ── 3. strict [[Set]] must consult the same chain the [[Get]] walks ────────── +// A non-writable inherited index makes a strict assignment throw; the owner +// search has to find it through the SAME depth the read uses. +const roProto: any = {}; +Object.defineProperty(roProto, "6", { value: "readonly", writable: false, enumerable: true }); +const midArr: any = []; +Object.setPrototypeOf(midArr, roProto); +const target: any = []; +Object.setPrototypeOf(target, midArr); + +let threw = "no-throw"; +try { + "use strict"; + const assign = new Function("o", '"use strict"; o[6] = "written";'); + assign(target); +} catch (error) { + threw = (error as Error).constructor.name; +} +show("strictSet.threw", threw); +show("strictSet.value", target[6]); +show("strictSet.own", Object.prototype.hasOwnProperty.call(target, "6")); + +// Clean up so the trailing summary is not polluted for other readers. +delete (Array.prototype as any)[42]; +delete (Object.prototype as any)[43]; +console.log("array-proto-depth-v1:done"); diff --git a/test-files/test_gap_9785_array_prototype_receivers.ts b/test-files/test_gap_9785_array_prototype_receivers.ts new file mode 100644 index 0000000000..0008e03183 --- /dev/null +++ b/test-files/test_gap_9785_array_prototype_receivers.ts @@ -0,0 +1,91 @@ +"use strict"; + +// Multi-hop array prototypes must preserve Receiver and stop at the first own +// property, including undefined values and writable shadows of readonly data. +const reads: string[] = []; +const far: any = []; +const near: any = []; +const receiver: any = ["receiver"]; +Object.defineProperty(far, "2", { + configurable: true, + get() { reads.push(`get:${this === receiver}`); return this[0]; }, +}); +Object.setPrototypeOf(near, far); +Object.setPrototypeOf(receiver, near); +console.log("receiver", receiver[2], Array.prototype.at.call(receiver, 2), reads.join("|")); +receiver.length = 3; +reads.length = 0; +console.log("join", Array.prototype.join.call(receiver, ","), reads.join("|")); + +const readonly: any = {}; +Object.defineProperty(readonly, "4", { value: "readonly", writable: false }); +const shadow: any = []; +shadow[4] = undefined; +Object.setPrototypeOf(shadow, readonly); +const child: any = []; +Object.setPrototypeOf(child, shadow); +console.log("shadow-before", child[4], 4 in child); +child[4] = "written"; +console.log("shadow-after", child[4], Object.hasOwn(child, 4), shadow[4]); + +// A grown prototype is still the same chain node, even when its old allocation +// became a forwarding stub after the child captured it. +const grown: any = []; +const grownChild: any = []; +Object.setPrototypeOf(grownChild, grown); +for (let i = 0; i < 100; i++) grown.push(i); +console.log("grown", grownChild[99], 99 in grownChild, 100 in grownChild); + +// Interleave arrays and ordinary objects before a Proxy to check that its +// traps still observe the original array and run once per internal operation. +const traps: string[] = []; +let original: any; +const proxy = new Proxy({}, { + get(_target, key, recv) { + if (key === "1") { traps.push(`get:${recv === original}`); return "one"; } + return undefined; + }, + has(_target, key) { + if (key === "1" || key === "7") traps.push(`has:${String(key)}`); + return key === "1"; + }, +}); +const objectHop = Object.create(proxy); +const arrayHop: any = []; +Object.setPrototypeOf(arrayHop, objectHop); +original = [0, , 2]; +Object.setPrototypeOf(original, arrayHop); +console.log("proxy-get", original[1], traps.join("|")); +traps.length = 0; +console.log("proxy-has", 1 in original, traps.join("|")); +traps.length = 0; +console.log("proxy-indexOf", Array.prototype.indexOf.call(original, "one"), traps.join("|")); +traps.length = 0; +console.log("proxy-missing", 7 in original, "7" in original, traps.join("|")); + +// An inherited Proxy get trap may itself perform an unrelated inherited read. +// The outer Receiver must not leak into that nested lookup. +const innerProto = { get marker() { return this.name; } }; +const inner = Object.create(innerProto); +inner.name = "inner"; +const nestedProxy = new Proxy({}, { + get(_target, key, recv) { + return `${recv === nested}:${inner.marker}`; + }, +}); +const nested: any = []; +Object.setPrototypeOf(nested, Object.create(nestedProxy)); +console.log("nested-trap", nested[1]); + +const targetGetter = { get 1() { return this.name; } }; +const noTrap = new Proxy(new Proxy(targetGetter, {}), {}); +const getterChild: any = []; +getterChild.name = "array"; +Object.setPrototypeOf(getterChild, noTrap); +console.log("proxy-getter", getterChild[1], Reflect.get(noTrap, "1", { name: "reflect" })); +let reflected: any; +const reflectProxy = new Proxy({}, { + get(_target, _key, recv) { return recv === reflected; }, +}); +reflected = {}; +console.log("reflect-receiver", Reflect.get(reflectProxy, "x", reflected)); diff --git a/test-files/test_gap_9786_array_proxy_prototype.ts b/test-files/test_gap_9786_array_proxy_prototype.ts new file mode 100644 index 0000000000..5e128f95e9 --- /dev/null +++ b/test-files/test_gap_9786_array_proxy_prototype.ts @@ -0,0 +1,60 @@ +// Array fast paths must walk the *actual* full prototype chain and must not +// treat Proxy prototypes as if no custom prototype existed. + +"use strict"; + +const setterLog: string[] = []; +const grand: any = {}; +Object.defineProperty(grand, "3", { + configurable: true, + get() { + return "from-grand"; + }, + set(this: any, value: any) { + setterLog.push(`${this === deep}:${value}`); + }, +}); +const middle: any[] = []; +Object.setPrototypeOf(middle, grand); +const deep: any[] = [0]; +Object.setPrototypeOf(deep, middle); +deep[3] = 17; +console.log(setterLog.join(","), Object.hasOwn(deep, 3), deep[3], deep.length); + +const proxyLog: string[] = []; +let proxied: any[]; +const proxyPrototype = new Proxy( + {}, + { + get(_target, key, receiver) { + if (key === "4") proxyLog.push(`get:${receiver === proxied}`); + return key === "4" ? "proxy-four" : Reflect.get(_target, key, receiver); + }, + has(_target, key) { + if (key === "4") proxyLog.push("has"); + return key === "4" || Reflect.has(_target, key); + }, + set(_target, key, value, receiver) { + proxyLog.push(`set:${String(key)}:${value}:${receiver === proxied}`); + return true; + }, + }, +); +proxied = [1]; +Object.setPrototypeOf(proxied, proxyPrototype); +proxied[4] = 29; +console.log(Object.hasOwn(proxied, 4), proxied[4], 4 in proxied, proxied.length); +console.log(proxyLog.join("|")); + +const holeGrand: any = { 1: "inherited-hole" }; +const holeMiddle: any[] = []; +Object.setPrototypeOf(holeMiddle, holeGrand); +const holey: any[] = [0, , 2]; +Object.setPrototypeOf(holey, holeMiddle); +const seen: string[] = []; +Array.prototype.forEach.call(holey, (v: any, i: number) => seen.push(`${i}:${v}`)); +console.log( + Array.prototype.join.call(holey, ","), + Array.prototype.indexOf.call(holey, "inherited-hole"), + seen.join("|"), +);