Skip to main content

TypedObjectStorage

Struct TypedObjectStorage 

Source
#[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:

  1. HeapValue::TypedObject becomes HeapValue::TypedObject(Arc<TypedObjectStorage>) — a typed Arc<T> payload like every other ADR-006 §2.3 heap arm.
  2. The Drop impl (Step 5) lives on TypedObjectStorage and dispatches per-field on NativeKind from the embedded field_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_id is the registry key for the TypeSchema. Kept for wire / snapshot round-trip and downstream schema-aware code (printing, marshal); not consulted at drop time.
  • slots is a per-field 8-byte storage array. Field at index i stores its bits per the schema’s FieldType for that field.
  • heap_mask has bit i set iff slot i holds a heap pointer (Arc<T> raw pointer per ADR-006 §2.4). Bits beyond slots.len() must be zero.
  • field_kinds is an Arc<[NativeKind]> of length slots.len(), one entry per field, carrying the proven NativeKind for that field’s slot bits. The Arc payload is shared per-schema: the construction path (in shape-runtime) maps schema_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: HeapHeader

v2-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: u64

Registry 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: u64

Bit 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

Source

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):

  1. slots.len() == field_kinds.len() — one kind per slot.
  2. For each bit i set in heap_mask, field_kinds[i] must be a heap-pointer kind (NativeKind::String or NativeKind::Ptr(_)) and the slot’s u64 must be the raw pointer of an Arc::into_raw::<T> for the matching T. Drop relies on this for soundness.
  3. field_kinds should be the per-schema cached Arc<[NativeKind]> (callers maintain a schema_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).

Source

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).

Source

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).

Source

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:

  1. 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.
  2. No aliased &mut ValueSlot: callers must NOT mint a &mut ValueSlot to slot idx from any path while this write is in flight. The Q14 dispatch in op_deref_store / op_set_index_ref is the only caller, and it operates on &TypedObjectStorage exclusively.
  3. Kind invariance: new_kind must equal self.field_kinds[idx]. The Q14 RefTarget carries the projected slot’s kind at construction (MakeFieldRef sources it from field_type_tag); the post-proof §2.7.5.1 contract forbids mid-life kind changes for typed fields. The caller debug_asserts this before calling.
  4. heap_mask bit consistency: for heap-kinded slots (NativeKind::String or Ptr(HeapKind::_)), the corresponding heap_mask bit must already be set per the TypedObjectStorage::new construction-side contract, AND the prior bits must be a valid Arc::into_raw::<T> for the slot’s kind. The returned prior_bits is exactly that share; the caller releases it via drop_with_kind after 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

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for TypedObjectStorage

Source§

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 i where heap_mask >> i & 1 == 1, the slot’s u64 bits are the result of Arc::into_raw::<T> where T matches field_kinds[i] (per the per-HeapKind table in drop_fields).
  • NativeKind::Ptr(HeapKind::{Future, ModuleFn, Char, NativeScalar}) are inline-scalar payloads (no Arc<T>); a heap_mask bit set with one of those kinds is a soundness violation surfaced by debug_assert in drop_fields.
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl HeapElement for TypedObjectStorage

Source§

unsafe fn release_elem(ptr: *const Self)

Decrement the reference count of *ptr. If the refcount reaches zero, fully deallocate the object (including any nested payload buffers per the implementor’s drop semantics). Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,