diff --git a/changelog.d/9220-array-prototype-index-paths.md b/changelog.d/9220-array-prototype-index-paths.md deleted file mode 100644 index 9021e363c6..0000000000 --- a/changelog.d/9220-array-prototype-index-paths.md +++ /dev/null @@ -1,25 +0,0 @@ -### fix(runtime): honor inherited array indices in writes and borrowed methods - -Indexed assignment on an array with a recorded custom prototype now performs -the inherited descriptor walk before creating an own element. Prototype -setters therefore run with the array as their receiver, inherited non-writable -data properties reject the assignment, and inherited writable data properties -still allow the normal own-property creation. - -The generic array-like engine used by `Array.prototype..call(array)` -now uses the same recorded-prototype classification for `Get` and -`HasProperty`. Prototype-filled holes are consequently visible to `join`, -`indexOf`, `map`, `forEach`, and the other generic methods. Default-chain -arrays retain their existing fast paths, while Proxy prototypes keep their -dedicated trap handling. Fixes #9220 and #9221. - -The `[[Set]]` owner walk takes the same chain hops the `[[Get]]` walk takes: -`Object.create(p)` models its link with a synthetic class id rather than a -recorded prototype (#809), so without that hop an inherited accessor two links -up was still silently replaced by an own element. - -Every one of the three new gates leads with the existing `array_static_proto_recorded` -process latch, so a program that never retargets an array keeps the previous -code path exactly — the strict store's number lane does not even read the slot -it is about to write, and the cold store tail performs no prototype-registry -probe. diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 5c3d47cc7b..0865d5ea47 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -430,13 +430,6 @@ pub(super) fn al_get(recv: f64, k: i64) -> f64 { if k < 0 { return undef(); } - // #9221: explicit Array.prototype..call(array, ...) must use - // the same recorded-prototype Get as a direct `array[k]`. Default-chain - // arrays retain the old `js_array_get_f64` lane, and Proxy prototypes - // remain on their dedicated path (the classification returns None). - if real_array_uses_recorded_spec_path(arr) { - return crate::array::array_spec_get(arr, k as u32); - } return js_array_get_f64(arr, k as u32); } let b = recv.to_bits(); @@ -542,17 +535,6 @@ fn object_get_property_chain(obj_ptr: usize, k: i64) -> f64 { undef() } -/// Whether a genuine Array receiver must use the #9219 recorded-prototype -/// classification for indexed Get/HasProperty. The process latch keeps the -/// per-array side-table probe out of programs that never retarget an array; -/// the classification itself excludes Proxy prototypes so their existing -/// dedicated trap path is not invoked twice. -#[inline] -fn real_array_uses_recorded_spec_path(arr: *const ArrayHeader) -> bool { - crate::object::prototype_chain::array_static_proto_recorded() - && unsafe { crate::array::array_custom_prototype(arr).is_some() } -} - /// `HasProperty(ToObject(recv), k)`. pub(super) fn al_has(recv: f64, k: i64) -> bool { if k < 0 { @@ -566,17 +548,8 @@ pub(super) fn al_has(recv: f64, k: i64) -> bool { } let el = *((arr as *const u8).add(std::mem::size_of::()) as *const f64) .add(k as usize); - if el.to_bits() != TAG_HOLE { - return true; - } + return el.to_bits() != TAG_HOLE; } - // #9221: a hole is absent only from the receiver. On a retargeted - // array, HasProperty must walk the recorded chain; this is exactly the - // `ArrayCustomProto::{Null, Array, Other}` policy used by direct reads. - if real_array_uses_recorded_spec_path(arr) { - return crate::array::array_spec_has_index(arr, k as u32); - } - return false; } let b = recv.to_bits(); if is_string_value(b) { diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index acec2003d0..2e6a8590e1 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -6,15 +6,10 @@ use std::sync::atomic::Ordering; #[path = "indexing_keyed.rs"] mod keyed; -#[path = "indexing_proto_chain.rs"] -mod proto_chain; pub use keyed::{ js_array_get_index_or_string, js_array_set_index_or_string, js_array_set_index_or_string_strict, js_array_set_string_key, }; -use proto_chain::array_oob_prototype_get; -pub(crate) use proto_chain::{array_custom_prototype, array_spec_get, array_spec_has_index}; -use proto_chain::{array_object_proto_index_owner, ArrayCustomProto}; const MAX_DENSE_ARRAY_GROW_LENGTH: u32 = 1_000_000; @@ -84,6 +79,58 @@ pub(crate) fn note_array_index_write(arr: usize) { } } +/// Out-of-bounds element read fallback: `Array.prototype[index]` when the +/// prototype has indexed properties (see `ARRAY_PROTO_HAS_INDEX`). Returns the +/// inherited value, or `undefined` if absent. Skipped entirely when the +/// receiver IS `Array.prototype` (avoids self-recursion) or the flag is unset. +/// +/// #6981: the `proto != receiver` self-recursion guard is an OBJECT IDENTITY +/// test, so both sides must be forwarding-resolved. `js_array_get_f64` resolves +/// its receiver through `clean_arr_ptr`; the prototype address comes from a +/// memoized cache, so it is healed here too. Comparing a stale address against +/// a resolved one makes the guard silently stop firing and +/// `js_array_get_f64` ⇄ this function recurse without bound. +#[inline] +unsafe fn array_oob_prototype_get(receiver: usize, index: u32) -> f64 { + const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); + // A custom [[Prototype]] (`Object.setPrototypeOf(arr, p)`) replaces the + // default chain — gated on a global relaxed flag. #9192: `p` need not be an + // array; a plain object / `Object.create(Array.prototype)` result answers + // the whole lookup through the generic resolver. + if crate::object::prototype_chain::array_static_proto_recorded() { + let arr = receiver as *const ArrayHeader; + 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) + } + 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); + } + } + None => {} + } + } + if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { + let proto = array_prototype_addr(); + if proto != 0 && proto != crate::value::resolve_forwarding(receiver) { + let proto_arr = proto as *const ArrayHeader; + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return js_array_get_f64(proto_arr, index); + } + } + } + // Object.prototype indexed property (data or defineProperty accessor): + // arr → Array.prototype → Object.prototype (concat/S15.4.4.4_A3_T3). + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return crate::array::sort_object_prototype_index_get(index); + } + TAG_UNDEFINED_F64 +} + #[inline] unsafe fn array_sparse_index_property_get(arr: *const ArrayHeader, index: u32) -> Option { let arr = clean_arr_ptr(arr); @@ -181,6 +228,406 @@ pub(crate) unsafe fn array_has_own_index(arr: *const ArrayHeader, index: u32) -> false } +/// Spec `[[HasProperty]]`(O, ToString(index)) for an ordinary Array receiver: +/// own property OR inherited indexed property from `Array.prototype`. +pub(crate) fn array_spec_has_index(arr: *const ArrayHeader, index: u32) -> bool { + let arr = clean_arr_ptr(arr); + if arr.is_null() { + return false; + } + unsafe { + if array_has_own_index(arr, index) { + 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. + 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; + } + } + None => {} + } + if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { + let proto = array_prototype_addr(); + if proto != 0 && proto != arr as usize { + let proto_arr = proto as *const ArrayHeader; + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return true; + } + } + } + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return true; + } + false + } +} + +/// How a recorded custom `[[Prototype]]` on a real array must be consulted. +/// +/// #9192: before this classification the array index paths accepted a recorded +/// prototype ONLY when it was itself a `GC_TYPE_ARRAY`; every other shape (a +/// plain object, an `Object.create(Array.prototype)` result, a class prototype) +/// was recorded — latching the process-wide index deopt — and then silently +/// ignored, so the array inherited nothing at all. +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-*). + 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 + /// further hops (`Object.create(Array.prototype)`, proxies) are walked. + Other(u64), +} + +/// Classify the `[[Prototype]]` an explicit `Object.setPrototypeOf` / +/// `__proto__` / `Reflect.setPrototypeOf` recorded for `arr`. `None` when the +/// array still carries the default `Array.prototype` chain. +pub(crate) unsafe fn array_custom_prototype(arr: *const ArrayHeader) -> Option { + let bits = crate::object::prototype_chain::object_static_prototype(arr as usize)?; + if bits == crate::value::TAG_NULL { + return Some(ArrayCustomProto::Null); + } + if let Some(proto_arr) = array_custom_array_prototype_from_bits(arr, bits) { + return Some(ArrayCustomProto::Array(proto_arr)); + } + // A Proxy prototype keeps its existing dedicated handling in the `in` / + // property-get arms; routing it through the generic resolver here as well + // would invoke the `has` trap twice, which is observable. + if crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 { + return None; + } + // A pointer-shaped record that is not a real array is the #9192 case. A + // record that is not pointer-shaped at all (a stale/garbage entry) is + // reported as "no custom prototype", exactly as before. + pointer_bits_of_recorded_prototype(bits).map(|_| ArrayCustomProto::Other(bits)) +} + +/// The heap address a recorded prototype's bits name, if they are pointer +/// shaped at all. The record may be NaN-boxed (0x7FFD) or a RAW untagged +/// pointer (module-level arrays are stored as raw I64s). +fn pointer_bits_of_recorded_prototype(bits: u64) -> Option { + let raw = if (bits >> 48) == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else if (bits >> 48) == 0 && bits > 0x10000 { + bits as usize + } else { + return None; + }; + if raw == 0 { + None + } else { + Some(raw) + } +} + +/// A custom `[[Prototype]]` installed on `arr` via `Object.setPrototypeOf` +/// that happens to be a real array — `None` for every other recorded shape +/// (which [`array_custom_prototype`] reports as [`ArrayCustomProto::Other`]). +unsafe fn array_custom_array_prototype_from_bits( + arr: *const ArrayHeader, + bits: u64, +) -> Option<*const ArrayHeader> { + let raw = pointer_bits_of_recorded_prototype(bits)?; + if raw < crate::gc::GC_HEADER_SIZE + 0x1000 || raw == arr as usize { + return None; + } + // A Proxy prototype is a small registered id, not a heap allocation — the + // GC-header read below would deref a fake pointer. + if crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 { + return None; + } + // #5625: the recorded prototype may be a *grown* array whose stored pointer + // was left FORWARDED by `js_array_grow` — its first 8 bytes now hold the + // forwarding pointer to the live head instead of length+capacity. (A real + // array grows when `Object.setPrototypeOf(arr, p)` captured `p` before a + // later push reallocated it, or the proto itself was built by appends — as + // in test262 copyWithin/coerced-values-start-change-start, whose + // `longDenseArray()` fills a `[0]` to 1024 elements.) Resolve the chain so + // we deref the current array head; reading the defunct old location yields + // the forwarding pointer's low 32 bits as a garbage `length`, making + // inherited-index reads silently miss (nondeterministic copyWithin output). + let resolved = clean_arr_ptr(raw as *const ArrayHeader); + if resolved.is_null() || resolved as usize == arr as usize { + return None; + } + let hdr = (resolved as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY { + Some(resolved) + } else { + None + } +} + +/// #9192: `[[Get]]`(index) through a NON-array custom `[[Prototype]]`, binding +/// `arr` as the receiver so an inherited index accessor sees the array as +/// `this`. `None` when the whole (replaced) chain lacks the index. +/// +/// Everything here allocates — `index.to_string()` interns a key, and the +/// resolver can run a user getter — so the array and the prototype are rooted +/// and re-read across the call. +unsafe fn array_object_proto_index_get( + arr: *const ArrayHeader, + proto_bits: u64, + index: u32, +) -> 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; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(arr as i64)); + 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); + if key_hdr.is_null() { + return None; + } + let key_handle = scope.root_nanbox_f64(crate::value::nanbox_string_key(key_hdr)); + let receiver_addr = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize; + let key_ptr = crate::value::js_nanbox_get_pointer(key_handle.get_nanbox_f64()) + as *const crate::StringHeader; + crate::object::prototype_chain::resolve_inherited_field_from_prototype( + receiver_addr, + proto.get_heap_word_u64(), + key_ptr, + ) + .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. +unsafe fn array_object_proto_index_owner(proto_bits: u64, key: &str) -> usize { + let mut bits = proto_bits; + for _ in 0..64 { + if bits == crate::value::TAG_NULL { + return 0; + } + if 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) + || !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; + } + match crate::object::prototype_chain::object_static_prototype(addr) { + Some(next) => bits = next, + None => return 0, + } + } + 0 +} + +/// #9192: `[[HasProperty]]`(index) through a NON-array custom `[[Prototype]]`. +unsafe fn array_object_proto_index_has(proto_bits: u64, index: u32) -> bool { + let scope = crate::gc::RuntimeHandleScope::new(); + 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); + if key_hdr.is_null() { + return false; + } + let key_handle = scope.root_nanbox_f64(crate::value::nanbox_string_key(key_hdr)); + let key_ptr = crate::value::js_nanbox_get_pointer(key_handle.get_nanbox_f64()) + as *const crate::StringHeader; + crate::object::prototype_value_has_property(proto.get_heap_word_u64(), key_ptr) +} + +/// Spec `[[Get]]`(O, ToString(index)) for an ordinary Array receiver: own value +/// (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 { + 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); + } + // #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) + } + 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()); + } + } + None => {} + } + if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { + let proto = array_prototype_addr(); + if proto != 0 && proto != arr as usize { + let proto_arr = proto as *const ArrayHeader; + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return array_inherited_index_get(proto_arr, index, receiver.get_nanbox_f64()); + } + } + } + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return crate::array::sort_object_prototype_index_get_with_receiver( + index, + receiver.get_nanbox_f64(), + ); + } + TAG_UNDEFINED_F64 + } +} + +/// Spec `Set(O, ToString(index), value, true)` for an Array receiver. Unlike +/// the internal dense setter, this observes an inherited indexed accessor +/// before creating an own element. Array mutators use it on their exotic path +/// because a prototype setter may mutate the receiver (including freezing it +/// or making `length` non-writable) before the mutator's final length Set. +pub(crate) fn array_spec_set(arr: *mut ArrayHeader, index: u32, value: f64) -> *mut ArrayHeader { + let arr = clean_arr_ptr_mut(arr); + if arr.is_null() { + return arr; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + let value_handle = scope.root_nanbox_f64(value); + let receiver = + || crate::value::js_nanbox_pointer(arr_handle.get_raw_mut_ptr::() as i64); + let key = index.to_string(); + + unsafe { + if array_has_own_index(arr_handle.get_raw_mut_ptr::(), index) { + return js_array_set_f64_extend_strict( + arr_handle.get_raw_mut_ptr::(), + index, + value_handle.get_nanbox_f64(), + ); + } + + // #9192: a non-array custom `[[Prototype]]` owns the whole answer — its + // chain supplies the inherited accessor / non-writable attributes, and + // the implicit `Array.prototype` / `Object.prototype` tail below must + // not run. An explicit null prototype inherits nothing at all. + let mut default_chain = true; + let mut inherited_owner = 0usize; + match array_custom_prototype(arr_handle.get_raw_mut_ptr::()) { + Some(ArrayCustomProto::Null) => default_chain = false, + Some(ArrayCustomProto::Other(bits)) => { + default_chain = false; + 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; + } + } + None => {} + } + if inherited_owner == 0 && default_chain { + let proto = array_prototype_addr(); + inherited_owner = if proto != 0 + && proto != arr_handle.get_raw_mut_ptr::() as usize + && array_has_own_index(proto as *const ArrayHeader, index) + { + proto + } else if object_prototype_has_index_flag() + && crate::array::object_prototype_has_index_prop(index) + { + object_prototype_addr() + } else { + 0 + }; + } + + if inherited_owner != 0 { + if let Some(accessor) = crate::object::get_accessor_descriptor(inherited_owner, &key) { + if accessor.set == 0 { + crate::collection_iter::throw_type_error(&format!( + "Cannot set property {index} which has only a getter" + )); + } + crate::object::invoke_accessor_setter( + accessor.set, + receiver(), + value_handle.get_nanbox_f64(), + ); + return arr_handle.get_raw_mut_ptr::(); + } + if crate::object::get_property_attrs(inherited_owner, &key) + .is_some_and(|attrs| !attrs.writable()) + { + throw_frozen_array_index_write(index); + } + } + + js_array_set_f64_extend_strict( + arr_handle.get_raw_mut_ptr::(), + index, + value_handle.get_nanbox_f64(), + ) + } +} + +/// Read an own indexed property from an Array prototype while preserving the +/// original receiver for an inherited accessor's `this` value. +unsafe fn array_inherited_index_get( + proto_arr: *const ArrayHeader, + index: u32, + receiver: f64, +) -> f64 { + if array_object_flags(proto_arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { + if let Some(acc) = + crate::object::get_accessor_descriptor(proto_arr as usize, &index.to_string()) + { + if acc.get != 0 { + return f64::from_bits( + crate::object::invoke_accessor_getter(acc.get, receiver).bits(), + ); + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + } + js_array_get_f64(proto_arr, index) +} + pub(crate) fn array_get_property_by_key( arr: *const ArrayHeader, key: *const crate::StringHeader, @@ -1055,29 +1502,12 @@ pub(crate) unsafe fn try_strict_dense_number_store( } else { value_bits }; - let slot = super::header::array_elements_ptr(arr).add(index as usize); - // #9220: this lane used to fill a hole directly. A hole is not an own - // property, so an inherited setter / non-writable data descriptor must be - // consulted before an own element can be created. A raw-f64 DENSE layout - // proves there are no holes and keeps its bit-for-bit old hot path; every - // other admitted layout proves ownership with the slot Perry is about to - // overwrite. - // The process latch leads: an array can only have an inherited index when - // SOME array has been retargeted, so a program that never calls - // `Object.setPrototypeOf` on an array keeps this lane bit-for-bit as it was - // (one relaxed load of a static bool, and the slot is never read here). - // `new Array(n)` fills are holey and would otherwise all fall off the lane. - let may_have_holes = flags & crate::gc::GC_ARRAY_RAW_F64_LAYOUT == 0 - || flags & crate::gc::GC_ARRAY_RAW_F64_HOLES != 0; - if crate::object::prototype_chain::array_static_proto_recorded() - && may_have_holes - && ptr::read(slot) == crate::value::TAG_HOLE - { - return None; - } // GC_STORE_AUDIT(POINTER_FREE): a number never holds a heap pointer, and // the receiver's layout was proved pointer-free or tag-scanned above. - ptr::write(slot, store_bits); + ptr::write( + super::header::array_elements_ptr(arr).add(index as usize), + store_bits, + ); Some(arr) } @@ -1184,20 +1614,6 @@ pub extern "C" fn js_array_set_f64_extend_strict( arr: *mut ArrayHeader, index: u32, value: f64, -) -> *mut ArrayHeader { - js_array_set_f64_extend_strict_impl(arr, index, value, false) -} - -/// Strict indexed assignment after optionally completing the inherited -/// descriptor walk. `prototype_already_checked` is true only for the callback -/// from [`array_spec_set`]; it prevents a writable inherited data property -/// from recursing when the spec walk proceeds to create the receiver's own -/// element. -fn js_array_set_f64_extend_strict_impl( - arr: *mut ArrayHeader, - index: u32, - value: f64, - prototype_already_checked: bool, ) -> *mut ArrayHeader { // Two exact fast lanes, each storing only what the general path below // would store and declining every shape it cannot prove. The plain-number @@ -1228,25 +1644,6 @@ fn js_array_set_f64_extend_strict_impl( return js_array_set_f64_extend(arr, index, value); } - // #9220: only a retargeted array with no own index pays the inherited - // [[Set]] walk. `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 - // 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. - // The process latch leads for the same reason it does in the read/HasProperty - // twin (`generic::real_array_uses_recorded_spec_path`): recording a - // prototype on ANY array sets it, so a clear latch proves this array cannot - // have one and the side-table probe is skipped entirely. - if !prototype_already_checked - && crate::object::prototype_chain::array_static_proto_recorded() - && unsafe { array_custom_prototype(clean).is_some() } - && unsafe { !array_has_own_index(clean, index) } - { - return array_spec_set(clean, index, value); - } - // SAFETY: the clean above resolved this exact live plain-array head. The // guard performs no Perry allocation/safepoint, so the proof remains live // for the store core. @@ -1599,101 +1996,3 @@ unsafe fn js_array_set_f64_extend_resolved( arr } } - -// `array_spec_set` stays in this module deliberately: it carries the seven -// pre-existing `get_raw_mut_ptr::()` sites this file's -// raw-handle ceiling already covers. Moving it to a new module would read -// to `raw_handle_debt.py --no-raise-vs` as debt appearing in a module that -// did not exist at the merge base, which is a raise it refuses by design -// (#7659) even though the repository total is unchanged. -/// Spec `Set(O, ToString(index), value, true)` for an Array receiver. Unlike -/// the internal dense setter, this observes an inherited indexed accessor -/// before creating an own element. Array mutators use it on their exotic path -/// because a prototype setter may mutate the receiver (including freezing it -/// or making `length` non-writable) before the mutator's final length Set. -pub(crate) fn array_spec_set(arr: *mut ArrayHeader, index: u32, value: f64) -> *mut ArrayHeader { - let arr = clean_arr_ptr_mut(arr); - if arr.is_null() { - return arr; - } - let scope = crate::gc::RuntimeHandleScope::new(); - let arr_handle = scope.root_raw_mut_ptr(arr); - let value_handle = scope.root_nanbox_f64(value); - let receiver = - || crate::value::js_nanbox_pointer(arr_handle.get_raw_mut_ptr::() as i64); - let key = index.to_string(); - - unsafe { - if array_has_own_index(arr_handle.get_raw_mut_ptr::(), index) { - return js_array_set_f64_extend_strict_impl( - arr_handle.get_raw_mut_ptr::(), - index, - value_handle.get_nanbox_f64(), - true, - ); - } - - // #9192: a non-array custom `[[Prototype]]` owns the whole answer — its - // chain supplies the inherited accessor / non-writable attributes, and - // the implicit `Array.prototype` / `Object.prototype` tail below must - // not run. An explicit null prototype inherits nothing at all. - let mut default_chain = true; - let mut inherited_owner = 0usize; - match array_custom_prototype(arr_handle.get_raw_mut_ptr::()) { - Some(ArrayCustomProto::Null) => default_chain = false, - Some(ArrayCustomProto::Other(bits)) => { - default_chain = false; - 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; - } - } - None => {} - } - if inherited_owner == 0 && default_chain { - let proto = array_prototype_addr(); - inherited_owner = if proto != 0 - && proto != arr_handle.get_raw_mut_ptr::() as usize - && array_has_own_index(proto as *const ArrayHeader, index) - { - proto - } else if object_prototype_has_index_flag() - && crate::array::object_prototype_has_index_prop(index) - { - object_prototype_addr() - } else { - 0 - }; - } - - if inherited_owner != 0 { - if let Some(accessor) = crate::object::get_accessor_descriptor(inherited_owner, &key) { - if accessor.set == 0 { - crate::collection_iter::throw_type_error(&format!( - "Cannot set property {index} which has only a getter" - )); - } - crate::object::invoke_accessor_setter( - accessor.set, - receiver(), - value_handle.get_nanbox_f64(), - ); - return arr_handle.get_raw_mut_ptr::(); - } - if crate::object::get_property_attrs(inherited_owner, &key) - .is_some_and(|attrs| !attrs.writable()) - { - throw_frozen_array_index_write(index); - } - } - - js_array_set_f64_extend_strict_impl( - arr_handle.get_raw_mut_ptr::(), - index, - value_handle.get_nanbox_f64(), - true, - ) - } -} diff --git a/crates/perry-runtime/src/array/indexing_proto_chain.rs b/crates/perry-runtime/src/array/indexing_proto_chain.rs deleted file mode 100644 index 1f612c8774..0000000000 --- a/crates/perry-runtime/src/array/indexing_proto_chain.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! Spec-level indexed `[[Get]]` / `[[HasProperty]]` / `[[Set]]` for an Array -//! receiver, including the recorded custom `[[Prototype]]` classification -//! (#9192/#9219) and the inherited-descriptor walk an indexed assignment must -//! perform before it may create an own element (#9220/#9221). -//! -//! Split out of `indexing.rs` to keep that file under the repo's 2000-line cap; -//! a pure move. Declared as a CHILD of `indexing`, so parent-private helpers -//! (`clean_arr_ptr`, the prototype-index latches, the strict store entry) stay -//! reachable through `super::*` without widening any visibility. -use super::*; -use std::sync::atomic::Ordering; - -/// Out-of-bounds element read fallback: `Array.prototype[index]` when the -/// prototype has indexed properties (see `ARRAY_PROTO_HAS_INDEX`). Returns the -/// inherited value, or `undefined` if absent. Skipped entirely when the -/// receiver IS `Array.prototype` (avoids self-recursion) or the flag is unset. -/// -/// #6981: the `proto != receiver` self-recursion guard is an OBJECT IDENTITY -/// test, so both sides must be forwarding-resolved. `js_array_get_f64` resolves -/// its receiver through `clean_arr_ptr`; the prototype address comes from a -/// memoized cache, so it is healed here too. Comparing a stale address against -/// a resolved one makes the guard silently stop firing and -/// `js_array_get_f64` ⇄ this function recurse without bound. -#[inline] -pub(super) unsafe fn array_oob_prototype_get(receiver: usize, index: u32) -> f64 { - const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); - // A custom [[Prototype]] (`Object.setPrototypeOf(arr, p)`) replaces the - // default chain — gated on a global relaxed flag. #9192: `p` need not be an - // array; a plain object / `Object.create(Array.prototype)` result answers - // the whole lookup through the generic resolver. - if crate::object::prototype_chain::array_static_proto_recorded() { - let arr = receiver as *const ArrayHeader; - 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) - } - 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); - } - } - None => {} - } - } - if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { - let proto = array_prototype_addr(); - if proto != 0 && proto != crate::value::resolve_forwarding(receiver) { - let proto_arr = proto as *const ArrayHeader; - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return js_array_get_f64(proto_arr, index); - } - } - } - // Object.prototype indexed property (data or defineProperty accessor): - // arr → Array.prototype → Object.prototype (concat/S15.4.4.4_A3_T3). - if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) - && crate::array::object_prototype_has_index_prop(index) - { - return crate::array::sort_object_prototype_index_get(index); - } - TAG_UNDEFINED_F64 -} - -/// Spec `[[HasProperty]]`(O, ToString(index)) for an ordinary Array receiver: -/// own property OR inherited indexed property from `Array.prototype`. -pub(crate) fn array_spec_has_index(arr: *const ArrayHeader, index: u32) -> bool { - let arr = clean_arr_ptr(arr); - if arr.is_null() { - return false; - } - unsafe { - if array_has_own_index(arr, index) { - 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. - 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; - } - } - None => {} - } - if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { - let proto = array_prototype_addr(); - if proto != 0 && proto != arr as usize { - let proto_arr = proto as *const ArrayHeader; - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return true; - } - } - } - if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) - && crate::array::object_prototype_has_index_prop(index) - { - return true; - } - false - } -} - -/// How a recorded custom `[[Prototype]]` on a real array must be consulted. -/// -/// #9192: before this classification the array index paths accepted a recorded -/// prototype ONLY when it was itself a `GC_TYPE_ARRAY`; every other shape (a -/// plain object, an `Object.create(Array.prototype)` result, a class prototype) -/// was recorded — latching the process-wide index deopt — and then silently -/// ignored, so the array inherited nothing at all. -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-*). - 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 - /// further hops (`Object.create(Array.prototype)`, proxies) are walked. - Other(u64), -} - -/// Classify the `[[Prototype]]` an explicit `Object.setPrototypeOf` / -/// `__proto__` / `Reflect.setPrototypeOf` recorded for `arr`. `None` when the -/// array still carries the default `Array.prototype` chain. -pub(crate) unsafe fn array_custom_prototype(arr: *const ArrayHeader) -> Option { - let bits = crate::object::prototype_chain::object_static_prototype(arr as usize)?; - if bits == crate::value::TAG_NULL { - return Some(ArrayCustomProto::Null); - } - if let Some(proto_arr) = array_custom_array_prototype_from_bits(arr, bits) { - return Some(ArrayCustomProto::Array(proto_arr)); - } - // A Proxy prototype keeps its existing dedicated handling in the `in` / - // property-get arms; routing it through the generic resolver here as well - // would invoke the `has` trap twice, which is observable. - if crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 { - return None; - } - // A pointer-shaped record that is not a real array is the #9192 case. A - // record that is not pointer-shaped at all (a stale/garbage entry) is - // reported as "no custom prototype", exactly as before. - pointer_bits_of_recorded_prototype(bits).map(|_| ArrayCustomProto::Other(bits)) -} - -/// The heap address a recorded prototype's bits name, if they are pointer -/// shaped at all. The record may be NaN-boxed (0x7FFD) or a RAW untagged -/// pointer (module-level arrays are stored as raw I64s). -fn pointer_bits_of_recorded_prototype(bits: u64) -> Option { - let raw = if (bits >> 48) == 0x7FFD { - (bits & crate::value::POINTER_MASK) as usize - } else if (bits >> 48) == 0 && bits > 0x10000 { - bits as usize - } else { - return None; - }; - if raw == 0 { - None - } else { - Some(raw) - } -} - -/// A custom `[[Prototype]]` installed on `arr` via `Object.setPrototypeOf` -/// that happens to be a real array — `None` for every other recorded shape -/// (which [`array_custom_prototype`] reports as [`ArrayCustomProto::Other`]). -unsafe fn array_custom_array_prototype_from_bits( - arr: *const ArrayHeader, - bits: u64, -) -> Option<*const ArrayHeader> { - let raw = pointer_bits_of_recorded_prototype(bits)?; - if raw < crate::gc::GC_HEADER_SIZE + 0x1000 || raw == arr as usize { - return None; - } - // A Proxy prototype is a small registered id, not a heap allocation — the - // GC-header read below would deref a fake pointer. - if crate::proxy::js_proxy_is_proxy(f64::from_bits(bits)) != 0 { - return None; - } - // #5625: the recorded prototype may be a *grown* array whose stored pointer - // was left FORWARDED by `js_array_grow` — its first 8 bytes now hold the - // forwarding pointer to the live head instead of length+capacity. (A real - // array grows when `Object.setPrototypeOf(arr, p)` captured `p` before a - // later push reallocated it, or the proto itself was built by appends — as - // in test262 copyWithin/coerced-values-start-change-start, whose - // `longDenseArray()` fills a `[0]` to 1024 elements.) Resolve the chain so - // we deref the current array head; reading the defunct old location yields - // the forwarding pointer's low 32 bits as a garbage `length`, making - // inherited-index reads silently miss (nondeterministic copyWithin output). - let resolved = clean_arr_ptr(raw as *const ArrayHeader); - if resolved.is_null() || resolved as usize == arr as usize { - return None; - } - let hdr = (resolved as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY { - Some(resolved) - } else { - None - } -} - -/// #9192: `[[Get]]`(index) through a NON-array custom `[[Prototype]]`, binding -/// `arr` as the receiver so an inherited index accessor sees the array as -/// `this`. `None` when the whole (replaced) chain lacks the index. -/// -/// Everything here allocates — `index.to_string()` interns a key, and the -/// resolver can run a user getter — so the array and the prototype are rooted -/// and re-read across the call. -unsafe fn array_object_proto_index_get( - arr: *const ArrayHeader, - proto_bits: u64, - index: u32, -) -> 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; - } - let scope = crate::gc::RuntimeHandleScope::new(); - let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(arr as i64)); - 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); - if key_hdr.is_null() { - return None; - } - let key_handle = scope.root_nanbox_f64(crate::value::nanbox_string_key(key_hdr)); - let receiver_addr = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as usize; - let key_ptr = crate::value::js_nanbox_get_pointer(key_handle.get_nanbox_f64()) - as *const crate::StringHeader; - crate::object::prototype_chain::resolve_inherited_field_from_prototype( - receiver_addr, - proto.get_heap_word_u64(), - key_ptr, - ) - .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. -pub(crate) unsafe fn array_object_proto_index_owner(proto_bits: u64, key: &str) -> usize { - let mut bits = proto_bits; - for _ in 0..64 { - if bits == crate::value::TAG_NULL { - return 0; - } - if 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) - || !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; - } - 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(); - } - } - } - 0 -} - -/// #9192: `[[HasProperty]]`(index) through a NON-array custom `[[Prototype]]`. -unsafe fn array_object_proto_index_has(proto_bits: u64, index: u32) -> bool { - let scope = crate::gc::RuntimeHandleScope::new(); - 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); - if key_hdr.is_null() { - return false; - } - let key_handle = scope.root_nanbox_f64(crate::value::nanbox_string_key(key_hdr)); - let key_ptr = crate::value::js_nanbox_get_pointer(key_handle.get_nanbox_f64()) - as *const crate::StringHeader; - crate::object::prototype_value_has_property(proto.get_heap_word_u64(), key_ptr) -} - -/// Spec `[[Get]]`(O, ToString(index)) for an ordinary Array receiver: own value -/// (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 { - 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); - } - // #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) - } - 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()); - } - } - None => {} - } - if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { - let proto = array_prototype_addr(); - if proto != 0 && proto != arr as usize { - let proto_arr = proto as *const ArrayHeader; - if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { - return array_inherited_index_get(proto_arr, index, receiver.get_nanbox_f64()); - } - } - } - if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) - && crate::array::object_prototype_has_index_prop(index) - { - return crate::array::sort_object_prototype_index_get_with_receiver( - index, - receiver.get_nanbox_f64(), - ); - } - TAG_UNDEFINED_F64 - } -} - -/// Read an own indexed property from an Array prototype while preserving the -/// original receiver for an inherited accessor's `this` value. -unsafe fn array_inherited_index_get( - proto_arr: *const ArrayHeader, - index: u32, - receiver: f64, -) -> f64 { - if array_object_flags(proto_arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { - if let Some(acc) = - crate::object::get_accessor_descriptor(proto_arr as usize, &index.to_string()) - { - if acc.get != 0 { - return f64::from_bits( - crate::object::invoke_accessor_getter(acc.get, receiver).bits(), - ); - } - return f64::from_bits(crate::value::TAG_UNDEFINED); - } - } - js_array_get_f64(proto_arr, index) -} diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 74285f8582..af030d5535 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -138,9 +138,8 @@ pub use self::immutable::{ js_array_with, js_arraylike_copy_within, }; pub(crate) use self::indexing::{ - array_custom_prototype, array_has_own_index, array_iteration_is_exotic, - array_iteration_is_exotic_resolved, array_prototype_has_index_flag, array_spec_get, - array_spec_has_index, array_spec_set, + array_has_own_index, array_iteration_is_exotic, array_iteration_is_exotic_resolved, + array_prototype_has_index_flag, array_spec_get, array_spec_has_index, array_spec_set, }; pub use self::indexing::{ js_array_get_element, js_array_get_element_f64, js_array_get_f64, js_array_get_f64_unchecked, diff --git a/crates/perry-runtime/src/array/strict_store_tests.rs b/crates/perry-runtime/src/array/strict_store_tests.rs index 3f3f3c092a..3b3a0efca6 100644 --- a/crates/perry-runtime/src/array/strict_store_tests.rs +++ b/crates/perry-runtime/src/array/strict_store_tests.rs @@ -127,29 +127,5 @@ fn strict_dense_number_store_fast_lane_matches_the_general_path() { let out = js_array_set_f64_extend_strict(boxed, 3, 3.0); assert_eq!((*out).length, 4); assert_eq!(js_array_get_f64(out, 3), 3.0); - - // #9220: an in-bounds hole is not an own property. The number lane - // must decline it so the strict entry can consult an inherited index - // setter / non-writable data descriptor before creating an element — - // but ONLY once some array has been retargeted. With the process latch - // clear (the overwhelmingly common case, including every `new Array(n)` - // fill) the lane keeps filling holes exactly as it did before #9220. - // - // Indices 4 and 5 are the SAME shape — two in-bounds holes on one - // array — so the latch is the only variable between the two arms. - js_array_set_length(out, 6.0); - assert!(!array_has_own_index(out, 4)); - assert!(!array_has_own_index(out, 5)); - let latch_was = - crate::object::prototype_chain::test_swap_array_static_proto_recorded(false); - assert!( - lane(out, 4, 8.0), - "no recorded array prototype: the hole fill stays on the fast lane" - ); - assert!(array_has_own_index(out, 4)); - crate::object::prototype_chain::test_swap_array_static_proto_recorded(true); - assert!(!lane(out, 5, 8.0), "hole slot requires the [[Set]] walk"); - assert!(!array_has_own_index(out, 5)); - crate::object::prototype_chain::test_swap_array_static_proto_recorded(latch_was); } } diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 97e74a4c09..b5a649c5f2 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -2598,14 +2598,7 @@ pub extern "C" fn js_typed_feedback_array_index_set_fallback_boxed( (raw_addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; match (*gc_header).obj_type { crate::gc::GC_TYPE_ARRAY | crate::gc::GC_TYPE_LAZY_ARRAY => { - // #9220: this is the cold continuation of a source-level - // `arr[index] = value` after the inline guard rejects a - // retargeted/prototype-sensitive array. It must preserve the - // assignment's strict Set semantics; the non-strict helper - // bypassed `js_array_set_f64_extend_strict` entirely, so an - // inherited setter/non-writable index was silently replaced - // by a new own element. - let new_arr = crate::array::js_array_set_index_or_string_strict( + let new_arr = crate::array::js_array_set_index_or_string( raw_addr as *mut ArrayHeader, index, value, diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index 58cabd8bab..aa02479a7d 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -59,20 +59,6 @@ fn assert_undefined(value: f64) { assert_eq!(value.to_bits(), crate::value::TAG_UNDEFINED); } -fn catch_runtime_throw(f: impl FnOnce()) -> bool { - let env = crate::exception::js_try_push(); - let jumped = unsafe { crate::ffi::setjmp::setjmp(env as *mut std::os::raw::c_int) }; - if jumped == 0 { - f(); - crate::exception::js_try_end(); - false - } else { - crate::exception::js_try_end(); - crate::exception::js_clear_exception(); - true - } -} - fn class_instance( class_id: u32, key_name: &'static [u8], @@ -586,9 +572,8 @@ fn typed_feedback_array_set_guards_reject_frozen_arrays() { 0 ); - assert!(catch_runtime_throw(|| { - js_typed_feedback_array_index_set_fallback_boxed(70, arr_box, 0.0, 99.0); - })); + let returned = js_typed_feedback_array_index_set_fallback_boxed(70, arr_box, 0.0, 99.0); + assert_eq!(returned.to_bits(), arr_box.to_bits()); assert_eq!( crate::array::js_array_get_f64(arr, 0).to_bits(), 1.0f64.to_bits() diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index 7ca573b098..d9f317b916 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -34,7 +34,6 @@ crates/perry-runtime/src/array/generic.rs | * | pre-existing GcHeader probe pred crates/perry-runtime/src/array/header.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/array/indexing.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/array/indexing_keyed.rs | * | same pre-existing GcHeader probe, moved from indexing.rs by the 2,000-line file split (js_array_set_string_key); migrate to addr_class::try_read_gc_header in a follow-up -crates/perry-runtime/src/array/indexing_proto_chain.rs | * | same pre-existing GcHeader probe, moved from indexing.rs by the 2,000-line file split (the spec [[Get]]/[[HasProperty]]/[[Set]] prototype-chain block); migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/array/is_array.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/array/iter_methods.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up crates/perry-runtime/src/array/iter_object.rs | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index 136e25806a..f476ad620e 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -32,9 +32,8 @@ handle-floor | crates/perry-runtime/src/array/concat_reverse.rs | 1 handle-floor | crates/perry-runtime/src/array/flat_clone.rs | 4 handle-floor | crates/perry-runtime/src/array/generic.rs | 4 handle-floor | crates/perry-runtime/src/array/header.rs | 3 -handle-floor | crates/perry-runtime/src/array/indexing.rs | 2 +handle-floor | crates/perry-runtime/src/array/indexing.rs | 3 handle-floor | crates/perry-runtime/src/array/indexing_keyed.rs | 1 -handle-floor | crates/perry-runtime/src/array/indexing_proto_chain.rs | 1 handle-floor | crates/perry-runtime/src/array/iter_object.rs | 1 handle-floor | crates/perry-runtime/src/array/iterator.rs | 2 handle-floor | crates/perry-runtime/src/array/push_pop.rs | 1 diff --git a/test-files/test_gap_9220_9221_array_proto_paths.ts b/test-files/test_gap_9220_9221_array_proto_paths.ts deleted file mode 100644 index 1934ad120f..0000000000 --- a/test-files/test_gap_9220_9221_array_proto_paths.ts +++ /dev/null @@ -1,296 +0,0 @@ -// #9220 / #9221: the array-index write and borrowed Array.prototype method -// paths must use the same recorded-[[Prototype]] classification as direct -// indexed reads. These were silent wrong answers: assignments created an own -// element instead of invoking/rejecting through an inherited descriptor, and -// the generic array-like engine treated a prototype-filled hole as absent. -// -// The array-prototype cases are controls: both bugs pre-date support for a -// non-array custom prototype and reproduce when the prototype is itself an -// array, a shape Perry has long supported. - -const hasOwn = (value: any, key: PropertyKey) => - Object.prototype.hasOwnProperty.call(value, key); - -// --- #9220: inherited accessor writes ------------------------------------- - -const writeCalls: any[] = []; -const writeTarget: any = [1, 2, 3]; -let writeThis = false; -const writeProto: any = {}; -Object.defineProperty(writeProto, "9", { - configurable: true, - get() { - return "acc9"; - }, - set(this: any, value: any) { - writeCalls.push(value); - writeThis = this === writeTarget; - }, -}); -Object.setPrototypeOf(writeTarget, writeProto); -writeTarget[9] = 5; -console.log( - "write object accessor:", - writeCalls.join(","), - writeThis, - hasOwn(writeTarget, 9), - writeTarget[9], - writeTarget.length, -); - -// Exercise the in-bounds-hole shape as well as the out-of-bounds shape above. -const holeWriteCalls: any[] = []; -const holeWriteTarget: any = [0, , 2]; -const holeWriteProto: any = {}; -Object.defineProperty(holeWriteProto, "1", { - configurable: true, - get() { - return "acc1"; - }, - set(value: any) { - holeWriteCalls.push(value); - }, -}); -Object.setPrototypeOf(holeWriteTarget, holeWriteProto); -holeWriteTarget[1] = 7; -console.log( - "write hole accessor:", - holeWriteCalls.join(","), - hasOwn(holeWriteTarget, 1), - holeWriteTarget[1], - holeWriteTarget.length, -); - -// Control: the same inherited-setter bug with an Array as [[Prototype]]. -const arrayWriteCalls: any[] = []; -const arrayWriteTarget: any = [1]; -const arrayWriteProto: any = []; -Object.defineProperty(arrayWriteProto, "4", { - configurable: true, - get() { - return "arrayAcc4"; - }, - set(value: any) { - arrayWriteCalls.push(value); - }, -}); -Object.setPrototypeOf(arrayWriteTarget, arrayWriteProto); -arrayWriteTarget[4] = 11; -console.log( - "write array accessor:", - arrayWriteCalls.join(","), - hasOwn(arrayWriteTarget, 4), - arrayWriteTarget[4], - arrayWriteTarget.length, -); - -// An inherited writable data property does not intercept OrdinarySet: the -// receiver gets an own property and the prototype value is unchanged. -const dataProto: any = { 5: "protoFive" }; -const dataTarget: any = [1]; -Object.setPrototypeOf(dataTarget, dataProto); -dataTarget[5] = "ownFive"; -console.log( - "write inherited data:", - hasOwn(dataTarget, 5), - dataTarget[5], - dataProto[5], - dataTarget.length, -); - -// A non-writable inherited data property rejects the assignment. This file is -// an ES module (the repository package is type=module), so rejection throws. -const lockedProto: any = {}; -Object.defineProperty(lockedProto, "6", { - configurable: true, - enumerable: true, - value: "lockedSix", - writable: false, -}); -const lockedTarget: any = [1]; -Object.setPrototypeOf(lockedTarget, lockedProto); -let lockedThrew = false; -try { - lockedTarget[6] = "changed"; -} catch { - lockedThrew = true; -} -console.log( - "write inherited readonly:", - lockedThrew, - hasOwn(lockedTarget, 6), - lockedTarget[6], - lockedTarget.length, -); - -// The inherited descriptor may be further up the chain. `Object.create(p)` -// models the link with a synthetic class id rather than a recorded prototype, -// so the [[Set]] owner walk has to take the same hop the [[Get]] walk takes. -const deepCalls: any[] = []; -const deepGrand: any = {}; -Object.defineProperty(deepGrand, "4", { - configurable: true, - get() { - return "deep4"; - }, - set(value: any) { - deepCalls.push(value); - }, -}); -const deepTarget: any = [1]; -Object.setPrototypeOf(deepTarget, Object.create(deepGrand)); -deepTarget[4] = 3; -console.log( - "write deep create:", - deepCalls.join(","), - hasOwn(deepTarget, 4), - deepTarget[4], - deepTarget.length, -); - -// The same shape with every link installed by setPrototypeOf. -const deepSetCalls: any[] = []; -const deepSetGrand: any = {}; -Object.defineProperty(deepSetGrand, "4", { - configurable: true, - get() { - return "deepSet4"; - }, - set(value: any) { - deepSetCalls.push(value); - }, -}); -const deepSetMid: any = {}; -Object.setPrototypeOf(deepSetMid, deepSetGrand); -const deepSetTarget: any = [1]; -Object.setPrototypeOf(deepSetTarget, deepSetMid); -deepSetTarget[4] = 6; -console.log( - "write deep setproto:", - deepSetCalls.join(","), - hasOwn(deepSetTarget, 4), - deepSetTarget[4], - deepSetTarget.length, -); - -// A non-extensible or frozen receiver does not stop an inherited setter: -// OrdinarySet finds the prototype accessor before it ever reaches the -// receiver's own-property create. -const frozenCalls: any[] = []; -const frozenProto: any = {}; -Object.defineProperty(frozenProto, "4", { - configurable: true, - get() { - return "frozen4"; - }, - set(value: any) { - frozenCalls.push(value); - }, -}); -const frozenTarget: any = [1]; -Object.setPrototypeOf(frozenTarget, frozenProto); -Object.freeze(frozenTarget); -frozenTarget[4] = 8; -console.log( - "write frozen receiver:", - frozenCalls.join(","), - hasOwn(frozenTarget, 4), - frozenTarget.length, -); - -// Control: the default chain keeps its strict rejection on a frozen array. -const frozenDefault: any = [1, 2, 3]; -Object.freeze(frozenDefault); -let frozenDefaultThrew = false; -try { - frozenDefault[0] = 9; -} catch { - frozenDefaultThrew = true; -} -console.log( - "write frozen default:", - frozenDefaultThrew, - frozenDefault[0], - frozenDefault.length, -); - -// --- #9221: borrowed Array.prototype methods over a real Array ------------ - -const genericProto: any = { 1: "holeFill" }; -const genericTarget: any = [0, , 2]; -Object.setPrototypeOf(genericTarget, genericProto); -const genericMapSeen: string[] = []; -const genericMapped: any = Array.prototype.map.call( - genericTarget, - (value: any, index: number) => { - genericMapSeen.push(index + ":" + value); - return String(value).toUpperCase(); - }, -); -const genericEachSeen: string[] = []; -Array.prototype.forEach.call(genericTarget, (value: any, index: number) => { - genericEachSeen.push(index + ":" + value); -}); -console.log("generic object join:", Array.prototype.join.call(genericTarget)); -console.log( - "generic object indexOf:", - Array.prototype.indexOf.call(genericTarget, "holeFill"), -); -console.log( - "generic object map:", - genericMapSeen.join("|"), - Array.prototype.join.call(genericMapped, "|"), - hasOwn(genericMapped, 1), -); -console.log("generic object forEach:", genericEachSeen.join("|")); - -// Accessor fill: Get must bind the original array as receiver. -let genericGetterCalls = 0; -let genericGetterThis = true; -const genericGetterTarget: any = [0, , 2]; -const genericGetterProto: any = {}; -Object.defineProperty(genericGetterProto, "1", { - configurable: true, - get(this: any) { - genericGetterCalls++; - genericGetterThis = genericGetterThis && this === genericGetterTarget; - return "getterFill"; - }, -}); -Object.setPrototypeOf(genericGetterTarget, genericGetterProto); -const getterJoined = Array.prototype.join.call(genericGetterTarget, "-"); -console.log( - "generic getter join:", - getterJoined, - genericGetterCalls, - genericGetterThis, -); - -// Control: identical generic-engine divergence with an Array [[Prototype]]. -const genericArrayProto: any = []; -genericArrayProto[1] = "arrayFill"; -const genericArrayTarget: any = [0, , 2]; -Object.setPrototypeOf(genericArrayTarget, genericArrayProto); -const genericArraySeen: string[] = []; -Array.prototype.forEach.call(genericArrayTarget, (value: any, index: number) => { - genericArraySeen.push(index + ":" + value); -}); -console.log( - "generic array control:", - Array.prototype.join.call(genericArrayTarget), - Array.prototype.indexOf.call(genericArrayTarget, "arrayFill"), - genericArraySeen.join("|"), -); - -// Default-chain control: ordinary holes remain holes for HasProperty methods. -const defaultTarget: any = [0, , 2]; -const defaultSeen: string[] = []; -Array.prototype.forEach.call(defaultTarget, (value: any, index: number) => { - defaultSeen.push(index + ":" + value); -}); -console.log( - "generic default control:", - Array.prototype.join.call(defaultTarget), - Array.prototype.indexOf.call(defaultTarget, undefined), - defaultSeen.join("|"), -);