diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 20efe708e..4619feb5e 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 a5b26e182..3535a9ba9 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 1874d24fd..32c833e90 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -255,24 +255,47 @@ 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 { + 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); } }