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
9 changes: 9 additions & 0 deletions changelog.d/9579-array-expando-growth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Named properties attached to arrays remain visible after an indexed write grows
the array's backing allocation.

The named-property table is keyed by the array header address, while growth
replaces that header and leaves a forwarding stub behind. Direct runtime
coverage now pins the required owner transfer to the replacement header and
asserts that the stale owner is removed. A Node differential also covers typed
and dynamic arrays through growth, including property enumeration and JSON
serialization.
39 changes: 39 additions & 0 deletions crates/perry-runtime/src/array/large_presized_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@

use super::*;

// #9201: named properties live in a side table keyed by the array allocation.
// A grow replaces that allocation, so both the values and the owner key must
// move before the old header becomes a forwarding stub.
#[test]
fn growth_rekeys_named_property_owner() {
let _global = crate::gc::global_side_table_test_lock();
unsafe {
const NAME: &str = "__perry_test_9201_expando__";
let key = crate::string::js_string_from_bytes(NAME.as_ptr(), NAME.len() as u32);
let mut arr = js_array_alloc(3);
arr = js_array_push_f64(arr, 1.0);
arr = js_array_push_f64(arr, 2.0);
arr = js_array_push_f64(arr, 3.0);

array_named_property_set(arr, key, 42.0);
let old_owner = arr as usize;
let old_capacity = (*arr).capacity;
assert_eq!(
array_named_property_get_by_name(arr, NAME),
Some(42.0),
"the expando must exist before growth"
);

arr = js_array_set_f64_extend(arr, old_capacity, 99.0);

assert_ne!(arr as usize, old_owner, "the fixture must grow the array");
assert_eq!(
array_named_property_get_by_name(arr, NAME),
Some(42.0),
"#9201: growth must preserve the named property"
);
assert!(test_array_named_property_owner_exists(arr as usize));
assert!(
!test_array_named_property_owner_exists(old_owner),
"the side table must no longer be keyed by the forwarding stub"
);
}
}

#[test]
fn large_presized_array_grows_its_dense_frontier() {
unsafe {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/array/push_pop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu
crate::gc::layout_transfer(arr as *mut u8, new_ptr as *mut u8);
// Array expandos and sparse numeric indices live in an address-keyed
// side table. Growth is not a collector move, so rekey it explicitly
// before the old address becomes a forwarding stub (#9371).
// before the old address becomes a forwarding stub (#9371, #9201).
transfer_array_named_property_owner(arr as usize, new_ptr as usize);
// `js_array_grow` is an allocation replacement outside the collector,
// so GC's normal side-table rekey phase does not run. Preserve every
Expand Down
19 changes: 19 additions & 0 deletions test-files/test_gap_9201_array_named_properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const arr: any[] = [1, 2, 3];
arr.foo = "bar";
console.log("before", arr.length, arr.foo, Object.keys(arr).join(","));

arr[10] = "sparse";

console.log("after", arr.length, arr.foo, arr[10]);
console.log(Object.keys(arr).join(","));

const dynamic: any = [4, 5];
dynamic.metadata = { total: 2 };
dynamic["hasMore"] = false;
console.log("before", dynamic.length, dynamic.metadata.total, dynamic.hasMore);

dynamic[12] = "tail";

console.log("after", dynamic.length, dynamic.metadata.total, dynamic.hasMore, dynamic[12]);
console.log(Object.keys(dynamic).join(","));
console.log(JSON.stringify(dynamic));
Loading