Skip to main content

praxis_runtime/
gc.rs

1//! The uniform object reference type and its header.
2//!
3//! Every runtime language value — `Int`, `Bool`, a record, a vector element —
4//! is a [`GcRef`] (§4.3, §11.1). The reference is a non-null pointer to a
5//! [`GcHeader`]; generated code treats it as opaque and passes it by value.
6//!
7//! `GcRef` is `#[repr(transparent)]` over `NonNull<GcHeader>`, which is itself
8//! pointer-representable, so it is FFI-safe and matches the calling convention
9//! in §10.3.
10//!
11//! See §12.2 for the conceptual header layout. The concrete fields here
12//! (ADR-011, as amended by ADR-039, ADR-103 and ADR-109) are a typed descriptor
13//! pointer, the offset the allocator laid the payload at, and the owning heap's
14//! identity. **A field every object pays for must be a field something reads**,
15//! which is why two obvious ones are absent: the mark colour is a bit in the
16//! object's page ([`crate::page`]), because a per-object colour byte costs a
17//! random-access store per surviving object per collection, and the payload size
18//! is nowhere at all, because the descriptor answers the size question for
19//! anyone who asks it.
20
21use std::cell::Cell;
22use std::num::NonZeroU32;
23use std::ptr::NonNull;
24use std::sync::atomic::{AtomicU32, Ordering};
25
26use crate::descriptor::TypeDescriptor;
27
28/// The identity of the heap that owns an allocation.
29///
30/// Every [`Heap`](crate::Heap) mints one at construction (and a fresh one at
31/// `reset`), and every header it allocates carries it. That makes "is this
32/// object mine?" an O(1) test the collector can run *before* it dereferences
33/// anything the header points at — which is what lets `Heap::mark` reject a
34/// root belonging to another heap, or a header the sweep has already poisoned.
35///
36/// `NonZeroU32` because 0 is reserved as the poisoned/unowned encoding in the
37/// header's `heap_id` field.
38#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
39pub struct HeapId(NonZeroU32);
40
41impl HeapId {
42    /// Mint a fresh, process-unique identity.
43    ///
44    /// # Panics
45    /// Panics after `u32::MAX - 1` heaps have been created in one process,
46    /// which no real program reaches (it would require minting one heap per
47    /// microsecond for over an hour).
48    pub(crate) fn mint() -> HeapId {
49        static NEXT: AtomicU32 = AtomicU32::new(1);
50        let raw = NEXT.fetch_add(1, Ordering::Relaxed);
51        HeapId(NonZeroU32::new(raw).expect("HeapId space exhausted"))
52    }
53
54    /// The raw value stored in a header. Never 0.
55    #[inline]
56    pub const fn get(self) -> u32 {
57        self.0.get()
58    }
59}
60
61/// Header prepended to every GC allocation (§12.2).
62///
63/// Layout is `#[repr(C)]` and the payload follows this header in the same
64/// allocation, at [`GcHeader::payload_offset_for`] bytes from the header's
65/// address — *not* necessarily at `size_of::<GcHeader>()`, because an
66/// over-aligned payload is padded forward. The header is addressable as
67/// `*mut GcHeader` and the payload is reached via [`GcHeader::payload`].
68///
69/// The fields are private: the allocator ([`Heap::alloc_raw`](crate::Heap)) is
70/// the only constructor, so an initialized header is the only kind that exists,
71/// and `payload_offset` cannot disagree with the address the allocator handed
72/// to the payload initializer.
73///
74/// **Sixteen bytes, and every field in them has a reader on a hot path**
75/// (ADR-109). This prefixes every allocation in the language, so a field here is
76/// a tax on every object a program makes — and once `#[repr(C)]` padding is
77/// counted, a four-byte field costs eight. Adding a field here is not a local
78/// decision — it moves `page::MIN_BLOCK`, the whole size-class ladder, and the
79/// immediate generated code folds to reach a payload, so it owes an ABI bump
80/// and an ADR.
81#[repr(C)]
82pub struct GcHeader {
83    /// The descriptor that centralizes every payload-aware operation (§11.4).
84    /// Stored as a typed pointer so the header's layout does not depend on the
85    /// descriptor's definition, yet access is type-safe.
86    ///
87    /// Null means **poisoned**: the storage has been swept and its payload
88    /// finalized. `Cell` so `poison` can run through a shared reference during
89    /// the sweep, which reaches every block through a `&PageHeader`.
90    descriptor: Cell<*const TypeDescriptor>,
91    /// Distance in bytes from this header's address to its payload's. **The
92    /// single layout authority** — written by the allocator from the same
93    /// calculation that produced the address it initialized, and read by
94    /// [`GcHeader::payload`], by the collector, and by generated code.
95    payload_offset: u16,
96    /// Which heap owns this allocation ([`HeapId`]). 0 means poisoned/unowned.
97    /// `Cell` for the same reason as `descriptor`.
98    ///
99    /// The page carries the same id, and could answer for it — but this copy is
100    /// what the mark phase reads *first*, and reading it first is what makes
101    /// masking the address to find the page sound at all (ADR-103): only a
102    /// header this heap allocated carries this heap's id, and every header this
103    /// heap allocated is inside one of its pages.
104    heap_id: Cell<u32>,
105}
106
107impl GcHeader {
108    /// Where the descriptor pointer sits, relative to the header's address.
109    ///
110    /// Generated code reads it: an `Inst::ExtractScalar` proves the object's
111    /// type inline (ADR-102) — one load from here, one compare against the
112    /// scalar descriptor's address — rather than calling `praxis_int_load` and
113    /// letting the wrapper prove it. The check is what makes the folded payload
114    /// offset below the offset the allocator actually used, and it is what keeps
115    /// a `praxis check`-clean program extracting an `Int` from a `Unit` a
116    /// refusal rather than an out-of-bounds read.
117    ///
118    /// Exported from here, derived with `offset_of!`, because ADR-039 decision 1
119    /// made the fields **private** to this module: the backend cannot reach for
120    /// the offset itself, and the alternative — writing `0` in the backend —
121    /// is exactly the re-derived literal that decision exists to prevent.
122    /// [`payload_offset_for`](Self::payload_offset_for) is the same idea one
123    /// step further along.
124    pub const DESCRIPTOR_OFFSET: usize = core::mem::offset_of!(GcHeader, descriptor);
125
126    /// Where the recorded payload displacement sits, relative to the header's
127    /// address (ADR-119).
128    ///
129    /// Read by nothing in generated code and **written** by one thing: the
130    /// inline claim sequence, which lays out a header itself. It is a `u16`, and
131    /// what it must be handed is what [`GcHeader::payload_offset_for`] answered
132    /// for the descriptor being stored beside it — the same value
133    /// `Heap::occupy` writes, from the same call. ADR-039 decision 1 is still
134    /// the authority; this is a second transcription of its answer, which is why
135    /// [`InlineClaimSite`](crate::InlineClaimSite) carries the offset and the
136    /// value together rather than letting a caller pair them.
137    pub const PAYLOAD_OFFSET_FIELD_OFFSET: usize = core::mem::offset_of!(GcHeader, payload_offset);
138
139    /// Where the owning [`HeapId`] sits, relative to the header's address
140    /// (ADR-119).
141    ///
142    /// The provenance word ADR-039 decision 2 made the mark phase's first read.
143    /// Generated code writes it — with the id it loaded out of the live `Heap`
144    /// it claimed the block from, never a compile-time constant: there is no
145    /// heap at compile time, and a debugger session replaces its `Jit` while
146    /// keeping its `Runtime` ([`crate::GcConst`]'s reason, one field along).
147    pub const HEAP_ID_OFFSET: usize = core::mem::offset_of!(GcHeader, heap_id);
148
149    /// Where the payload begins, relative to the header's address, for a
150    /// payload with the given alignment.
151    ///
152    /// This is **the** object-layout calculation: `Heap::alloc_raw` uses it to
153    /// place the payload, `payload_offset` records what it returned, and
154    /// generated code calls it to reach a payload directly. `const` so codegen
155    /// can fold it into an immediate.
156    ///
157    /// # Panics
158    /// Panics if `payload_align` is not a power of two.
159    #[inline]
160    pub const fn payload_offset_for(payload_align: usize) -> usize {
161        assert!(
162            payload_align.is_power_of_two(),
163            "payload alignment must be a power of two"
164        );
165        round_up(std::mem::size_of::<GcHeader>(), payload_align)
166    }
167
168    /// Construct an initialized header. Only the allocator calls this.
169    #[inline]
170    pub(crate) fn new(
171        descriptor: &'static TypeDescriptor,
172        payload_offset: u16,
173        heap_id: HeapId,
174    ) -> GcHeader {
175        GcHeader {
176            descriptor: Cell::new(descriptor as *const TypeDescriptor),
177            payload_offset,
178            heap_id: Cell::new(heap_id.get()),
179        }
180    }
181
182    /// The descriptor describing this object's payload (§11.4).
183    ///
184    /// Descriptors are always `'static` (built-in constants or compiler-emitted
185    /// statics), so the returned lifetime is unconstrained.
186    ///
187    /// # Panics
188    /// Panics if the header has been poisoned by the sweep. Callers that may
189    /// hold a stale reference must check [`GcHeader::is_poisoned`] first; the
190    /// collector does this via [`GcHeader::heap_id`].
191    #[inline]
192    pub fn descriptor(&self) -> &'static TypeDescriptor {
193        let ptr = self.descriptor.get();
194        assert!(
195            !ptr.is_null(),
196            "descriptor read from a poisoned (swept) GcHeader"
197        );
198        // SAFETY: every live `GcHeader` is allocated with a descriptor pointer
199        // that points at a `'static TypeDescriptor`. The allocator is the only
200        // constructor of headers, and it upholds this; the null case — the only
201        // other value the field ever holds — is rejected above.
202        unsafe { &*ptr }
203    }
204
205    /// Pointer to this header's payload bytes.
206    ///
207    /// The caller is responsible for knowing the payload type (via the
208    /// descriptor); this is the low-level escape hatch used by descriptor
209    /// callbacks and typed accessors.
210    #[inline]
211    pub fn payload<T>(&self) -> *mut T {
212        // SAFETY: the payload lives `payload_offset` bytes into the same
213        // allocation, at the exact address the allocator initialized. This is a
214        // raw pointer calculation; dereferencing safely is the caller's job.
215        let header_ptr = self as *const GcHeader as *mut u8;
216        unsafe { header_ptr.add(self.payload_offset as usize) as *mut T }
217    }
218
219    /// The heap that owns this allocation, or `None` if the header is poisoned.
220    #[inline]
221    pub fn heap_id(&self) -> Option<HeapId> {
222        NonZeroU32::new(self.heap_id.get()).map(HeapId)
223    }
224
225    /// Whether this header's storage has been swept.
226    ///
227    /// A poisoned header is not an object: its payload has been finalized and
228    /// its bytes may be reused. Reading anything but this predicate off it is a
229    /// bug.
230    ///
231    /// **"May be reused" is why this predicate has a shelf life.** It answers
232    /// "has this block been reclaimed" only until the allocator reissues the
233    /// block and writes a fresh header over the poison. The collector's weak
234    /// arm ([`crate::debug::DebugFrameStackHeader::clear_reclaimed`], ADR-106)
235    /// is the one caller that depends on that, and it runs inside the
236    /// collection — after the sweep and before any allocation — for exactly
237    /// this reason.
238    #[inline]
239    pub fn is_poisoned(&self) -> bool {
240        self.descriptor.get().is_null()
241    }
242
243    /// Mark this header's storage as reclaimed: no descriptor, no owning heap.
244    ///
245    /// Called by the sweep *after* finalizing the payload and before the block's
246    /// `allocated` bit is cleared, so a stale `GcRef` that reaches it afterwards
247    /// is rejected by [`GcHeader::heap_id`] instead of being traced through
248    /// freed storage.
249    #[inline]
250    pub(crate) fn poison(&self) {
251        self.descriptor.set(std::ptr::null());
252        self.heap_id.set(0);
253    }
254
255    /// A header owned by no heap, for tests that need a non-null `GcRef`
256    /// address and never dereference the object behind it.
257    ///
258    /// The zero `heap_id` is what keeps this safe where `Heap::mark` masks an
259    /// accepted address to find its page: no heap's id is zero, so a detached
260    /// header is rejected by the provenance check *before* anything derives a
261    /// page from its address.
262    #[cfg(test)]
263    pub(crate) fn detached() -> GcHeader {
264        GcHeader {
265            descriptor: Cell::new(std::ptr::null()),
266            payload_offset: std::mem::size_of::<GcHeader>() as u16,
267            heap_id: Cell::new(0),
268        }
269    }
270}
271
272/// Round `n` up to the next multiple of `align` (which must be a power of two).
273///
274/// The object-layout primitive behind [`GcHeader::payload_offset_for`]; kept
275/// `const` so the offset folds into a compile-time immediate.
276pub(crate) const fn round_up(n: usize, align: usize) -> usize {
277    debug_assert!(align.is_power_of_two());
278    (n + align - 1) & !(align - 1)
279}
280
281/// A non-null, uniformly-typed reference to a garbage-collected object.
282///
283/// Construction is `unsafe` because the caller must guarantee the pointer
284/// points to a valid, live allocation of the right shape. The safe accessors
285/// are the ordinary way to interact with a `GcRef` from Rust runtime wrappers.
286///
287/// `PartialEq`/`Eq`/`Hash` are by **pointer identity**: two `GcRef`s are equal
288/// iff they point at the same object. (Structural value equality goes through
289/// [`GcRef::equals`](crate::GcRef::equals) and the descriptors, §5.5.)
290#[repr(transparent)]
291pub struct GcRef(NonNull<GcHeader>);
292
293impl PartialEq for GcRef {
294    #[inline]
295    fn eq(&self, other: &GcRef) -> bool {
296        self.as_ptr() == other.as_ptr()
297    }
298}
299impl Eq for GcRef {}
300
301impl std::hash::Hash for GcRef {
302    #[inline]
303    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
304        self.as_ptr().hash(state);
305    }
306}
307
308impl GcRef {
309    /// Wrap a non-null pointer. The pointer must point to a valid `GcHeader`
310    /// allocation; the caller (always internal runtime code) upholds this.
311    ///
312    /// # Safety
313    /// `ptr` must be non-null, properly aligned, and dereferenceable for the
314    /// full object it heads.
315    #[inline]
316    pub unsafe fn from_non_null(ptr: NonNull<GcHeader>) -> GcRef {
317        GcRef(ptr)
318    }
319
320    /// Wrap a non-null raw header pointer. Internal convenience for callers
321    /// (e.g. the shadow frame) that hold a `*mut GcHeader` already known to be
322    /// non-null.
323    ///
324    /// # Safety
325    /// `ptr` must be non-null, properly aligned, and point at a valid live
326    /// `GcHeader`.
327    #[inline]
328    pub unsafe fn from_raw(ptr: *mut GcHeader) -> GcRef {
329        // SAFETY: forwarded to the caller's contract.
330        let nn = unsafe { NonNull::new_unchecked(ptr) };
331        GcRef(nn)
332    }
333
334    /// The raw pointer this reference carries. Never null.
335    #[inline]
336    pub fn as_ptr(self) -> *mut GcHeader {
337        self.0.as_ptr()
338    }
339
340    /// The underlying non-null pointer, for safe interior access in runtime code.
341    #[inline]
342    pub fn as_non_null(self) -> NonNull<GcHeader> {
343        self.0
344    }
345
346    /// The header this reference points at.
347    #[inline]
348    pub fn header(&self) -> &GcHeader {
349        // SAFETY: `self.0` is a non-null pointer to a live `GcHeader` for as
350        // long as the `GcRef` is live (the caller of `from_non_null` upholds
351        // this; the GC does not move objects — ADR-011).
352        unsafe { self.0.as_ref() }
353    }
354
355    /// The descriptor describing this object's payload (§11.4).
356    #[inline]
357    pub fn descriptor(&self) -> &'static TypeDescriptor {
358        self.header().descriptor()
359    }
360
361    /// Pointer to the payload bytes immediately following this object's header.
362    ///
363    /// This is the low-level escape hatch; prefer the typed accessors on
364    /// [`crate::Runtime`] / the descriptor callbacks where possible.
365    #[inline]
366    pub fn payload<T>(&self) -> *mut T {
367        self.header().payload::<T>()
368    }
369}
370
371impl Clone for GcRef {
372    #[inline]
373    fn clone(&self) -> GcRef {
374        *self
375    }
376}
377impl Copy for GcRef {}
378
379impl std::fmt::Debug for GcRef {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        write!(f, "GcRef({:p})", self.0)
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    /// `GcRef` must be exactly pointer-sized and FFI-safe (§10.3). A regression
390    /// here would silently break the generated calling convention.
391    #[test]
392    fn gcref_is_pointer_sized() {
393        assert_eq!(
394            std::mem::size_of::<GcRef>(),
395            std::mem::size_of::<*mut u8>(),
396            "GcRef must be exactly one pointer"
397        );
398        assert_eq!(
399            std::mem::align_of::<GcRef>(),
400            std::mem::align_of::<*mut u8>()
401        );
402    }
403
404    #[test]
405    fn gcref_round_trips_a_real_header() {
406        let mut header = GcHeader::detached();
407        let nn = NonNull::from(&mut header);
408        // SAFETY: `nn` points at a live, aligned `GcHeader`.
409        let r = unsafe { GcRef::from_non_null(nn) };
410        assert_eq!(r.as_ptr(), nn.as_ptr());
411        assert_eq!(r.as_non_null(), nn);
412    }
413
414    #[test]
415    fn round_up_is_correct() {
416        assert_eq!(round_up(0, 8), 0);
417        assert_eq!(round_up(1, 8), 8);
418        assert_eq!(round_up(8, 8), 8);
419        assert_eq!(round_up(9, 8), 16);
420        assert_eq!(round_up(16, 1), 16);
421    }
422
423    /// The header must stay small and 8-aligned: it prefixes every allocation,
424    /// and `#[repr(C)]` plus this assertion is what lets generated code compute
425    /// a payload address (see `payload_offset_for`).
426    #[test]
427    fn header_layout_is_fixed() {
428        assert_eq!(std::mem::size_of::<GcHeader>(), 16);
429        assert_eq!(std::mem::align_of::<GcHeader>(), 8);
430    }
431
432    /// **One test for every number generated code depends on.**
433    ///
434    /// Three separate facts have to hold together for a Praxis binary to read
435    /// its own objects, and they are asserted in one place so that the next
436    /// person who repacks the header trips exactly one assertion and is sent to
437    /// exactly one decision record:
438    ///
439    /// * the header is 16 bytes and 8-aligned, so `page::MIN_BLOCK` and
440    ///   `page::BLOCK_GRANULE` — which derive from those two numbers — put the
441    ///   ladder's floor where ADR-109 says it is;
442    /// * `DESCRIPTOR_OFFSET` is 0, which is ADR-102's inline type proof: the
443    ///   backend folds it into the load that precedes every inlined scalar read;
444    /// * `payload_offset_for(8)` is 16, which is the immediate `Inst::EnumTag`
445    ///   and `emit_scalar_load` fold into an `iadd_imm`.
446    ///
447    /// The failure this guards is silent. Compiler and runtime are the same
448    /// binary, so `assert_abi_version` is trivially satisfied and would not
449    /// notice a header that changed width; the protection is that
450    /// `payload_offset_for` is the single `const` authority (ADR-039 Decision 1)
451    /// and that this test pins what it folds to. Nobody may hand-write 16.
452    #[test]
453    fn the_header_is_descriptor_offset_and_heap_id_and_nothing_else() {
454        assert_eq!(std::mem::size_of::<GcHeader>(), 16);
455        assert_eq!(std::mem::align_of::<GcHeader>(), 8);
456        assert_eq!(GcHeader::DESCRIPTOR_OFFSET, 0);
457        assert_eq!(GcHeader::payload_offset_for(8), 16);
458    }
459
460    /// **The folded payload offset, pinned beside the ABI version.**
461    ///
462    /// `Inst::EnumTag` reaches an enum's tag by calling `payload_offset_for` at
463    /// compile time and folding the answer into an `iadd_imm`, and
464    /// `emit_scalar_load` does the same for an inlined scalar read. Neither has
465    /// a literal to update — that is ADR-039 Decision 1 working — but a runtime
466    /// and a compiler that disagreed about where every payload in the language
467    /// begins would not be caught by the compiler-runtime version check, because
468    /// they are one binary. So the guard is this: the immediate is pinned here,
469    /// beside the version number that declares such a disagreement, so the two
470    /// can only be updated together.
471    ///
472    /// **The pin rides the current version**, not the version the offset last
473    /// moved at: what this asserts is that whoever bumps the version comes
474    /// through here and re-confirms the immediate. A pin frozen at one version
475    /// would make the next bump a mechanical edit of a failing number, which is
476    /// the same thing as deleting the test.
477    #[test]
478    fn the_folded_payload_offset_moved_at_v19_and_is_pinned_here() {
479        assert_eq!(std::mem::size_of::<GcHeader>(), 16);
480        assert_eq!(
481            GcHeader::payload_offset_for(std::mem::align_of::<GcHeader>()),
482            16
483        );
484        assert_eq!(
485            GcHeader::payload_offset_for(std::mem::align_of::<crate::enums::EnumPayload>()),
486            16,
487            "the offset lower.rs:Inst::EnumTag folds into an immediate"
488        );
489        assert_eq!(
490            crate::abi::RUNTIME_ABI_VERSION,
491            20,
492            "the offset above last moved at v19 and is 16 at this version; a \
493             bump must re-confirm it here rather than orphan this test"
494        );
495    }
496
497    /// The ladder's floor is the header, and the granule is the header's
498    /// alignment.
499    ///
500    /// `page::MIN_BLOCK` and `page::BLOCK_GRANULE` are written as
501    /// `size_of::<GcHeader>()` and `align_of::<GcHeader>()`, so a change to the
502    /// header re-derives `NUM_CLASSES`, `MAX_BLOCKS`, `BITMAP_WORDS` and the
503    /// whole size-class ladder without a single edit to `page.rs`. This codebase
504    /// pins derivations, because the alternative is that someone "simplifies"
505    /// `MIN_BLOCK` to a literal 16 and the next header change silently strands
506    /// the ladder one rung above the smallest block.
507    #[test]
508    fn the_ladder_floor_follows_the_header() {
509        assert_eq!(crate::page::MIN_BLOCK, std::mem::size_of::<GcHeader>());
510        assert_eq!(crate::page::BLOCK_GRANULE, std::mem::align_of::<GcHeader>());
511    }
512
513    /// `payload_offset_for` is the single layout authority. For any alignment
514    /// up to the header's own it is the header size; beyond that it pads.
515    ///
516    /// The 16 case is worth stating: the header is itself 16 bytes, so a
517    /// 16-aligned payload is not padded forward at all. The 64 case is the one
518    /// `heap::tests::OVERALIGNED` and the large-page path exercise.
519    #[test]
520    fn payload_offset_pads_only_for_overaligned_payloads() {
521        let header = std::mem::size_of::<GcHeader>();
522        for align in [1_usize, 2, 4, 8, 16] {
523            assert_eq!(GcHeader::payload_offset_for(align), header);
524        }
525        assert_eq!(GcHeader::payload_offset_for(64), 64);
526    }
527
528    /// The descriptor is the first word of the header, and generated code reads
529    /// it there (ADR-102).
530    ///
531    /// Asserting the *value* as well as the round trip is deliberate: the
532    /// backend folds `DESCRIPTOR_OFFSET` into an immediate, so a field reorder
533    /// that moved the descriptor would be a silent miscompile of every scalar
534    /// extract in the language if nothing here noticed. The round trip is what
535    /// proves the constant names the field rather than merely being small.
536    #[test]
537    fn the_descriptor_is_at_the_offset_generated_code_reads() {
538        assert_eq!(GcHeader::DESCRIPTOR_OFFSET, 0);
539        let header = GcHeader::new(
540            &crate::scalars::INT,
541            GcHeader::payload_offset_for(8) as u16,
542            HeapId::mint(),
543        );
544        let base = &header as *const GcHeader as *const u8;
545        // SAFETY: `DESCRIPTOR_OFFSET` is within the header by construction, and
546        // the field is a `Cell<*const TypeDescriptor>` — one pointer, so reading
547        // it as a `*const TypeDescriptor` is reading it at its own width.
548        let read_back = unsafe {
549            base.add(GcHeader::DESCRIPTOR_OFFSET)
550                .cast::<*const TypeDescriptor>()
551                .read()
552        };
553        assert!(
554            std::ptr::eq(read_back, &crate::scalars::INT),
555            "the word at DESCRIPTOR_OFFSET is the descriptor the header was built with"
556        );
557    }
558
559    /// The offset a header records must be the one `payload_offset_for`
560    /// computes — the invariant that makes `payload()` and the allocator agree.
561    #[test]
562    fn payload_offset_is_recorded_in_the_header() {
563        let header = GcHeader::new(
564            &crate::scalars::INT,
565            GcHeader::payload_offset_for(8) as u16,
566            HeapId::mint(),
567        );
568        let base = &header as *const GcHeader as usize;
569        assert_eq!(
570            header.payload::<i64>() as usize - base,
571            GcHeader::payload_offset_for(8)
572        );
573    }
574
575    #[test]
576    fn a_poisoned_header_has_no_heap_and_reports_itself() {
577        let header = GcHeader::new(
578            &crate::scalars::INT,
579            GcHeader::payload_offset_for(8) as u16,
580            HeapId::mint(),
581        );
582        assert!(!header.is_poisoned());
583        assert!(header.heap_id().is_some());
584
585        header.poison();
586
587        assert!(header.is_poisoned());
588        assert_eq!(header.heap_id(), None);
589    }
590
591    #[test]
592    fn minted_heap_ids_are_distinct_and_non_zero() {
593        let a = HeapId::mint();
594        let b = HeapId::mint();
595        assert_ne!(a, b);
596        assert_ne!(a.get(), 0);
597    }
598}