#[repr(C)]pub struct TypedObjectStorage {
pub header: HeapHeader,
pub schema_id: u64,
pub slots: Box<[ValueSlot]>,
pub heap_mask: u64,
pub field_kinds: Arc<[NativeKind]>,
}Expand description
Schema-keyed object storage. Extracted from the inline
HeapValue::TypedObject { schema_id, slots, heap_mask } struct
variant per ADR-006 §2.3, so that:
HeapValue::TypedObjectbecomesHeapValue::TypedObject(Arc<TypedObjectStorage>)— a typedArc<T>payload like every other ADR-006 §2.3 heap arm.- The
Dropimpl (Step 5) lives onTypedObjectStorageand dispatches per-field onNativeKindfrom the embeddedfield_kinds: Arc<[NativeKind]>table — no schema-registry probe and no cross-crate function-pointer hook at drop time.
Field invariants (ADR-006 §2.3):
schema_idis the registry key for the TypeSchema. Kept for wire / snapshot round-trip and downstream schema-aware code (printing, marshal); not consulted at drop time.slotsis a per-field 8-byte storage array. Field at indexistores its bits per the schema’sFieldTypefor that field.heap_maskhas bitiset iff slotiholds a heap pointer (Arc<T>raw pointer per ADR-006 §2.4). Bits beyondslots.len()must be zero.field_kindsis anArc<[NativeKind]>of lengthslots.len(), one entry per field, carrying the provenNativeKindfor that field’s slot bits. The Arc payload is shared per-schema: the construction path (inshape-runtime) mapsschema_id ⇒ Arc<[NativeKind]>once per schema (one HashMap probe at the first construction; cached for subsequent constructions) and clones the cached Arc into each instance — so 1M Customer-objects of the same shape share one[NativeKind]allocation. Drop is then constant-time per slot without any cross-crate registry call.
Why the Arc<[NativeKind]> (Option B’ per supervisor ruling, ADR-006 §17):
- Option B (per-instance
Box<[NativeKind]>) was rejected: it duplicates the same NativeKind sequence across every instance of a schema (1M × 8 fields = 16MB cumulative duplication). - Option C (function-pointer hook in shape-value installed by shape-runtime) was rejected: it adds a cross-crate runtime hook for metadata that’s already known at construction time.
- Option B’ (this one) does the lookup once at construction (where the schema is in scope) and shares the result via Arc — 8-byte pointer per instance, single payload allocation per schema, no probe at drop. Per Q8 spirit: the schema lookup happens, but it’s profile-driven preempted to construction time and cached.
TypedObjectStorage is pub with pub fields so the existing
destructuring call sites can migrate by reading
storage.schema_id / storage.slots / storage.heap_mask. The
struct is intentionally not Clone — clone semantics belong to the
enclosing Arc<TypedObjectStorage> (one atomic refcount bump).
Wave 2 Agent D1 (2026-05-14): HeapHeader-equipped shape change.
Per audit §4.3 Obstacle O-3.a resolution + ADR-006 §2.3 amendment, the
struct now carries a HeapHeader at offset 0 (#[repr(C)]) so v2-raw
raw-pointer allocations (_new / _drop + impl HeapElement) can
dispatch refcount on the header via v2_retain / v2_release. Existing
Arc<TypedObjectStorage> construction sites continue to work
unchanged — Arc::new(TypedObjectStorage::new(...)) produces a Rust
Arc-wrapped instance whose embedded header sits at refcount=1 unused;
the dispatch arms continue to use Arc::increment_strong_count /
Arc::decrement_strong_count on those bits. The new _new-allocated
raw-pointer bits use the header’s refcount via the HeapElement trait.
Agent D2 (Wave 2 Round 2) migrates the 18 production construction sites
to the raw-pointer carrier; Agent E (Wave 2 Round 2) consumes the same
shape change for TraitObjectStorage. Until that migration completes,
both carriers coexist at the struct level; the slot-ABI discriminator
(NativeKind::Ptr(HeapKind::TypedObject)) is unchanged.
Fields§
§header: HeapHeaderv2-raw HeapHeader at offset 0 (8 bytes). Refcount/kind/flags.
Initialized to HeapHeader::new(HEAP_KIND_V2_TYPED_OBJECT) by
_new; for Arc-wrapped instances allocated via
TypedObjectStorage::new the header sits at refcount=1 unused
(the enclosing Arc owns the lifecycle). See struct docstring.
schema_id: u64Registry key for the TypeSchema describing each slot’s FieldType.
slots: Box<[ValueSlot]>Per-field 8-byte storage. Length matches the schema’s field count.
heap_mask: u64Bit i set ⇔ slot i holds a heap pointer that participates in
Arc refcount discipline. Bits beyond slots.len() must be zero.
field_kinds: Arc<[NativeKind]>Per-field NativeKind table — same length as slots. Shared
per-schema via Arc: every instance of the same schema clones
the same payload (one atomic refcount bump per construction).
Consulted by Drop to dispatch per-slot Arc::decrement_strong_count
without any schema-registry probe.
Implementations§
Source§impl TypedObjectStorage
impl TypedObjectStorage
Sourcepub fn new(
schema_id: u64,
slots: Box<[ValueSlot]>,
heap_mask: u64,
field_kinds: Arc<[NativeKind]>,
) -> Self
pub fn new( schema_id: u64, slots: Box<[ValueSlot]>, heap_mask: u64, field_kinds: Arc<[NativeKind]>, ) -> Self
Construct a new TypedObjectStorage.
Construction-side contract (callers in shape-runtime):
slots.len() == field_kinds.len()— one kind per slot.- For each bit
iset inheap_mask,field_kinds[i]must be a heap-pointer kind (NativeKind::StringorNativeKind::Ptr(_)) and the slot’su64must be the raw pointer of anArc::into_raw::<T>for the matchingT. Drop relies on this for soundness. field_kindsshould be the per-schema cachedArc<[NativeKind]>(callers maintain aschema_id ⇒ Arc<[NativeKind]>cache to avoid per-instance allocation).
Returns the storage by value; the canonical wrap is
Arc::new(TypedObjectStorage::new(...)) immediately followed by
HeapValue::TypedObject(arc) or ValueSlot::from_typed_object(arc).
Sourcepub fn _new(
schema_id: u64,
slots: Box<[ValueSlot]>,
heap_mask: u64,
field_kinds: Arc<[NativeKind]>,
) -> *mut Self
pub fn _new( schema_id: u64, slots: Box<[ValueSlot]>, heap_mask: u64, field_kinds: Arc<[NativeKind]>, ) -> *mut Self
Wave 2 Agent D1 (2026-05-14): v2-raw raw-pointer allocator.
Allocates a new TypedObjectStorage on the heap and returns a raw
pointer with refcount initialized to 1. Mirrors the DecimalObj::new
/ StringObj::new precedents at crates/shape-value/src/v2/ —
#[repr(C)] struct with HeapHeader at offset 0; refcount discipline
goes through v2_retain / v2_release via the HeapElement trait.
Construction-side contract: same as new() — slots.len() == field_kinds.len(); heap-mask bits correspond to heap-kinded slots
whose bits are Arc::into_raw::<T> for the matching T. The raw-
pointer carrier owns one strong-count share for every heap-kinded
slot it carries, retired by _drop at refcount=0.
Callers (Wave 2 Agent D2, Round 2 cascade): construct via
TypedObjectStorage::_new(...) and store the pointer in
ValueSlot::from_typed_object_raw(ptr). Drop runs at refcount=0
via the HeapElement::release_elem trait method, NOT via Rust
Arc::drop (the Arc<TypedObjectStorage> path is the legacy
transitional carrier; both coexist at the struct level per the
struct docstring).
Sourcepub unsafe fn _drop(ptr: *mut Self)
pub unsafe fn _drop(ptr: *mut Self)
Wave 2 Agent D1 (2026-05-14): v2-raw raw-pointer deallocator.
Runs the per-field heap-mask walk (releasing one strong-count share
per heap-kinded slot via Arc::decrement_strong_count) and then
deallocates the struct’s heap memory via Layout::new::<Self>().
The field walk delegates to drop_fields so the same logic powers
both the legacy impl Drop for TypedObjectStorage path (used by
Arc<TypedObjectStorage> instances) and this raw-pointer path.
Mirrors the DecimalObj::drop / StringObj::drop precedents.
§Safety
ptr must point to a live TypedObjectStorage allocated via
Self::_new with no remaining references. Must not be called more
than once on the same pointer; must not be called on
Arc<TypedObjectStorage>-allocated instances (those run through
Rust’s Arc drop machinery + impl Drop for TypedObjectStorage).
Sourcepub unsafe fn write_slot_in_place(&self, idx: usize, new_bits: u64) -> u64
pub unsafe fn write_slot_in_place(&self, idx: usize, new_bits: u64) -> u64
In-place write of slot idx through a shared &TypedObjectStorage
(i.e. through an Arc<TypedObjectStorage> with refcount > 1).
Returns the prior (bits, kind) so the caller can run
drop_with_kind on the released share. The caller transfers
ownership of new_bits (one strong-count share for heap kinds) to
the slot.
This is the Q14 / ADR-006 §2.7.13 in-place write path for
RefTarget::TypedField projection writes — the receiver Arc
is shared between the ref carrier and the originating binding,
so Arc::get_mut / Arc::make_mut cannot apply (refcount > 1
by construction, and TypedObjectStorage is intentionally not
Clone per the §2.5 documentation). The Box<[ValueSlot]>
inside the storage is logically owned; the single-word u64
inside each ValueSlot is written atomically (single-word
aligned store on every supported architecture).
§Safety
Callers must guarantee:
- Single-threaded write: the VM is single-threaded, and the
refs that drive this path are constrained by the §3.1
ref-escape analysis to stay within their originating task
(refs cannot cross task boundaries — error B0014
NonSendableAcrossTaskBoundary). No other thread may hold an&Arc<TypedObjectStorage>to the same storage at the same time the write executes. - No aliased
&mut ValueSlot: callers must NOT mint a&mut ValueSlotto slotidxfrom any path while this write is in flight. The Q14 dispatch inop_deref_store/op_set_index_refis the only caller, and it operates on&TypedObjectStorageexclusively. - Kind invariance:
new_kindmust equalself.field_kinds[idx]. The Q14 RefTarget carries the projected slot’s kind at construction (MakeFieldRefsources it fromfield_type_tag); the post-proof§2.7.5.1contract forbids mid-life kind changes for typed fields. The caller debug_asserts this before calling. heap_maskbit consistency: for heap-kinded slots (NativeKind::String or Ptr(HeapKind::_)), the correspondingheap_maskbit must already be set per theTypedObjectStorage::newconstruction-side contract, AND the prior bits must be a validArc::into_raw::<T>for the slot’s kind. The returnedprior_bitsis exactly that share; the caller releases it viadrop_with_kindafter running the post-write barrier.
Q14 / ADR-006 §2.7.13. Mirror of the clone_with_kind /
drop_with_kind symmetry used by RefTarget::Local and
RefTarget::ModuleBinding writes (stack_write_kinded and
module_binding_write_kinded already encapsulate this pattern
for non-projected places; this is the projected-place mirror).
Trait Implementations§
Source§impl Debug for TypedObjectStorage
impl Debug for TypedObjectStorage
Source§impl Drop for TypedObjectStorage
impl Drop for TypedObjectStorage
Source§fn drop(&mut self)
fn drop(&mut self)
ADR-006 §2.5 + Wave 2 Agent D1 (2026-05-14): delegates to the shared
drop_fields helper that walks heap_mask and dispatches per-slot
on field_kinds[i]. The same helper powers _drop (raw-pointer
path) so both the legacy Arc<TypedObjectStorage> lifecycle and
the v2-raw raw-pointer lifecycle retire heap-slot Arc shares with
identical semantics.
Soundness contract (must hold by construction; see
TypedObjectStorage::new / _new):
- For every
iwhereheap_mask >> i & 1 == 1, the slot’su64bits are the result ofArc::into_raw::<T>whereTmatchesfield_kinds[i](per the per-HeapKind table indrop_fields). NativeKind::Ptr(HeapKind::{Future, ModuleFn, Char, NativeScalar})are inline-scalar payloads (noArc<T>); a heap_mask bit set with one of those kinds is a soundness violation surfaced by debug_assert indrop_fields.
Source§impl HeapElement for TypedObjectStorage
impl HeapElement for TypedObjectStorage
Source§unsafe fn release_elem(ptr: *const Self)
unsafe fn release_elem(ptr: *const Self)
*ptr. If the refcount reaches
zero, fully deallocate the object (including any nested payload
buffers per the implementor’s drop semantics). Read more