diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 8f399e182b..308399ee1f 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -1252,7 +1252,14 @@ unsafe fn squeeze_holes_and_delete( crate::gc::runtime_write_barrier_external_slot_span(keys as usize, elements as usize, out); } super::rebuild_array_layout_from_slots(keys); - set_object_live_slot_count(obj, std::cmp::min(out, alloc_limit) as u32); + // Publish the squeezed shape BEFORE touching the live-slot bound: the + // squeeze changed the keys array's length in place, and + // `set_object_live_slot_count`'s unchanged-bound early return asserts + // parity against the STAMPED descriptor — which still carries the + // pre-squeeze logical_key_count until the publish below runs. At scale + // the floored bound is usually unchanged, so that assert fired mid- + // transition (#9108, reserved_floor at-scale test SIGABRT). + // // Slots moved: the per-array key index and any stale descriptors for the // pre-squeeze states are wrong now. Drop the index (rebuilt on demand) // and publish the squeezed shape at exactly the surviving hole count — @@ -1260,4 +1267,5 @@ unsafe fn squeeze_holes_and_delete( // for an iterator-family one. crate::object::shapes::shape_drop(keys); super::shapes::publish_object_shape_holes(obj, floor as u32); + set_object_live_slot_count(obj, std::cmp::min(out, alloc_limit) as u32); } diff --git a/crates/perry-runtime/src/object/shapes_slot_list.rs b/crates/perry-runtime/src/object/shapes_slot_list.rs index 691c4ff60c..8ef66eb25f 100644 --- a/crates/perry-runtime/src/object/shapes_slot_list.rs +++ b/crates/perry-runtime/src/object/shapes_slot_list.rs @@ -226,9 +226,17 @@ pub(crate) unsafe fn publish_object_shape_holes( if generation == 0 { super::shape_id_exhausted_abort(); } + // The key count comes from the ARRAY, not the lineage: the O(1) hole + // delete leaves the length untouched (array == lineage), but the squeeze + // shrinks it in place before republishing — carrying the lineage count + // there left a descriptor disagreeing with the authoritative keys edge, + // which the very next parity assert caught (#9108: reserved_floor + // at-scale SIGABRT took the whole suite down behind it). + let keys_ptr = current.keys as usize as *mut super::ArrayHeader; + let logical_key_count = crate::array::keys_array_len_capped_to_capacity(keys_ptr) as u32; let id = super::publish_shape_result(super::shape_descriptor_ensure_with_holes( - current.keys as usize as *mut super::ArrayHeader, - current.logical_key_count, + keys_ptr, + logical_key_count, current.live_inline_slot_count, generation, current.object_kind,