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}