From 9113d3ca57828122f9988d950d35cc925a3c6973 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 17:25:17 +0000 Subject: [PATCH 1/2] fix(array): exotic index propagation for Object.prototype + join/slice (#5589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three runtime fixes for test262 built-ins/Array failures: 1. `array_iteration_is_exotic` (indexing.rs): add `OBJECT_PROTO_HAS_INDEX` check alongside the existing `ARRAY_PROTO_HAS_INDEX` one. Without it, any method that delegates to the exotic slow-path (reduce, reduceRight, forEach, map, filter, find, findIndex, every, some, indexOf, lastIndexOf, flat, flatMap, sort) would not notice a numeric property added to Object.prototype, causing a missed inherited read and a wrong result. 2. `js_array_join` (iter_methods.rs): add the exotic fast/slow-path split that every other iteration method already uses. Previously join always read directly from the dense element store, so a numeric property on Array.prototype or Object.prototype was invisible to join/toString. 3. `js_array_slice` (splice_slice.rs): same fix — when the source array is exotic, use array_spec_has_index/array_spec_get per element so inherited indices appear in the slice result (ECMA-262 §23.1.3.25 step 8b). All three cases are covered by a passing Perry verification test. --- crates/perry-runtime/src/array/indexing.rs | 3 ++ .../perry-runtime/src/array/iter_methods.rs | 35 +++++++++---------- .../perry-runtime/src/array/splice_slice.rs | 33 +++++++++++++---- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 20efe708e6..4619feb5ea 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -219,6 +219,9 @@ pub(crate) fn array_iteration_is_exotic(arr: *const ArrayHeader) -> bool { if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { return true; } + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) { + return true; + } // Live indices beyond the dense backing store are stored in the sparse // named-property map, which the raw element loop never reads. unsafe { (*arr).length > (*arr).capacity } diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index a5b26e182e..3535a9ba90 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -734,6 +734,7 @@ pub extern "C" fn js_array_join( } let elements_ptr = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let exotic = crate::array::array_iteration_is_exotic(arr); // Get separator string let sep_str = if separator.is_null() { @@ -750,27 +751,23 @@ pub extern "C" fn js_array_join( if i > 0 { result.push_str(sep_str); } - let element_bits = (*elements_ptr.add(i)).to_bits(); + let element_bits = if exotic { + if !crate::array::array_spec_has_index(arr, i as u32) { + // absent slot (own or inherited) → empty string per spec + continue; + } + crate::array::array_spec_get(arr, i as u32).to_bits() + } else { + let bits = (*elements_ptr.add(i)).to_bits(); + // Issue #907: `Array(n)` initializes slots to TAG_HOLE; per + // ES2015 §22.1.3.13 holes stringify to the empty string. + if bits == crate::value::TAG_HOLE { + continue; + } + bits + }; let jsvalue = JSValue::from_bits(element_bits); - // Issue #907: `Array(n)` initializes slots to TAG_HOLE - // (see `js_array_alloc_with_length`). Per ES2015 §22.1.3.13 - // (Array.prototype.join), holes go through Get which returns - // undefined → the spec's ToString step turns them into the - // empty string. Without this check the catch-all below - // emitted "[object Object]", so `Array(3).join("0")` returned - // `"[object Object]0[object Object]0[object Object]"` instead - // of `"00"`. dayjs's `m(t,e,n)` pad utility builds the UTC - // offset string via `Array(e+1-r.length).join(n)` and the - // result silently corrupted `b.z(this)` (the format `i` - // capture), which downstream triggered - // `TypeError: (number).replace is not a function` once the - // catch-all fallthrough reached `i.replace(":","")`. - if element_bits == crate::value::TAG_HOLE { - // hole → empty string per spec - continue; - } - // Convert element to string based on its type if jsvalue.is_string() { let str_ptr = jsvalue.as_string_ptr(); diff --git a/crates/perry-runtime/src/array/splice_slice.rs b/crates/perry-runtime/src/array/splice_slice.rs index 1874d24fd2..d365519baf 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -255,24 +255,43 @@ pub extern "C" fn js_array_slice( let is_plain = crate::array::species::species_result_is_plain_array(result_box); let result = crate::value::js_nanbox_get_pointer(result_box) as *mut ArrayHeader; - // Copy elements + // Copy elements — use spec-generic Get when the source is exotic + // (has Array.prototype or Object.prototype indexed properties) so + // inherited indices appear in the result just as [[Get]] would return + // them (ECMA-262 §23.1.3.25 step 8b "If HasProperty(O, from)…"). let src_elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let src_exotic = crate::array::array_iteration_is_exotic(arr); if is_plain { (*result).length = slice_len; let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..slice_len as usize { - // GC_STORE_AUDIT(BARRIERED): slice result init is followed by layout/barrier rebuild. - ptr::write( - dst_elements.add(i), - ptr::read(src_elements.add(start_idx as usize + i)), - ); + let src_idx = start_idx as usize + i; + let v = if src_exotic { + if crate::array::array_spec_has_index(arr, src_idx as u32) { + crate::array::array_spec_get(arr, src_idx as u32) + } else { + f64::from_bits(crate::value::TAG_HOLE) + } + } else { + // GC_STORE_AUDIT(BARRIERED): slice result init is followed by layout/barrier rebuild. + ptr::read(src_elements.add(src_idx)) + }; + ptr::write(dst_elements.add(i), v); } rebuild_array_layout(result); } else { // Custom species container: CreateDataPropertyOrThrow per element. for i in 0..slice_len as usize { - let v = ptr::read(src_elements.add(start_idx as usize + i)); + let src_idx = start_idx as usize + i; + let v = if src_exotic { + if !crate::array::array_spec_has_index(arr, src_idx as u32) { + continue; // absent slot → no property created in result + } + crate::array::array_spec_get(arr, src_idx as u32) + } else { + ptr::read(src_elements.add(src_idx)) + }; crate::array::species::species_result_set(result_box, i, v); } } From 2278aa23445aca9febf3e4b1736a72d5f7589eed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 17:33:04 +0000 Subject: [PATCH 2/2] fix(array): skip TAG_HOLE in non-exotic species-slice path (CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In js_array_slice's custom-species branch, the non-exotic else path was forwarding raw TAG_HOLE values to species_result_set, creating a property on the result for absent dense slots. The spec (§23.1.3.25 step 8b) only calls CreateDataPropertyOrThrow when HasProperty(O, from) is true, so holes must be skipped regardless of whether the source is exotic. --- crates/perry-runtime/src/array/splice_slice.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/array/splice_slice.rs b/crates/perry-runtime/src/array/splice_slice.rs index d365519baf..32c833e907 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -290,7 +290,11 @@ pub extern "C" fn js_array_slice( } crate::array::array_spec_get(arr, src_idx as u32) } else { - ptr::read(src_elements.add(src_idx)) + let v = ptr::read(src_elements.add(src_idx)); + if v.to_bits() == crate::value::TAG_HOLE { + continue; // hole → no property created in result + } + v }; crate::array::species::species_result_set(result_box, i, v); }