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
Remove ObjectHeader::object_type and field_count, shrinking a common two-slot object from 56 B to 48 B and the eight-slot case from 104 B to 96 B. This is an independent rung of #8047 that needs none of the GC descriptor-rooting work in #8112.
Why this is a separate, landable rung
Measured with rustc -O on the exact #[repr(C)] shapes (LP64):
Neither field alone buys anything — the struct re-pads. Together they are a clean 8 bytes, exactly half of #8047's measured prize, and they do not touch the keys edge that #8112 has to redesign.
Work
1. Replace the seven raw-offset-0 Error discriminators
ObjectHeader.object_type is prefix-punned against error::ErrorHeader, whose own first u32 is also object_type. These read raw offset 0 on an untyped pointer:
Delete the word and offset 0 becomes class_id; an ordinary object whose class_id equals OBJECT_TYPE_ERROR is then read at ErrorHeader's offsets. ErrorHeader is already allocated GC_TYPE_ERROR (error.rs:198-203), so GcHeader.obj_type is the replacement — the same move #8086 made for object_is_regular.
2. proxy.rs:1523 needs the descriptor kind, not object_is_regular
gates plan_eligible. It deliberately excludes OBJECT_TYPE_CLASS, and the comment above it records what breaks otherwise (#6595 — bundled zod's ZodX.create vanishing from ClassRef static dispatch). object_is_regular is true for class objects, so it is not a valid substitution. Use object_kind == ShapeObjectKind::Ordinary.
3. Make the live-slot publication atomic
object/mod.rs:1909 set_object_live_slot_count clears the stamp, writes the header word, then re-mints. Consumers falling back to the already-widened header word is what makes that window safe today, and synchronize_object_shape_descriptor_from allocates (HashMap insert), so a collection can land inside it. With no header word, a collection there sees the old live bound and the newly exposed slot is invisible to tracing and rewriting — a fresh #7154/#7164. Restructure to mint-then-stamp.
4. Give the two capacity consumers a real fact
field_get_set/field_ops.rs:127 computes alloc_limit = max((*obj).field_count, INLINE_SLOT_FLOOR) for its OOB bound and says so ("a generous limit … to avoid false positives"). dyn_eval/env.rs uses the same idiom. Both want physical capacity, which field_count only approximates.
5. Codegen
lower_call/new_alloc.rs — the inline new writes object_type at raw+8 and field_count packed at raw+16; collapse to one class_id ‖ shape_id store. Also fix the three stale comments there (:250-254 says the header is 24 bytes without meta; :606 says slots start at raw+32 when it is raw+40; :380-383 says the ILP32 header is 20 when it is 24).
Renumber the 11 hard-coded IR offsets from class_id @+4 / ShapeId @+8 to +0/+4: class_field_inline_guard.rs:279,283,400,495,499; element_shape_guard.rs:359; generic_dispatch.rs:389; proxy_reflect.rs:550,557,867,872. GcHeader-relative offsets (-8/-7/-6) are unaffected.
The ~21 opaque object_header_size_bytes header-skip geps need no edit.
perry-ffi/src/types.rs:37-53 mirror + the offset_of! asserts at :149-175.
perry-ext-ws/src/lib.rs:847 (let n = (*ptr).field_count;) — needs a C accessor or js_object_keys + js_array_length.
perry-stdlib/src/worker_threads.rs:677-683 — structured-clone walk. worker_options.rs:114 already models the replacement, but the keys_array.is_null() branch is deliberate (class instances take the field_count arm); verify js_object_keys returns the same set for class_id != 0 before collapsing, since it filters private #x keys.
perry-ui-android/src/json.rs:487,490,496,598 — likely just delete the module: every function is private with no callers and its own trailing comment says js_json_* now lives in perry-runtime/json.rs.
scripts/raw_handle_debt_files.txt:115 and scripts/addr_class_ratchet_baseline.txt:138,257 — an entry matching nothing fails, and addr_class_inventory.py is a required lint step.
perry-ffi's object_header_matches_runtime has never executed — it is #[cfg(all(test, feature = "runtime-link"))] and runtime-link is enabled nowhere in .github/. Field deletion still goes red via rustc-warnings, but a size/padding divergence is invisible. Wire it up as part of this change.
perry-ffi is published to crates.io — an out-of-tree wrapper on the old mirror linked against a new runtime reads class_id out of the deleted object_type slot with no compile error. Needs a deliberate semver decision.
Docs: docs/src/platforms/watchos.md:35,106-111 is the only user-facing statement of the LP64/ILP32 pair. Four codegen doc comments already say "24 on 64-bit, 20 on ILP32" and have been wrong since meta landed (property_get.rs:1724, generic_dispatch.rs:435, property_set.rs:1443, lower_call/new.rs:693).
Acceptance
size_of::<ObjectHeader>() == 24 (LP64); {a,b} totals 48 B including GcHeader and two slots; the wide case reaches 96 B. Report both.
Forced evacuation, evacuation verification and protect-fromspace canaries pass with the moving collector demonstrably live — assert copied_objects > 0 or promoted_objects > 0, not merely that nothing threw.
The 19-program corpus is byte-exact and exit-checked; report instructions / cycles / RSS / copying-minor counts rather than extrapolating from the padding probe.
Summary
Remove
ObjectHeader::object_typeandfield_count, shrinking a common two-slot object from 56 B to 48 B and the eight-slot case from 104 B to 96 B. This is an independent rung of #8047 that needs none of the GC descriptor-rooting work in #8112.Why this is a separate, landable rung
Measured with
rustc -Oon the exact#[repr(C)]shapes (LP64):size_of::<ObjectHeader>(){a,b}object_typeonlyfield_countonlykeys_arraytoo (#8047)Neither field alone buys anything — the struct re-pads. Together they are a clean 8 bytes, exactly half of #8047's measured prize, and they do not touch the keys edge that #8112 has to redesign.
Work
1. Replace the seven raw-offset-0 Error discriminators
ObjectHeader.object_typeis prefix-punned againsterror::ErrorHeader, whose own firstu32is alsoobject_type. These read raw offset 0 on an untyped pointer:error.rs:750(js_error_is_error),error.rs:1542(gates(*error).errors)exception.rs:452,exception.rs:492(print_uncaught)value/dynamic_object.rs:531,548and:728,731Delete the word and offset 0 becomes
class_id; an ordinary object whoseclass_idequalsOBJECT_TYPE_ERRORis then read atErrorHeader's offsets.ErrorHeaderis already allocatedGC_TYPE_ERROR(error.rs:198-203), soGcHeader.obj_typeis the replacement — the same move #8086 made forobject_is_regular.2.
proxy.rs:1523needs the descriptor kind, notobject_is_regulargates
plan_eligible. It deliberately excludesOBJECT_TYPE_CLASS, and the comment above it records what breaks otherwise (#6595 — bundled zod'sZodX.createvanishing from ClassRef static dispatch).object_is_regularis true for class objects, so it is not a valid substitution. Useobject_kind == ShapeObjectKind::Ordinary.3. Make the live-slot publication atomic
object/mod.rs:1909 set_object_live_slot_countclears the stamp, writes the header word, then re-mints. Consumers falling back to the already-widened header word is what makes that window safe today, andsynchronize_object_shape_descriptor_fromallocates (HashMap insert), so a collection can land inside it. With no header word, a collection there sees the old live bound and the newly exposed slot is invisible to tracing and rewriting — a fresh #7154/#7164. Restructure to mint-then-stamp.4. Give the two capacity consumers a real fact
field_get_set/field_ops.rs:127computesalloc_limit = max((*obj).field_count, INLINE_SLOT_FLOOR)for its OOB bound and says so ("a generous limit … to avoid false positives").dyn_eval/env.rsuses the same idiom. Both want physical capacity, whichfield_countonly approximates.5. Codegen
lower_call/new_alloc.rs— the inlinenewwritesobject_typeatraw+8andfield_countpacked atraw+16; collapse to oneclass_id ‖ shape_idstore. Also fix the three stale comments there (:250-254says the header is 24 bytes withoutmeta;:606says slots start atraw+32when it israw+40;:380-383says the ILP32 header is 20 when it is 24).class_id @+4/ ShapeId@+8to+0/+4:class_field_inline_guard.rs:279,283,400,495,499;element_shape_guard.rs:359;generic_dispatch.rs:389;proxy_reflect.rs:550,557,867,872. GcHeader-relative offsets (-8/-7/-6) are unaffected.object_header_size_bytesheader-skip geps need no edit.expr/proxy_reflect.rs:922andstmt/loops.rs:3207divide the header size by 8 for a word index.24 / 8is exact; the new ILP32 header is 16 (see below) so16 / 8stays exact — but convert them to byte geps anyway, since perf(object/GC): finish the common-object header shrink from 56 B to 40 B after shape-transition migration #8047 makes the value 12 and12 / 8 == 1silently. Note a{u32, u32, *mut}ILP32 header is 12 bytes with align 4, which would put 8-byte JSValue slots at a 4-aligned offset and violate thei64:64arm64_32 ABInew_alloc.rs:588-591warns about; this rung keepskeys_array, so ILP32 stays 16 and the hazard is deferred to perf(object/GC): finish the common-object header shrink from 56 B to 40 B after shape-transition migration #8047.6. FFI and out-of-runtime
perry-ffi/src/types.rs:37-53mirror + theoffset_of!asserts at:149-175.perry-ext-ws/src/lib.rs:847(let n = (*ptr).field_count;) — needs a C accessor orjs_object_keys+js_array_length.perry-stdlib/src/worker_threads.rs:677-683— structured-clone walk.worker_options.rs:114already models the replacement, but thekeys_array.is_null()branch is deliberate (class instances take thefield_countarm); verifyjs_object_keysreturns the same set forclass_id != 0before collapsing, since it filters private#xkeys.perry-ui-android/src/json.rs:487,490,496,598— likely just delete the module: every function is private with no callers and its own trailing comment saysjs_json_*now lives inperry-runtime/json.rs.7. Gates
scripts/shape_descriptor_census.py'sFIELDStuple, self-test fixture, sabotage fixture, and the baseline JSON. (ci(object): make the ObjectHeader shape-descriptor census a real gate #8110 wires this script intolint; land that first.)scripts/raw_handle_debt_files.txt:115andscripts/addr_class_ratchet_baseline.txt:138,257— an entry matching nothing fails, andaddr_class_inventory.pyis a requiredlintstep.perry-ffi'sobject_header_matches_runtimehas never executed — it is#[cfg(all(test, feature = "runtime-link"))]andruntime-linkis enabled nowhere in.github/. Field deletion still goes red viarustc-warnings, but a size/padding divergence is invisible. Wire it up as part of this change.perry-ffiis published to crates.io — an out-of-tree wrapper on the old mirror linked against a new runtime readsclass_idout of the deletedobject_typeslot with no compile error. Needs a deliberate semver decision.docs/src/platforms/watchos.md:35,106-111is the only user-facing statement of the LP64/ILP32 pair. Four codegen doc comments already say "24 on 64-bit, 20 on ILP32" and have been wrong sincemetalanded (property_get.rs:1724,generic_dispatch.rs:435,property_set.rs:1443,lower_call/new.rs:693).Acceptance
size_of::<ObjectHeader>() == 24(LP64);{a,b}totals 48 B includingGcHeaderand two slots; the wide case reaches 96 B. Report both.GcHeaderstays 8 bytes.copied_objects > 0orpromoted_objects > 0, not merely that nothing threw.Refs #8047, #8067, #8086, #8110, #8112, #6595, #7154, #7164, #7916.