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}