Skip to main content

shape_value/v2/
heap_header.rs

1//! 8-byte heap header for all v2 heap-allocated objects.
2//!
3//! ## Memory layout (8 bytes)
4//!
5//! ```text
6//! Offset  Size  Field
7//! ------  ----  -----
8//!   0       4   refcount (AtomicU32)
9//!   4       2   kind (u16 — GC/debug/serialization, never hot-path dispatch)
10//!   6       1   flags (bitfield)
11//!   7       1   _pad
12//! ```
13//!
14//! Refcount is at offset 0 for fastest access — single-cycle load from base pointer.
15//! Compiled code never reads `kind`; it knows the concrete type at compile time.
16
17use std::sync::atomic::{AtomicU32, Ordering};
18
19// HeapHeader kind constants for v2 types.
20// These start at 80 to avoid collision with v1 HeapKind variants.
21pub const HEAP_KIND_V2_TYPED_ARRAY: u16 = 80;
22pub const HEAP_KIND_V2_STRING: u16 = 81;
23pub const HEAP_KIND_V2_TYPED_MAP: u16 = 82;
24pub const HEAP_KIND_V2_STRUCT: u16 = 83;
25/// Closure-spec Phase F: escape-fallback `TypedClosureHeader` allocation kind.
26/// A `MakeClosureHeap` opcode allocates a `TypedClosureHeader`-shaped block
27/// with this kind; Drop glue walks the closure's `heap_capture_mask` and
28/// releases each pointer capture. See `docs/v2-closure-specialization.md` §1.3
29/// and §5.3 for the full ABI.
30pub const HEAP_KIND_V2_CLOSURE: u16 = 84;
31/// R20 S2-prime-production: v2-raw `DecimalObj` carrier kind. The 24-byte
32/// `#[repr(C)]` struct (HeapHeader at offset 0 + inline `rust_decimal::Decimal`
33/// at offset 8) lives in `decimal_obj.rs`. Per ADR-006 §2.7.24 Q25.A SUPERSEDED
34/// + audit §4.1.D.1 — the on-header `kind` byte tags the v2-raw carrier; the
35/// `NativeKind::Ptr(HeapKind::Decimal)` label is unchanged at the slot ABI.
36pub const HEAP_KIND_V2_DECIMAL: u16 = 85;
37/// Wave 2 Agent D1 (2026-05-14): v2-raw `TypedObjectStorage` carrier kind.
38/// `TypedObjectStorage` grows a `HeapHeader` at offset 0 (per ADR-006 §2.3
39/// amendment and audit §4.3 Obstacle O-3.a resolution). The struct is
40/// `#[repr(C)]`; allocation goes through `TypedObjectStorage::_new` which
41/// returns `*mut TypedObjectStorage` with the header initialized via
42/// `HeapHeader::new(HEAP_KIND_V2_TYPED_OBJECT)`. Refcount discipline on the
43/// raw-pointer path goes through `v2_retain` / `v2_release` per the
44/// `HeapElement` trait. The `NativeKind::Ptr(HeapKind::TypedObject)` label
45/// is unchanged at the slot ABI. Next free post-`HEAP_KIND_V2_DECIMAL=85`.
46pub const HEAP_KIND_V2_TYPED_OBJECT: u16 = 86;
47/// Wave 2 Agent E (2026-05-14): v2-raw `TraitObjectStorage` carrier kind.
48/// `TraitObjectStorage` grows a `HeapHeader` at offset 0 (per ADR-006
49/// §Q25.C.5 amendment and audit §4.3 Obstacle O-3.a resolution). The struct
50/// is `#[repr(C)]`; allocation goes through `TraitObjectStorage::_new` which
51/// returns `*mut TraitObjectStorage` with the header initialized via
52/// `HeapHeader::new(HEAP_KIND_V2_TRAIT_OBJECT)`. Refcount discipline on the
53/// raw-pointer path goes through `v2_retain` / `v2_release` per the
54/// `HeapElement` trait. The `NativeKind::Ptr(HeapKind::TraitObject)` label
55/// is unchanged at the slot ABI. The struct continues to carry an inner
56/// `Arc<TypedObjectStorage>` value half + `Arc<VTable>` vtable half during
57/// Wave 2 Round 2 transition; D2's parallel close evaporates the inner
58/// `Arc<TypedObjectStorage>` wrapping. Next free post-`HEAP_KIND_V2_TYPED_OBJECT=86`.
59pub const HEAP_KIND_V2_TRAIT_OBJECT: u16 = 87;
60
61// Flag bits
62pub const FLAG_MARKED: u8 = 0x01;
63pub const FLAG_PINNED: u8 = 0x02;
64pub const FLAG_READONLY: u8 = 0x04;
65
66/// 8-byte header for all v2 heap-allocated objects.
67/// Refcount at offset 0 for fastest access.
68#[repr(C)]
69#[derive(Debug)]
70pub struct HeapHeader {
71    /// Atomic reference count (offset 0, 4 bytes).
72    pub refcount: AtomicU32,
73    /// Object kind for GC/debug/serialization (offset 4, 2 bytes).
74    /// Never used for hot-path type dispatch — compiled code knows the concrete type.
75    pub kind: u16,
76    /// Bitfield flags: FLAG_MARKED, FLAG_PINNED, FLAG_READONLY (offset 6, 1 byte).
77    pub flags: u8,
78    /// Padding to 8 bytes (offset 7, 1 byte).
79    pub _pad: u8,
80}
81
82impl HeapHeader {
83    /// Create a new HeapHeader with the given kind, refcount initialized to 1.
84    #[inline]
85    pub fn new(kind: u16) -> Self {
86        Self {
87            refcount: AtomicU32::new(1),
88            kind,
89            flags: 0,
90            _pad: 0,
91        }
92    }
93
94    /// Get the kind field.
95    #[inline]
96    pub fn kind(&self) -> u16 {
97        self.kind
98    }
99
100    /// Get the flags field.
101    #[inline]
102    pub fn flags(&self) -> u8 {
103        self.flags
104    }
105
106    /// Check if a flag is set.
107    #[inline]
108    pub fn has_flag(&self, flag: u8) -> bool {
109        self.flags & flag != 0
110    }
111
112    /// Set a flag.
113    #[inline]
114    pub fn set_flag(&mut self, flag: u8) {
115        self.flags |= flag;
116    }
117
118    /// Clear a flag.
119    #[inline]
120    pub fn clear_flag(&mut self, flag: u8) {
121        self.flags &= !flag;
122    }
123
124    /// Increment the reference count.
125    #[inline(always)]
126    pub fn retain(&self) {
127        self.refcount.fetch_add(1, Ordering::Relaxed);
128    }
129
130    /// Decrement the reference count. Returns `true` if the count reached zero
131    /// (caller must deallocate).
132    #[inline(always)]
133    pub fn release(&self) -> bool {
134        let old = self.refcount.fetch_sub(1, Ordering::Release);
135        if old == 1 {
136            std::sync::atomic::fence(Ordering::Acquire);
137            true
138        } else {
139            false
140        }
141    }
142
143    /// Get the current reference count (for debugging/testing).
144    #[inline]
145    pub fn get_refcount(&self) -> u32 {
146        self.refcount.load(Ordering::Relaxed)
147    }
148
149    /// Byte offset constants for JIT codegen.
150    pub const OFFSET_REFCOUNT: usize = 0;
151    pub const OFFSET_KIND: usize = 4;
152    pub const OFFSET_FLAGS: usize = 6;
153}
154
155// Compile-time size assertion.
156const _: () = {
157    assert!(std::mem::size_of::<HeapHeader>() == 8);
158};
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_size_of_heap_header() {
166        assert_eq!(std::mem::size_of::<HeapHeader>(), 8);
167    }
168
169    #[test]
170    fn test_field_offsets() {
171        let h = HeapHeader::new(HEAP_KIND_V2_TYPED_ARRAY);
172        let base = &h as *const _ as usize;
173
174        let refcount_offset = &h.refcount as *const _ as usize - base;
175        let kind_offset = &h.kind as *const _ as usize - base;
176        let flags_offset = &h.flags as *const _ as usize - base;
177        let pad_offset = &h._pad as *const _ as usize - base;
178
179        assert_eq!(refcount_offset, 0, "refcount must be at offset 0");
180        assert_eq!(kind_offset, 4, "kind must be at offset 4");
181        assert_eq!(flags_offset, 6, "flags must be at offset 6");
182        assert_eq!(pad_offset, 7, "_pad must be at offset 7");
183
184        // Verify against the declared constants
185        assert_eq!(refcount_offset, HeapHeader::OFFSET_REFCOUNT);
186        assert_eq!(kind_offset, HeapHeader::OFFSET_KIND);
187        assert_eq!(flags_offset, HeapHeader::OFFSET_FLAGS);
188    }
189
190    #[test]
191    fn test_new_initializes_refcount_to_one() {
192        let h = HeapHeader::new(HEAP_KIND_V2_STRING);
193        assert_eq!(h.get_refcount(), 1);
194        assert_eq!(h.kind(), HEAP_KIND_V2_STRING);
195        assert_eq!(h.flags(), 0);
196    }
197
198    #[test]
199    fn test_retain_increments_refcount() {
200        let h = HeapHeader::new(HEAP_KIND_V2_STRUCT);
201        assert_eq!(h.get_refcount(), 1);
202
203        h.retain();
204        assert_eq!(h.get_refcount(), 2);
205
206        h.retain();
207        assert_eq!(h.get_refcount(), 3);
208    }
209
210    #[test]
211    fn test_release_decrements_refcount() {
212        let h = HeapHeader::new(HEAP_KIND_V2_TYPED_MAP);
213        h.retain(); // refcount = 2
214        h.retain(); // refcount = 3
215
216        assert!(!h.release()); // 3 -> 2, not zero
217        assert_eq!(h.get_refcount(), 2);
218
219        assert!(!h.release()); // 2 -> 1, not zero
220        assert_eq!(h.get_refcount(), 1);
221
222        assert!(h.release()); // 1 -> 0, caller must dealloc
223    }
224
225    #[test]
226    fn test_flags_operations() {
227        let mut h = HeapHeader::new(HEAP_KIND_V2_TYPED_ARRAY);
228        assert!(!h.has_flag(FLAG_MARKED));
229        assert!(!h.has_flag(FLAG_PINNED));
230        assert!(!h.has_flag(FLAG_READONLY));
231
232        h.set_flag(FLAG_MARKED);
233        assert!(h.has_flag(FLAG_MARKED));
234        assert!(!h.has_flag(FLAG_PINNED));
235
236        h.set_flag(FLAG_PINNED);
237        assert!(h.has_flag(FLAG_MARKED));
238        assert!(h.has_flag(FLAG_PINNED));
239
240        h.clear_flag(FLAG_MARKED);
241        assert!(!h.has_flag(FLAG_MARKED));
242        assert!(h.has_flag(FLAG_PINNED));
243
244        h.set_flag(FLAG_READONLY);
245        assert!(h.has_flag(FLAG_PINNED));
246        assert!(h.has_flag(FLAG_READONLY));
247    }
248
249    #[test]
250    fn test_kind_constants_are_distinct() {
251        let kinds = [
252            HEAP_KIND_V2_TYPED_ARRAY,
253            HEAP_KIND_V2_STRING,
254            HEAP_KIND_V2_TYPED_MAP,
255            HEAP_KIND_V2_STRUCT,
256            HEAP_KIND_V2_CLOSURE,
257            HEAP_KIND_V2_DECIMAL,
258            HEAP_KIND_V2_TYPED_OBJECT,
259            HEAP_KIND_V2_TRAIT_OBJECT,
260        ];
261        for i in 0..kinds.len() {
262            for j in (i + 1)..kinds.len() {
263                assert_ne!(kinds[i], kinds[j], "kind constants must be unique");
264            }
265        }
266    }
267
268    #[test]
269    fn test_thread_safety_retain_release() {
270        use std::sync::Arc;
271
272        // Allocate the header on the heap so we can share it across threads.
273        let header = Arc::new(HeapHeader::new(HEAP_KIND_V2_TYPED_ARRAY));
274
275        // Start with refcount 1. We'll have 8 threads each do 1000 retain+release pairs,
276        // which should leave the refcount at 1 when done.
277        let threads: Vec<_> = (0..8)
278            .map(|_| {
279                let h = Arc::clone(&header);
280                std::thread::spawn(move || {
281                    for _ in 0..1000 {
282                        h.retain();
283                    }
284                    for _ in 0..1000 {
285                        h.release();
286                    }
287                })
288            })
289            .collect();
290
291        for t in threads {
292            t.join().unwrap();
293        }
294
295        // After all threads complete, refcount should be back to 1.
296        assert_eq!(header.get_refcount(), 1);
297    }
298
299    #[test]
300    fn test_release_returns_true_on_last_ref() {
301        let h = HeapHeader::new(HEAP_KIND_V2_STRING);
302        // refcount starts at 1
303        assert!(h.release()); // 1 -> 0, should signal dealloc
304    }
305
306    #[test]
307    fn test_multiple_retain_then_release_to_zero() {
308        let h = HeapHeader::new(HEAP_KIND_V2_STRUCT);
309        h.retain(); // 2
310        h.retain(); // 3
311        h.retain(); // 4
312
313        assert!(!h.release()); // 4 -> 3
314        assert!(!h.release()); // 3 -> 2
315        assert!(!h.release()); // 2 -> 1
316        assert!(h.release());  // 1 -> 0 => true
317    }
318}