From 5b1c0576a5e3d3e2ce483ce2aa6c254c1c5332b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 30 Aug 2026 01:34:01 +0200 Subject: [PATCH] fix(runtime): squeeze republishes coherent facts before the live-bound publish (#9108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tombstone squeeze (default-on since #9038) shrank the keys array in place and then called set_object_live_slot_count BEFORE republishing the shape. When the floored bound is unchanged — the at-scale/overflow case — that call's early return asserts parity against the STILL-STAMPED pre-squeeze descriptor and SIGABRTs the debug suite (reserved_floor at-scale test), masking the ~1000 tests behind it on every branch. Two coherence fixes: - squeeze_holes_and_delete orders shape_drop -> publish_object_shape_holes -> set_object_live_slot_count, so the bound publish always sees the squeezed descriptor; - publish_object_shape_holes reads logical_key_count from the ARRAY, not the lineage: identical for the O(1) hole delete (length untouched), and the only correct source right after a squeeze changed the length. Release binaries were unaffected (debug_assert only), which is why the differential battery stayed byte-identical while the test suite died. Full perry-runtime suite now runs to completion: 2819 passed / 0 failed. Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP --- crates/perry-runtime/src/object/delete_rest.rs | 10 +++++++++- crates/perry-runtime/src/object/shapes_slot_list.rs | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) 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,