You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A source/IR/FFI audit of main at 08173f8b4 found that #8047's prerequisite is not complete. Rungs 1–2 made ObjectHeader.parent_class_id a uniform, birth-stamped shape word, but the planned rung 3 never landed: keys_array and field_count are still authoritative mutable object state, several guards still compare the keys pointer directly, and object_type still carries the live OBJECT_TYPE_CLASS semantic.
This issue is the missing prerequisite. It deliberately does not remove fields or change ObjectHeader size; that atomic ABI/layout payoff stays in #8047 after this issue proves the replacement facts.
Concrete blockers on current main
1. ShapeId cannot recover the fields #8047 proposes to delete
crates/perry-runtime/src/object/shapes.rs still defines:
ShapeTable.entries: PtrHashMap<usize, Shape> keyed by the keys-array address;
Shape { indexed_len, shape_id, slots }, with no authoritative rooted keys edge and no by-id lookup;
shape_id_for_keys_ensure(keys, key_count), shape_keys_grown(old_keys, new_keys), shape_drop(keys), and the GC prune/rekey paths all address-keyed.
docs/shape-tree-plan.md says this explicitly: keys_array remains the ordered-key artifact and the transition cache remains separate. A header containing only { class_id, shape_id, meta } therefore has no way to enumerate keys or recover the live field/slot bound.
The ShapeId allocator also deliberately returns 0 on range exhaustion. Generated PICs then fall back to keys_array pointer tokens. Removing the pointer requires a sound exhausted-id behavior rather than silently deleting the fallback.
2. field_count is independently mutable, not keys_array.length
The publication/liveness invariant is load-bearing:
object/field_get_set/field_ops.rs::js_object_set_field widens field_countbefore a pointer store so object::gc_field_slot_range traces and rewrites the new slot;
the same pre-store widening exists in object/field_set_by_name.rs, field_set_by_name/{fast_paths,tail}.rs, and object_ops/keys_array.rs;
object/delete_rest.rs::js_object_delete_field compacts and shrinks it;
gc/layout.rs::{gc_child_slots,init_typed_shape_layout,with_shape_shared_descriptor} and gc/heap_snapshot.rs consume it;
by-index stores can grow it with no keys write, while class instances may carry fields with a null or separately-built keys array.
So a future shape descriptor needs an authoritative live-slot/count protocol and publication ordering. Replacing the load with keys_array.length is not equivalent.
3. Rung-3 keys-pointer consumers are still live
Generated IR still bakes the old layout and pointer identity in these families:
The hard-coded current user-header offsets are object_type @0, class_id @4, shape word @8, field_count @12, keys_array @16, meta @24, fields @32 on LP64. The inline allocator additionally writes them at raw allocation offsets +8/+16/+24 after the 8-byte GcHeader.
Runtime typed-feedback still deliberately preserves the pointer contract:
typed_feedback.rs::object_shape retains a class_id == 0 gate and returns keys_array for class instances;
typed_feedback/guards.rs::{method_direct_call_contract,class_field_get_contract,class_field_fast_contract,class_field_set_contract} compare shape_addr / (*obj).keys_array with expected_keys and bound slots with (*obj).field_count;
js_method_direct_shape_guard exports the live keys pointer.
The retained comments in typed_feedback.rs and changelog.d/7983-6759-rung1-uniform-shape-word.md name this as the unlanded “rung 3”. The old handoff's discriminating sabotage fixture is the wide non-numeric class case: delete the first field, then read/write a middle field whose old index remains below the post-delete count.
4. object_type is not currently a derivable constant
Ordinary objects use OBJECT_TYPE_REGULAR, but class-expression values are the same ObjectHeader layout with a mutable OBJECT_TYPE_CLASS marker:
is_class_object_ptr, field_set_by_name/write_helpers.rs, and field_get_set/get_field_by_name_tail.rs read it;
native_abi.rs, promise/then_probe.rs, typed_feedback/guards.rs, object get/set tails, and generated PIC guards use the regular-object check.
GcHeader.obj_type alone is also insufficient today because RegExpHeader is deliberately allocated as GC_TYPE_OBJECT; regex_header_has_magic is the extra discriminator in the GC/object paths. #8047 needs a proven replacement for the class-object marker and ordinary-vs-RegExp classification before removing the word.
The 298 raw member sites span 77 files. Separately, codegen has 34 object_header_size_bytes references; runtime/external crates have 67 size_of::<...ObjectHeader> references; perry-ffi/src/types.rs::ObjectHeader mirrors the public ABI and its runtime-link test asserts every current offset. Direct out-of-runtime layout readers include perry-ui-android/src/json.rs, perry-ext-ws/src/lib.rs, and perry-stdlib/src/worker_threads.rs.
The semantic migration should install an exact checked-in census/ratchet, because a broad textual count is a safe superset but cannot distinguish another struct's field_count from an ObjectHeader read.
Required design/implementation
Make a ShapeId resolve to an authoritative descriptor containing at least the rooted/moving ordered keys artifact and the logical/live slot facts needed by enumeration, bounds, GC tracing, and typed-layout lookup.
Define add/delete/defineProperty/descriptor/prototype transitions and the by-index publication protocol. A shared descriptor must never be mutated to describe only one sibling.
Preserve realm/thread isolation: ids are process-global today, tables are per-thread, class keys also cross module and worker boundaries.
Resolve ShapeId exhaustion without a removed-header pointer fallback.
Migrate every runtime/codegen guard above from keys identity/count loads to exact ShapeId/descriptor facts, including the entry-hoisted key roots and their ratchet tests.
Add adversarial delete/defineProperty/prototype/cross-module/realm tests and a checked-in source/IR census that rejects reintroduced old-offset or pointer-identity consumers.
Acceptance
A newborn instance and every runtime allocator has a usable authoritative ShapeId before publication, or takes an explicitly tested fail-closed path.
Adding, deleting, compacting, defining a descriptor, mutating a prototype, crossing a module boundary, and worker/realm transfer cannot make two different layouts compare equal.
GC mark/rewrite/evacuation can recover and rewrite keys plus the exact live field range without ObjectHeader.keys_array or .field_count.
The class-expression typeof/new/instanceof behavior survives without ObjectHeader.object_type.
No emitted IR uses header offsets 0/12/16 for the removed facts and no guard token is a keys pointer.
Summary
A source/IR/FFI audit of
mainat08173f8b4found that #8047's prerequisite is not complete. Rungs 1–2 madeObjectHeader.parent_class_ida uniform, birth-stamped shape word, but the planned rung 3 never landed:keys_arrayandfield_countare still authoritative mutable object state, several guards still compare the keys pointer directly, andobject_typestill carries the liveOBJECT_TYPE_CLASSsemantic.This issue is the missing prerequisite. It deliberately does not remove fields or change
ObjectHeadersize; that atomic ABI/layout payoff stays in #8047 after this issue proves the replacement facts.Concrete blockers on current
main1.
ShapeIdcannot recover the fields #8047 proposes to deletecrates/perry-runtime/src/object/shapes.rsstill defines:ShapeTable.entries: PtrHashMap<usize, Shape>keyed by the keys-array address;Shape { indexed_len, shape_id, slots }, with no authoritative rooted keys edge and no by-id lookup;shape_id_for_keys_ensure(keys, key_count),shape_keys_grown(old_keys, new_keys),shape_drop(keys), and the GC prune/rekey paths all address-keyed.docs/shape-tree-plan.mdsays this explicitly:keys_arrayremains the ordered-key artifact and the transition cache remains separate. A header containing only{ class_id, shape_id, meta }therefore has no way to enumerate keys or recover the live field/slot bound.The ShapeId allocator also deliberately returns
0on range exhaustion. Generated PICs then fall back tokeys_arraypointer tokens. Removing the pointer requires a sound exhausted-id behavior rather than silently deleting the fallback.2.
field_countis independently mutable, notkeys_array.lengthThe publication/liveness invariant is load-bearing:
object/field_get_set/field_ops.rs::js_object_set_fieldwidensfield_countbefore a pointer store soobject::gc_field_slot_rangetraces and rewrites the new slot;object/field_set_by_name.rs,field_set_by_name/{fast_paths,tail}.rs, andobject_ops/keys_array.rs;object/delete_rest.rs::js_object_delete_fieldcompacts and shrinks it;gc/layout.rs::{gc_child_slots,init_typed_shape_layout,with_shape_shared_descriptor}andgc/heap_snapshot.rsconsume it;So a future shape descriptor needs an authoritative live-slot/count protocol and publication ordering. Replacing the load with
keys_array.lengthis not equivalent.3. Rung-3 keys-pointer consumers are still live
Generated IR still bakes the old layout and pointer identity in these families:
perry-codegen/src/expr/class_field_inline_guard.rsemit_class_field_loop_preheader_checkemit_proven_shape_recheckemit_class_field_inline_precheckperry-codegen/src/expr/element_shape_guard.rs::emit_element_shape_field_loadperry-codegen/src/expr/property_get/generic_dispatch.rs::lower_generic_property_getperry-codegen/src/expr/proxy_reflect.rsperry-codegen/src/lower_call/new_alloc.rsperry-codegen/src/target_layout.rs::object_header_size_bytesThe hard-coded current user-header offsets are
object_type @0,class_id @4, shape word@8,field_count @12,keys_array @16,meta @24, fields@32on LP64. The inline allocator additionally writes them at raw allocation offsets+8/+16/+24after the 8-byteGcHeader.Runtime typed-feedback still deliberately preserves the pointer contract:
typed_feedback.rs::object_shaperetains aclass_id == 0gate and returnskeys_arrayfor class instances;typed_feedback/guards.rs::{method_direct_call_contract,class_field_get_contract,class_field_fast_contract,class_field_set_contract}compareshape_addr/(*obj).keys_arraywithexpected_keysand bound slots with(*obj).field_count;js_method_direct_shape_guardexports the live keys pointer.The retained comments in
typed_feedback.rsandchangelog.d/7983-6759-rung1-uniform-shape-word.mdname this as the unlanded “rung 3”. The old handoff's discriminating sabotage fixture is the wide non-numeric class case: delete the first field, then read/write a middle field whose old index remains below the post-delete count.4.
object_typeis not currently a derivable constantOrdinary objects use
OBJECT_TYPE_REGULAR, but class-expression values are the sameObjectHeaderlayout with a mutableOBJECT_TYPE_CLASSmarker:object/class_registry/parent_static.rs::js_object_mark_classwrites it;is_class_object_ptr,field_set_by_name/write_helpers.rs, andfield_get_set/get_field_by_name_tail.rsread it;native_abi.rs,promise/then_probe.rs,typed_feedback/guards.rs, object get/set tails, and generated PIC guards use the regular-object check.GcHeader.obj_typealone is also insufficient today becauseRegExpHeaderis deliberately allocated asGC_TYPE_OBJECT;regex_header_has_magicis the extra discriminator in the GC/object paths. #8047 needs a proven replacement for the class-object marker and ordinary-vs-RegExp classification before removing the word.Exact census snapshot
Reproducible textual census at
08173f8b4:Counts from those exact checks:
object_typefield_countkeys_arrayThe 298 raw member sites span 77 files. Separately, codegen has 34
object_header_size_bytesreferences; runtime/external crates have 67size_of::<...ObjectHeader>references;perry-ffi/src/types.rs::ObjectHeadermirrors the public ABI and its runtime-link test asserts every current offset. Direct out-of-runtime layout readers includeperry-ui-android/src/json.rs,perry-ext-ws/src/lib.rs, andperry-stdlib/src/worker_threads.rs.The semantic migration should install an exact checked-in census/ratchet, because a broad textual count is a safe superset but cannot distinguish another struct's
field_countfrom anObjectHeaderread.Required design/implementation
ShapeIdresolve to an authoritative descriptor containing at least the rooted/moving ordered keys artifact and the logical/live slot facts needed by enumeration, bounds, GC tracing, and typed-layout lookup.OBJECT_TYPE_CLASSand the regular/RegExp discriminator with a proven source outside the three words perf(object/GC): finish the common-object header shrink from 56 B to 40 B after shape-transition migration #8047 will delete.Acceptance
ObjectHeader.keys_arrayor.field_count.typeof/new/instanceofbehavior survives withoutObjectHeader.object_type.0/12/16for the removed facts and no guard token is a keys pointer.ObjectHeader; perf(object/GC): finish the common-object header shrink from 56 B to 40 B after shape-transition migration #8047 then performs the runtime/codegen/FFI layout change atomically.Refs #6759, #6798, #7916, #7981, #7983, #8009, #8010, #8047.