Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/8568-array-join-direct-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**Speed up `Array.prototype.join` by writing directly into GC string storage.**

Dense joins of strings, booleans, holes, `null`, and `undefined` now compute the
exact output size, allocate one `StringHeader`, and copy each piece into its
payload instead of building and freeing an intermediate Rust `String`. Joins
that invoke getters or `toString` use a rooted growable writer, preserving
exactly-once coercion, moving-GC safety, and WTF-8 lone-surrogate behavior.
280 changes: 0 additions & 280 deletions crates/perry-runtime/src/array/iter_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,286 +1077,6 @@ pub(crate) fn throw_reduce_of_empty() -> ! {
crate::exception::js_throw(f64::from_bits(err_value))
}

/// Read one exotic join element while keeping the receiver current across a
/// getter or prototype lookup that can allocate and move it.
#[cold]
#[inline(never)]
fn join_exotic_element(arr: *const ArrayHeader, index: u32) -> (Option<u64>, *const ArrayHeader) {
let scope = crate::gc::RuntimeHandleScope::new();
let arr_handle = scope.root_raw_const_ptr(arr);
let present = arr_handle.with_const_ptr(|arr| crate::array::array_spec_has_index(arr, index));
if !present {
return (None, arr_handle.with_const_ptr(normalize_array_receiver));
}
let value = arr_handle.with_const_ptr(|arr| crate::array::array_spec_get(arr, index));
(
Some(value.to_bits()),
arr_handle.with_const_ptr(normalize_array_receiver),
)
}

/// Run an allocating element conversion with the receiver and value rooted,
/// then return the receiver's post-collection address.
#[cold]
#[inline(never)]
fn join_element_to_string(
arr: *const ArrayHeader,
element_bits: u64,
) -> (*const crate::string::StringHeader, *const ArrayHeader) {
let scope = crate::gc::RuntimeHandleScope::new();
let arr_handle = scope.root_raw_const_ptr(arr);
let element_handle = scope.root_nanbox_u64(element_bits);
let string = crate::value::js_jsvalue_to_string(element_handle.get_nanbox_f64());
(string, arr_handle.with_const_ptr(normalize_array_receiver))
}

#[cold]
#[inline(never)]
fn join_bigint_to_string(
arr: *const ArrayHeader,
element_bits: u64,
) -> (*const crate::string::StringHeader, *const ArrayHeader) {
let scope = crate::gc::RuntimeHandleScope::new();
let arr_handle = scope.root_raw_const_ptr(arr);
let element_handle = scope.root_nanbox_u64(element_bits);
let value = crate::value::JSValue::from_bits(element_handle.get_nanbox_u64());
let string = crate::bigint::js_bigint_to_string(value.as_bigint_ptr());
(string, arr_handle.with_const_ptr(normalize_array_receiver))
}

/// join - Join array elements into a string with a separator
/// Returns pointer to new StringHeader
#[no_mangle]
pub extern "C" fn js_array_join(
arr: *const ArrayHeader,
separator: *const crate::string::StringHeader,
) -> *mut crate::string::StringHeader {
use crate::string::{js_string_from_bytes, OwnedStringBytes, StringHeader};
use crate::value::JSValue;

let mut arr = normalize_array_receiver(arr);
if arr.is_null() {
return crate::string::js_string_from_bytes(b"".as_ptr(), 0);
}
// #3148: TypedArray receiver — join element-typed values (Node formatting).
if crate::typedarray::lookup_typed_array_kind(arr as usize).is_some() {
return crate::typedarray::js_typed_array_join(
arr as *const crate::typedarray::TypedArrayHeader,
separator,
);
}
unsafe {
let length = (*arr).length;

// Empty array returns empty string
if length == 0 {
return js_string_from_bytes(ptr::null(), 0);
}

let mut elements_ptr =
(arr as *const u8).add(std::mem::size_of::<ArrayHeader>()) as *const f64;
let exotic = crate::array::array_iteration_is_exotic(arr);

// Element coercion can run user code and move a heap separator. Keep an
// owned snapshot so the hot loop never holds a GC payload borrow across
// a collection point.
let separator_snapshot =
(!separator.is_null()).then(|| OwnedStringBytes::copy_from_header(separator));
let separator_str = separator_snapshot
.as_ref()
.map_or(",", |bytes| std::str::from_utf8_unchecked(bytes.as_bytes()));

// Separators are an exact lower bound for the result. Element
// coercion remains single-pass because it can run user code.
let separator_bytes = separator_str
.len()
.saturating_mul(length.saturating_sub(1) as usize);
let mut result = String::with_capacity(separator_bytes);

for i in 0..length as usize {
if i > 0 {
result.push_str(separator_str);
}
let element_bits = if exotic {
let (element, arr_now) = join_exotic_element(arr, i as u32);
arr = arr_now;
let Some(bits) = element else {
// absent slot (own or inherited) → empty string per spec
continue;
};
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);

// Convert element to string based on its type
if jsvalue.is_string() {
let str_ptr = jsvalue.as_string_ptr();
let str_len = (*str_ptr).byte_len as usize;
let str_data = (str_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
let s =
std::str::from_utf8_unchecked(std::slice::from_raw_parts(str_data, str_len));
result.push_str(s);
} else if jsvalue.is_short_string() {
// v0.5.214 SSO — decode inline into a stack buffer
// and push bytes. No heap roundtrip via
// materialize_to_heap.
let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
let n = jsvalue.short_string_to_buf(&mut scratch);
let s = std::str::from_utf8_unchecked(&scratch[..n]);
result.push_str(s);
} else if jsvalue.is_pointer() {
// POINTER_TAG. Two cases:
// 1. A genuine string NaN-boxed with POINTER_TAG instead of
// STRING_TAG (a cross-module mis-tag) — read its bytes.
// 2. A real heap object/array/error/buffer — these must go
// through the spec `ToString` (`js_jsvalue_to_string`):
// Array→nested join, Error→"name: message" (#2135), an
// object with a custom `toString`→that result, buffers,
// etc. The old code read *every* pointer as a
// `StringHeader`, so a non-string's garbage `byte_len`
// produced corrupted output (`[err].join()` → empty).
// Distinguish via the GcHeader type tag, excluding the
// headerless buffer/symbol pointers first.
let ptr_addr = (element_bits & 0x0000_FFFF_FFFF_FFFF) as usize;
if ptr_addr >= 0x1000 {
let is_string_obj = !crate::buffer::is_registered_buffer(ptr_addr)
&& !crate::symbol::is_registered_symbol(ptr_addr)
&& {
let gc_header = (ptr_addr as *const u8).sub(crate::gc::GC_HEADER_SIZE)
as *const crate::gc::GcHeader;
(*gc_header).obj_type == crate::gc::GC_TYPE_STRING
};
let s_ptr = if is_string_obj {
ptr_addr as *const StringHeader
} else {
let (s_ptr, arr_now) = join_element_to_string(arr, element_bits);
arr = arr_now;
if !exotic {
elements_ptr = array_elements_ptr(arr);
}
s_ptr
};
if !s_ptr.is_null() {
let str_len = (*s_ptr).byte_len as usize;
let str_data =
(s_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
result.push_str(std::str::from_utf8_unchecked(std::slice::from_raw_parts(
str_data, str_len,
)));
}
} else {
result.push_str("[object Object]");
}
} else if jsvalue.is_bigint() {
// BigInt elements are NaN-boxed with BIGINT_TAG (not POINTER_TAG),
// so they bypass the pointer arm above and previously fell through
// to the `[object Object]` catch-all. ToString(BigInt) is the plain
// decimal digits with NO `n` suffix (`[10n].join() === "10"`).
let (s_ptr, arr_now) = join_bigint_to_string(arr, element_bits);
arr = arr_now;
if !exotic {
elements_ptr = array_elements_ptr(arr);
}
if !s_ptr.is_null() {
let str_len = (*s_ptr).byte_len as usize;
let str_data = (s_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
result.push_str(std::str::from_utf8_unchecked(std::slice::from_raw_parts(
str_data, str_len,
)));
}
} else if jsvalue.is_number() {
let n = jsvalue.as_number();
if n.is_nan() {
result.push_str("NaN");
} else if n.is_infinite() {
result.push_str(if n > 0.0 { "Infinity" } else { "-Infinity" });
} else if n == 0.0 {
result.push('0');
} else if n.fract() == 0.0 && n.abs() < 1e15 {
result.push_str(&format!("{}", n as i64));
} else {
result.push_str(&format!("{}", n));
}
} else if jsvalue.is_null() {
// null stringifies to empty string in join
} else if jsvalue.is_undefined() {
// undefined stringifies to empty string in join
} else if jsvalue.is_bool() {
result.push_str(if jsvalue.as_bool() { "true" } else { "false" });
} else if element_bits > 0x1000
&& element_bits < 0x0001_0000_0000_0000
&& (element_bits & 0x3) == 0
{
// Raw pointer fallback — string stored without NaN-box tag
let str_ptr = element_bits as *const StringHeader;
let str_len = (*str_ptr).byte_len as usize;
if str_len < 10_000_000 {
let str_data = (str_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
let s = std::str::from_utf8_unchecked(std::slice::from_raw_parts(
str_data, str_len,
));
result.push_str(s);
} else {
result.push_str("[object Object]");
}
} else {
// For objects/arrays, just use placeholder
result.push_str("[object Object]");
}
}

// Create result string - extract ptr/len before passing to avoid
// potential LLVM reordering of String drop vs copy_nonoverlapping
let result_ptr = result.as_ptr();
let result_len = result.len() as u32;
let ret = js_string_from_bytes(result_ptr, result_len);
// Ensure result String stays alive until after the copy completes
std::hint::black_box(&result);
drop(result);
ret
}
}

#[no_mangle]
pub extern "C" fn js_array_join_value(
arr: *const ArrayHeader,
separator_value: f64,
) -> *mut crate::string::StringHeader {
let separator = if separator_value.to_bits() == crate::value::TAG_UNDEFINED {
ptr::null()
} else {
// `ToString(separator)`: a Symbol separator throws a TypeError
// (§7.1.17) instead of rendering as "Symbol(…)".
if unsafe { crate::symbol::js_is_symbol(separator_value) } != 0 {
crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a string");
}
crate::value::js_jsvalue_to_string(separator_value) as *const crate::string::StringHeader
};
js_array_join(arr, separator)
}

// Symbol retention: codegen lowers `arr.join(sep)` to a call to
// `js_array_join_value`, but its only in-crate caller sits behind a dispatch
// path the auto-optimize whole-program-bitcode build can prove unreachable and
// dead-strip — which broke that link with `undefined _js_array_join_value`.
// The feature-gated `#[used]` static pins the symbol for the bitcode-LTO
// link (`keepalive-anchors`); the classic link keeps it via the program's
// own undefined reference. Same pattern as `node_stream_keepalive.rs`.
#[cfg(feature = "keepalive-anchors")]
#[used]
static KEEP_ARRAY_JOIN_VALUE: extern "C" fn(
*const ArrayHeader,
f64,
) -> *mut crate::string::StringHeader = js_array_join_value;

/// `arr.toLocaleString(locales?, options?)` (#2808).
///
/// Per the ECMAScript `Array.prototype.toLocaleString` algorithm: walk the
Expand Down
Loading
Loading