diff --git a/changelog.d/8943-length-zero-header-lane.md b/changelog.d/8943-length-zero-header-lane.md new file mode 100644 index 0000000000..174694e517 --- /dev/null +++ b/changelog.d/8943-length-zero-header-lane.md @@ -0,0 +1 @@ +- **runtime:** `arr.length = 0` on a plain dense array (both the ordinary and the strict entry) is decided from one header read — no receiver resolution through the registries, no length coercion, no second flag resolution, no named-property probe — and then does exactly the plain-shrink branch's work; frozen/sealed/descriptor-carrying arrays, forwarded heads and arrays with named properties still take the full entry. diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index 000bcba229..260b4457ff 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -1211,8 +1211,62 @@ pub extern "C" fn js_array_pop_f64(arr: *mut ArrayHeader) -> f64 { /// non-throwing `[[DefineOwnProperty]]`/no-Throw contract. Only the assignment /// codegen paths (`field_set_by_name` / `property_set` / proxy `PutValue`) route /// here. test262 built-ins/Array length-write-on-frozen. +/// `arr.length = 0` on a plain dense array, decided from one header read. +/// +/// Both `length =` entries resolve the receiver through `clean_arr_ptr_mut` +/// (allocator ownership, forwarding, the Buffer / typed-array registries), +/// coerce the new length, resolve the flags a second time and probe the +/// named-property table before reaching the plain-shrink branch — for an +/// object pool's `pooled.length = 0` that tower was the whole cost, five +/// thousand times a frame. Exactly the header facts the pop fast path proves +/// are enough here: a `GC_TYPE_ARRAY` head that is not forwarded, none of the +/// integrity / descriptor flags (a non-writable `length` is recorded under +/// `OBJ_FLAG_ARRAY_DESCRIPTORS`, so neither entry has anything to throw), a +/// dense `length <= capacity`, and no named properties. The work is the +/// plain-shrink branch's, unchanged: holes over the retired prefix, the +/// length, one layout rebuild. Anything else declines to the full entry. +#[inline(always)] +fn try_truncate_plain_array_to_zero(arr: *mut ArrayHeader) -> bool { + let Some(header) = (unsafe { crate::value::addr_class::try_read_gc_header(arr as usize) }) + else { + return false; + }; + let guarded_flags = crate::gc::OBJ_FLAG_FROZEN + | crate::gc::OBJ_FLAG_SEALED + | crate::gc::OBJ_FLAG_NO_EXTEND + | crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS; + if header.obj_type != crate::gc::GC_TYPE_ARRAY + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + || header._reserved & guarded_flags != 0 + { + return false; + } + unsafe { + let cur = (*arr).length; + if cur > (*arr).capacity || array_has_named_properties_resolved(arr) { + return false; + } + if cur == 0 { + return true; + } + let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + for i in 0..cur { + // GC_STORE_AUDIT(BARRIERED): the suffix becomes unreachable when + // length is published below; rebuild_array_layout then rebuilds + // the complete live-prefix layout/barrier state. + ptr::write(elements.add(i as usize), crate::value::TAG_HOLE); + } + (*arr).length = 0; + rebuild_array_layout(arr); + } + true +} + #[no_mangle] pub extern "C" fn js_array_set_length_strict(arr: *mut ArrayHeader, new_length: f64) { + if new_length.to_bits() == 0 && try_truncate_plain_array_to_zero(arr) { + return; + } let cleaned = clean_arr_ptr_mut(arr); if cleaned.is_null() { // #7574: `a.length = n` on a `class X extends Array` instance reached @@ -1235,6 +1289,9 @@ pub extern "C" fn js_array_set_length_strict(arr: *mut ArrayHeader, new_length: #[no_mangle] pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { + if new_length.to_bits() == 0 && try_truncate_plain_array_to_zero(arr) { + return; + } let arr = clean_arr_ptr_mut(arr); if arr.is_null() { return; diff --git a/crates/perry-runtime/src/array/push_pop_tests.rs b/crates/perry-runtime/src/array/push_pop_tests.rs index 7cdcfd2585..a155e98739 100644 --- a/crates/perry-runtime/src/array/push_pop_tests.rs +++ b/crates/perry-runtime/src/array/push_pop_tests.rs @@ -33,6 +33,50 @@ fn pop_on_an_empty_plain_array_is_undefined_from_the_fast_path() { ); } +/// `length = 0` through both entries takes the header-only lane on a plain +/// dense array — holes over the retired prefix, length published, the array +/// still usable — and declines it for a frozen array (the strict entry must +/// still throw) and for one carrying a named property. +#[test] +fn length_zero_takes_the_header_lane_on_a_plain_array_and_declines_otherwise() { + let mut arr = js_array_alloc(4); + for v in 1..=3 { + arr = js_array_push_f64(arr, v as f64); + } + js_array_set_length(arr, 0.0); + assert_eq!(js_array_length(arr), 0); + js_array_set_length(arr, 2.0); + for index in 0..2 { + assert_eq!( + array_spec_get(arr, index).to_bits(), + crate::value::TAG_UNDEFINED + ); + } + js_array_set_length(arr, 0.0); + let arr = js_array_push_f64(arr, 9.0); + assert_eq!(js_array_get_f64(arr, 0), 9.0); + assert_eq!(js_array_length(arr), 1); + js_array_set_length_strict(arr, 0.0); + assert_eq!(js_array_length(arr), 0); + // Already empty: a no-op through both entries. + js_array_set_length(arr, 0.0); + js_array_set_length_strict(arr, 0.0); + assert_eq!(js_array_length(arr), 0); + + // A named property keeps the full entry (which clears both representations). + let mut named = js_array_alloc(4); + named = js_array_push_f64(named, 1.0); + let key = crate::string::js_string_from_bytes(b"0".as_ptr(), 1); + unsafe { array_named_property_set(named, key, 99.0) }; + js_array_set_length(named, 0.0); + assert_eq!(js_array_length(named), 0); + js_array_set_length(named, 1.0); + assert_eq!( + array_spec_get(named, 0).to_bits(), + crate::value::TAG_UNDEFINED + ); +} + #[test] fn test_array_pop_and_push() { let arr = js_array_alloc(4);