Summary
ObjectHeader.keys_array is the last blocker for #8047's header shrink. #8086 made the ShapeId descriptor authoritative for the keys value, but the header word is still the only thing that roots the keys array and the only rewritable location the evacuator can hand to a slot visitor. Removing it unroots every keys array.
This issue is the missing GC protocol. It deliberately does not change ObjectHeader's size; #8047 performs that atomically afterwards.
The state on main (12f758a22), in the tree's own words
crates/perry-runtime/src/gc/layout_slot_visit.rs:63
// #8067: the header keys slot above is the sole strong edge.
crates/perry-runtime/src/object/shapes.rs:947-950 (scan_shape_table_rekey_mut)
/// Metadata-only forwarding repair for the weak descriptor table and
/// pointer-keyed slot indices. Mark/copy mode does not root anything; live
/// object scans provide descriptor reachability, and post-copy rewrite
/// follows only forwarding records those live edges already created.
crates/perry-runtime/src/object/gc_slots.rs:4 keeps the field deliberately, as a "Compatibility scratch slot", and names #8047 as where it is replaced:
// #8047 can replace this scratch with a descriptor-table rewrite without
// changing the source of the edge.
So today: the descriptor supplies the value; gc_keys_array_slot writes it into the header field; the collector marks and rewrites that word; and scan_shape_table_rekey_mut afterwards repairs the weak table by following forwarding records the header edge already created.
Why the obvious implementation is wrong
Promoting ShapeDescriptor.keys to a GC slot is ruled out in layout_slot_visit.rs:64-68:
// Never enumerate the HashMap bucket as a GC slot: dirty-page work may
// retain enumerated slot addresses across budgeted resumptions, during
// which descriptor insertion can reallocate the table.
The descriptor lives in a rehashing PtrHashMap, and the incremental collector retains enumerated slot addresses across budgeted resumptions.
Required design
- Stable-address descriptor storage. A chunked/arena-backed record store whose
keys field has a fixed address for the descriptor's lifetime, so the collector can enumerate and rewrite it across budgeted resumptions. The HashMap may remain as an index into that store, but must not own the edge.
- An ephemeron / liveness protocol. The table must root
keys only while some live object still carries that ShapeId; unconditional rooting makes every keys array ever minted immortal. Note prune_dead_shape_keys currently asks "is the keys array dead?" — that becomes circular the moment the table roots it, and must become "was this id observed on a live object during mark?" (e.g. a mark-time ShapeId bitset, then a fixpoint round because keys arrays contain strings).
- Evacuation and verification.
PERRY_GC_FORCE_EVACUATE, PERRY_GC_VERIFY_EVACUATION and PERRY_GC_PROTECT_FROMSPACE must all pass with the moving collector demonstrably live — assert the subject ran (copied_objects > 0 or promoted_objects > 0), not merely that nothing threw.
- Sibling and agent isolation preserved.
shape_drop_does_not_delete_a_potential_siblings_descriptor, shared_sibling_append_clones_before_descriptor_version_changes, a_foreign_agent_id_misses_instead_of_aliasing_same_address and process_global_module_shape_id_installs_with_agent_local_keys must keep passing.
Acceptance
- GC mark, rewrite and evacuation recover and rewrite a keys array without reading or writing
ObjectHeader.keys_array — the field may still exist, but nothing in the collector consults it.
- A keys array reachable only through the descriptor table survives a collection; a keys array whose last carrying object died is reclaimed (both directions tested — an immortality bug is as real as a use-after-free).
- The instrument is sabotage-tested: plant a stale descriptor edge and show the canary distinguishes it from a live object, in the manner of
quarantine_catches_a_planted_stale_from_space_deref.
ObjectHeader size is unchanged by this issue.
Refs #8047, #8067, #8086, #7154, #7164.
Summary
ObjectHeader.keys_arrayis the last blocker for #8047's header shrink. #8086 made theShapeIddescriptor authoritative for the keys value, but the header word is still the only thing that roots the keys array and the only rewritable location the evacuator can hand to a slot visitor. Removing it unroots every keys array.This issue is the missing GC protocol. It deliberately does not change
ObjectHeader's size; #8047 performs that atomically afterwards.The state on
main(12f758a22), in the tree's own wordscrates/perry-runtime/src/gc/layout_slot_visit.rs:63crates/perry-runtime/src/object/shapes.rs:947-950(scan_shape_table_rekey_mut)crates/perry-runtime/src/object/gc_slots.rs:4keeps the field deliberately, as a "Compatibility scratch slot", and names #8047 as where it is replaced:So today: the descriptor supplies the value;
gc_keys_array_slotwrites it into the header field; the collector marks and rewrites that word; andscan_shape_table_rekey_mutafterwards repairs the weak table by following forwarding records the header edge already created.Why the obvious implementation is wrong
Promoting
ShapeDescriptor.keysto a GC slot is ruled out inlayout_slot_visit.rs:64-68:The descriptor lives in a rehashing
PtrHashMap, and the incremental collector retains enumerated slot addresses across budgeted resumptions.Required design
keysfield has a fixed address for the descriptor's lifetime, so the collector can enumerate and rewrite it across budgeted resumptions. TheHashMapmay remain as an index into that store, but must not own the edge.keysonly while some live object still carries thatShapeId; unconditional rooting makes every keys array ever minted immortal. Noteprune_dead_shape_keyscurrently asks "is the keys array dead?" — that becomes circular the moment the table roots it, and must become "was this id observed on a live object during mark?" (e.g. a mark-time ShapeId bitset, then a fixpoint round because keys arrays contain strings).PERRY_GC_FORCE_EVACUATE,PERRY_GC_VERIFY_EVACUATIONandPERRY_GC_PROTECT_FROMSPACEmust all pass with the moving collector demonstrably live — assert the subject ran (copied_objects > 0orpromoted_objects > 0), not merely that nothing threw.shape_drop_does_not_delete_a_potential_siblings_descriptor,shared_sibling_append_clones_before_descriptor_version_changes,a_foreign_agent_id_misses_instead_of_aliasing_same_addressandprocess_global_module_shape_id_installs_with_agent_local_keysmust keep passing.Acceptance
ObjectHeader.keys_array— the field may still exist, but nothing in the collector consults it.quarantine_catches_a_planted_stale_from_space_deref.ObjectHeadersize is unchanged by this issue.Refs #8047, #8067, #8086, #7154, #7164.