use std::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout};
use std::cell::RefCell;
#[cfg(test)]
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::sync::OnceLock;
mod pages;
const ALIGN: usize = 16;
const MAX_HEAP_OBJECT: usize = (isize::MAX as usize) - (ALIGN - 1);
const INITIAL_THRESHOLD: usize = 1 << 20;
struct Obj {
addr: usize,
size: usize,
scan: bool,
marked: bool,
}
struct Heap {
pages: pages::Pages,
large: Vec<Obj>,
work: Vec<(usize, usize)>,
alloc_since_gc: usize,
next_gc: usize,
live_bytes: usize,
base: usize,
collections: u64,
freed_bytes: u64,
#[cfg(test)]
requested: BTreeMap<usize, usize>,
}
impl Heap {
const fn new() -> Self {
Heap {
pages: pages::Pages::new(),
large: Vec::new(),
work: Vec::new(),
alloc_since_gc: 0,
next_gc: INITIAL_THRESHOLD,
live_bytes: 0,
base: 0,
collections: 0,
freed_bytes: 0,
#[cfg(test)]
requested: BTreeMap::new(),
}
}
}
thread_local! {
static HEAP: RefCell<Heap> = const { RefCell::new(Heap::new()) };
static ROOTS: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
static PINNED: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
}
extern "C" {
fn nova_gc_collect_roots(stack_base: *mut c_void);
}
fn stress() -> bool {
static S: OnceLock<bool> = OnceLock::new();
*S.get_or_init(|| std::env::var_os("NOVA_GC_STRESS").is_some())
}
fn debug() -> bool {
static D: OnceLock<bool> = OnceLock::new();
*D.get_or_init(|| std::env::var_os("NOVA_GC_DEBUG").is_some())
}
fn heap_layout(size: usize) -> Option<Layout> {
Layout::from_size_align(size, ALIGN).ok()
}
pub fn alloc(size: usize, scan: bool) -> *mut u8 {
let size = size.max(8);
let Some(layout) = heap_layout(size) else {
eprintln!(
"nova: panic: allocation of {size} bytes exceeds the maximum object size of {MAX_HEAP_OBJECT} bytes"
);
std::process::abort();
};
let class = (size <= pages::SMALL_MAX).then(|| pages::class_of(size));
let taken = class.map_or(size, |c| pages::CLASS_SIZES[c]);
maybe_collect(taken);
HEAP.with(|h| {
let mut h = h.borrow_mut();
let p = match class {
Some(c) => h.pages.alloc_slot(c, scan) as *mut u8,
None => {
let p = unsafe { alloc_zeroed(layout) };
if p.is_null() {
handle_alloc_error(layout);
}
h.large.push(Obj {
addr: p as usize,
size,
scan,
marked: false,
});
p
}
};
h.alloc_since_gc += taken;
h.live_bytes += taken;
#[cfg(test)]
h.requested.insert(p as usize, size);
p
})
}
pub fn add_root(ptr: *mut u8) {
PINNED.with(|p| p.borrow_mut().push(ptr as usize));
}
pub fn remove_root(ptr: *mut u8) {
PINNED.with(|p| {
let mut v = p.borrow_mut();
if let Some(i) = v.iter().rposition(|&a| a == ptr as usize) {
v.swap_remove(i);
}
});
}
#[cfg(test)]
pub(crate) fn object_info(addr: usize) -> Option<(usize, bool)> {
HEAP.with(|h| {
let h = h.borrow();
let scan = h
.pages
.slot_scan(addr)
.or_else(|| h.large.iter().find(|o| o.addr == addr).map(|o| o.scan))?;
let size = *h
.requested
.get(&addr)
.expect("alloc records every object's requested size in test builds");
Some((size, scan))
})
}
#[cfg(test)]
pub(crate) fn root_count(addr: usize) -> usize {
PINNED.with(|p| p.borrow().iter().filter(|&&a| a == addr).count())
}
#[cfg(test)]
#[inline(always)]
pub(crate) fn collect_for_test() {
collect();
}
#[cfg(test)]
pub(crate) fn sweep_with_roots_for_test(roots: &[usize]) {
collect_with_roots(roots);
}
fn maybe_collect(incoming: usize) {
let over = HEAP.with(|h| {
let h = h.borrow();
h.alloc_since_gc + incoming >= h.next_gc
});
if over || stress() {
collect();
}
}
fn collect() {
let base = HEAP.with(|h| {
let mut h = h.borrow_mut();
if h.base == 0 {
h.base = stack_base().unwrap_or_else(|| {
if debug() {
eprintln!(
"nova-gc: no stack bounds on this platform; \
collection is disabled and every allocation leaks"
);
}
usize::MAX
});
}
h.base
});
if base == usize::MAX {
HEAP.with(|h| h.borrow_mut().alloc_since_gc = 0);
return;
}
ROOTS.with(|r| {
let mut r = r.borrow_mut();
r.clear();
PINNED.with(|p| r.extend_from_slice(&p.borrow()));
});
unsafe { nova_gc_collect_roots(base as *mut c_void) };
let roots = ROOTS.with(|r| std::mem::take(&mut *r.borrow_mut()));
collect_with_roots(&roots);
}
fn collect_with_roots(roots: &[usize]) {
HEAP.with(|h| {
let mut guard = h.borrow_mut();
let h = &mut *guard;
h.large.sort_unstable_by_key(|o| o.addr);
let (lo, hi) = heap_range(&h.pages, &h.large);
let mut work = std::mem::take(&mut h.work);
for &w in roots {
mark_word(w, lo, hi, &mut h.pages, &mut h.large, &mut work);
}
while let Some((addr, len)) = work.pop() {
let mut p = addr;
let end = addr + len;
while p + 8 <= end {
let w = unsafe { *(p as *const usize) };
mark_word(w, lo, hi, &mut h.pages, &mut h.large, &mut work);
p += 8;
}
}
h.work = work;
let swept = h.pages.sweep();
let mut freed = swept.freed_bytes;
h.large.retain_mut(|o| {
if o.marked {
o.marked = false;
return true;
}
let layout = heap_layout(o.size)
.expect("a live object's size was accepted by heap_layout at allocation");
unsafe { dealloc(o.addr as *mut u8, layout) };
freed += o.size;
false
});
let (pages, large) = (&h.pages, &h.large);
crate::task::prune_freed_states(&|addr| {
pages.slot_scan(addr).is_some() || large.binary_search_by_key(&addr, |o| o.addr).is_ok()
});
h.freed_bytes += freed as u64;
h.live_bytes = swept.live_bytes + h.large.iter().map(|o| o.size).sum::<usize>();
h.alloc_since_gc = 0;
h.collections += 1;
h.next_gc = std::cmp::max(INITIAL_THRESHOLD, h.live_bytes.saturating_mul(2));
if debug() {
eprintln!(
"nova-gc: collection {} freed {freed} bytes, {} objects live ({} bytes)",
h.collections,
swept.live_slots + h.large.len(),
h.live_bytes,
);
}
});
}
fn heap_range(pages: &pages::Pages, large: &[Obj]) -> (usize, usize) {
let (mut lo, mut hi) = pages.bounds().unwrap_or((usize::MAX, 0));
for o in large {
lo = lo.min(o.addr);
hi = hi.max(o.addr + o.size);
}
if lo < hi {
(lo, hi)
} else {
(0, 0)
}
}
fn mark_word(
w: usize,
lo: usize,
hi: usize,
pages: &mut pages::Pages,
large: &mut [Obj],
work: &mut Vec<(usize, usize)>,
) {
if w < lo || w >= hi {
return;
}
match pages.try_mark(w) {
pages::Hit::Push(addr, len) => work.push((addr, len)),
pages::Hit::Done => {}
pages::Hit::Outside => {
let pos = large.partition_point(|o| o.addr <= w);
if pos == 0 {
return;
}
let o = &mut large[pos - 1];
if w < o.addr + o.size && !o.marked {
o.marked = true;
if o.scan {
work.push((o.addr, o.size));
}
}
}
}
}
#[no_mangle]
pub extern "C" fn nova_gc_scan_range(lo: *const c_void, hi: *const c_void) {
let lo = lo as usize;
let hi = hi as usize;
if lo == 0 || hi == 0 || lo >= hi {
return;
}
let mut p = (lo + 7) & !7; ROOTS.with(|r| {
let mut r = r.borrow_mut();
while p + 8 <= hi {
let w = unsafe { *(p as *const usize) };
r.push(w);
p += 8;
}
});
}
#[cfg(windows)]
fn stack_base() -> Option<usize> {
extern "system" {
fn GetCurrentThreadStackLimits(low_limit: *mut usize, high_limit: *mut usize);
}
let mut low = 0usize;
let mut high = 0usize;
unsafe { GetCurrentThreadStackLimits(&mut low, &mut high) };
(high > low).then_some(high)
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn stack_base() -> Option<usize> {
unsafe {
let mut attr: libc::pthread_attr_t = std::mem::zeroed();
if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
return None;
}
let mut addr: *mut libc::c_void = std::ptr::null_mut();
let mut size: libc::size_t = 0;
let rc = libc::pthread_attr_getstack(&attr, &mut addr, &mut size);
libc::pthread_attr_destroy(&mut attr);
if rc != 0 || addr.is_null() || size == 0 {
return None;
}
Some(addr as usize + size)
}
}
#[cfg(target_os = "macos")]
fn stack_base() -> Option<usize> {
let top = unsafe { libc::pthread_get_stackaddr_np(libc::pthread_self()) };
(!top.is_null()).then_some(top as usize)
}
#[cfg(not(any(
windows,
all(target_os = "linux", target_env = "gnu"),
target_os = "macos"
)))]
fn stack_base() -> Option<usize> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn reset() {
HEAP.with(|h| {
let mut h = h.borrow_mut();
h.pages.free_all();
for o in h.large.drain(..) {
let layout = heap_layout(o.size).expect("tracked size is describable");
unsafe { dealloc(o.addr as *mut u8, layout) };
}
h.requested.clear();
h.alloc_since_gc = 0;
h.live_bytes = 0;
h.next_gc = INITIAL_THRESHOLD;
});
crate::task::prune_freed_states(&|_| false);
PINNED.with(|p| p.borrow_mut().clear());
}
fn count() -> usize {
HEAP.with(|h| {
let h = h.borrow();
h.pages.live_slots() + h.large.len()
})
}
fn heap_bounds() -> (usize, usize) {
HEAP.with(|h| {
let h = h.borrow();
heap_range(&h.pages, &h.large)
})
}
#[test]
fn unrooted_objects_are_freed() {
reset();
let a = alloc(24, true) as usize;
let b = alloc(24, true) as usize;
let _c = alloc(24, true) as usize;
assert_eq!(count(), 3);
collect_with_roots(&[a, b]);
assert_eq!(count(), 2);
}
#[test]
fn no_roots_frees_everything() {
reset();
let _ = alloc(16, true);
let _ = alloc(16, true);
collect_with_roots(&[]);
assert_eq!(count(), 0);
}
#[test]
fn transitive_marking_keeps_referenced_objects() {
reset();
let child = alloc(16, true) as usize;
let parent = alloc(16, true) as *mut usize;
unsafe { *parent = child };
collect_with_roots(&[parent as usize]);
assert_eq!(count(), 2);
}
#[test]
fn interior_pointer_keeps_object() {
reset();
let a = alloc(32, true) as usize;
collect_with_roots(&[a + 16]);
assert_eq!(count(), 1);
}
#[test]
fn heap_layout_describes_ordinary_sizes() {
let layout = heap_layout(24).expect("24 bytes is describable");
assert_eq!(layout.size(), 24);
assert_eq!(layout.align(), ALIGN);
}
#[test]
fn the_crossing_allocation_collects_before_taking_its_slot() {
reset();
HEAP.with(|h| h.borrow_mut().next_gc = 64);
let count = || HEAP.with(|h| h.borrow().alloc_since_gc);
alloc(16, false);
alloc(16, false);
alloc(16, false);
assert_eq!(count(), 48, "three 16-byte slots, all below the threshold");
alloc(16, false);
assert_eq!(
count(),
16,
"the fourth reaches 64: it must collect, resetting the count, then charge its own slot"
);
reset();
}
#[test]
fn small_sizes_are_always_describable() {
assert!(heap_layout(pages::SMALL_MAX).is_some());
}
#[test]
fn small_max_takes_a_slot_and_one_byte_more_takes_the_large_path() {
reset();
let small = alloc(pages::SMALL_MAX, true) as usize;
let large = alloc(pages::SMALL_MAX + 1, true) as usize;
HEAP.with(|h| {
let h = h.borrow();
assert!(
h.pages.slot_scan(small).is_some(),
"2048 bytes must take a page slot"
);
assert!(
h.pages.slot_scan(large).is_none(),
"2049 bytes must not take a page slot"
);
assert!(
h.large.iter().any(|o| o.addr == large),
"2049 bytes must take the large path"
);
});
reset();
}
#[test]
fn heap_layout_rejects_undescribable_sizes() {
assert!(heap_layout(9_223_372_036_854_775_800).is_none());
assert!(heap_layout(usize::MAX).is_none());
}
#[test]
fn max_heap_object_is_the_largest_describable_size() {
assert!(heap_layout(MAX_HEAP_OBJECT).is_some());
assert!(heap_layout(MAX_HEAP_OBJECT + 1).is_none());
}
#[test]
fn leaf_objects_are_not_traced() {
reset();
let victim = alloc(16, true) as usize;
let leaf = alloc(16, false) as *mut usize;
unsafe { *leaf = victim };
collect_with_roots(&[leaf as usize]);
assert_eq!(count(), 1);
}
#[test]
fn sizes_over_small_max_take_the_large_path() {
reset();
let small = alloc(pages::SMALL_MAX, true) as usize;
let large = alloc(pages::SMALL_MAX + 1, true) as usize;
HEAP.with(|h| {
let h = h.borrow();
assert_eq!(h.pages.slot_scan(small), Some(true));
assert_eq!(h.pages.slot_scan(large), None);
assert!(h
.large
.iter()
.any(|o| o.addr == large && o.size == pages::SMALL_MAX + 1));
});
assert_eq!(object_info(small), Some((pages::SMALL_MAX, true)));
assert_eq!(object_info(large), Some((pages::SMALL_MAX + 1, true)));
}
#[test]
fn a_small_large_small_chain_survives_from_one_root() {
reset();
let tail = alloc(16, true) as usize;
let mid = alloc(4096, true) as *mut usize;
let head = alloc(16, true) as *mut usize;
unsafe {
*mid.add(100) = tail;
*head = mid as usize;
}
let _unreachable_large = alloc(4096, true);
collect_with_roots(&[head as usize]);
assert_eq!(
count(),
3,
"head, mid and tail survive; the other large object does not"
);
assert!(object_info(tail).is_some());
assert!(object_info(mid as usize).is_some());
}
#[test]
fn a_large_only_heap_collects() {
reset();
let keep = alloc(3000, false) as usize;
let _gone = alloc(5000, true);
collect_with_roots(&[keep + 1234]);
assert_eq!(count(), 1);
assert_eq!(object_info(keep), Some((3000, false)));
}
#[test]
fn a_pointer_into_a_rounding_tail_keeps_its_object() {
reset();
let a = alloc(17, true) as usize; collect_with_roots(&[a + 20]);
assert_eq!(count(), 1);
}
#[test]
fn words_outside_the_heap_range_mark_nothing() {
reset();
let a = alloc(16, true) as usize;
let (lo, hi) = heap_bounds();
assert_eq!(lo, a, "the only page's first slot starts the range");
collect_with_roots(&[lo - 1, hi]);
assert_eq!(count(), 0);
let _b = alloc(16, true);
collect_with_roots(&[heap_bounds().0]);
assert_eq!(count(), 1, "the range's first byte is inside it");
}
#[test]
fn a_reused_slot_does_not_trace_its_previous_objects_words() {
reset();
let victim = alloc(16, true) as usize;
let old = alloc(16, true) as *mut usize;
unsafe { *old.add(1) = victim };
collect_with_roots(&[victim]);
assert_eq!(count(), 1, "`old` is freed, its stale word left behind");
let new = alloc(8, true) as usize;
assert_eq!(new, old as usize, "the freed slot is the lowest free one");
collect_with_roots(&[new]);
assert_eq!(
count(),
1,
"the reused slot's zeroed tail kept the old pointer's target alive"
);
assert!(object_info(victim).is_none());
}
#[test]
fn a_reused_reserve_page_is_swept_and_traced_on_every_collection() {
reset();
let _ = alloc(16, true);
collect_with_roots(&[]);
let child = alloc(48, true) as usize;
let parent = alloc(48, true) as *mut usize;
unsafe { *parent = child };
assert_eq!(
HEAP.with(|h| h.borrow().pages.page_count()),
1,
"the class-48 objects went into the reserved class-16 page"
);
collect_with_roots(&[parent as usize]);
assert_eq!(count(), 2, "parent and child survive the first collection");
collect_with_roots(&[parent as usize]);
assert_eq!(count(), 2, "and the second, traced again");
collect_with_roots(&[]);
assert_eq!(count(), 0, "garbage in a reused reserve page is freed");
}
#[test]
fn registering_the_same_address_twice_requires_removing_it_twice() {
reset();
let obj = alloc(16, true);
add_root(obj);
add_root(obj);
remove_root(obj);
assert_eq!(
PINNED.with(|p| p.borrow().len()),
1,
"removing once should leave exactly one registration of a twice-registered address"
);
remove_root(obj);
assert_eq!(
PINNED.with(|p| p.borrow().len()),
0,
"the second remove_root should clear the second registration"
);
}
#[test]
fn removing_an_unregistered_address_does_not_panic_or_change_the_registry() {
reset();
let obj = alloc(16, true);
add_root(obj);
let never_registered = alloc(16, true);
remove_root(never_registered);
assert_eq!(
PINNED.with(|p| p.borrow().len()),
1,
"removing an address that was never registered changed the registry"
);
remove_root(obj);
}
#[cfg(any(
windows,
all(target_os = "linux", target_env = "gnu"),
target_os = "macos"
))]
#[test]
fn stack_base_lies_above_the_current_frame() {
let local = 0u8;
let here = std::ptr::addr_of!(local) as usize;
let base = stack_base().expect("this platform implements stack bounds");
assert!(
base > here,
"top {base:#x} must lie above this frame {here:#x}"
);
assert!(
base - here < 1 << 30,
"top {base:#x} is implausibly far above {here:#x}"
);
}
#[cfg(any(
windows,
all(target_os = "linux", target_env = "gnu"),
target_os = "macos"
))]
#[test]
fn stack_base_is_the_calling_threads_own() {
const STACK: usize = 256 * 1024;
let (base, here) = std::thread::Builder::new()
.stack_size(STACK)
.spawn(|| {
let local = 0u8;
(stack_base(), std::ptr::addr_of!(local) as usize)
})
.expect("spawn a thread")
.join()
.expect("join it");
let base = base.expect("this platform implements stack bounds");
assert!(base > here, "top {base:#x} must lie above {here:#x}");
assert!(
base - here < 4 * STACK,
"top {base:#x} is not this thread's: {here:#x}"
);
}
mod registry {
use super::*;
#[inline(never)]
fn hide(addr: usize) -> usize {
!addr
}
#[inline(never)]
fn reveal(hidden: usize) -> usize {
!hidden
}
#[test]
#[ignore = "not flaky on its own -- never observed to fail by itself. \
Gated only to stay paired with the sweep-asserting control \
test that does flake: this one asserts an object SURVIVED, \
which also passes against a collector that frees nothing at \
all, so running it while its control is ignored would leave \
a green assertion proving nothing. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn a_registered_root_survives_a_collection_with_no_stack_reference() {
let mut obj = alloc(64, true);
add_root(obj);
let hidden = hide(obj as usize);
obj = std::ptr::null_mut::<u8>();
std::hint::black_box(obj);
std::hint::black_box(hidden);
collect();
let addr = reveal(hidden);
assert!(
object_info(addr).is_some(),
"a registered root was swept; the registry is not seeding the mark set"
);
remove_root(addr as *mut u8);
}
#[test]
#[ignore = "flaky: asserts an unreachable object was SWEPT, and the \
conservative stack scan intermittently retains it instead \
-- a stale stack word in an already-returned frame is read \
as a root, in debug as well as release, at default test \
parallelism. Mechanism identified, not fixed; two remedies \
were each measured to make it worse. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn an_unregistered_object_is_swept() {
let mut obj = alloc(64, true);
let hidden = hide(obj as usize);
obj = std::ptr::null_mut::<u8>();
std::hint::black_box(obj);
std::hint::black_box(hidden);
collect();
let addr = reveal(hidden);
assert!(
object_info(addr).is_none(),
"an unreachable, unregistered object survived; this test cannot \
discriminate a working registry from a collector that frees nothing"
);
}
#[test]
#[ignore = "flaky: asserts an unreachable object was SWEPT, and the \
conservative stack scan intermittently retains it instead \
-- a stale stack word in an already-returned frame is read \
as a root, in debug as well as release, at default test \
parallelism. Mechanism identified, not fixed; two remedies \
were each measured to make it worse. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn remove_root_actually_unroots() {
let mut obj = alloc(64, true);
let hidden = hide(obj as usize);
add_root(obj);
remove_root(obj);
obj = std::ptr::null_mut::<u8>();
std::hint::black_box(obj);
std::hint::black_box(hidden);
collect();
let addr = reveal(hidden);
assert!(
object_info(addr).is_none(),
"either remove_root did not unroot, or (see this test's comment) \
an accidental conservative root retained the object -- this \
assertion alone cannot tell the two apart"
);
}
#[inline(never)]
fn setup_parent_and_child(register: bool) -> (usize, usize) {
let mut parent = alloc(16, true);
let mut child = alloc(32, true);
let child_addr = hide(child as usize);
let parent_addr = hide(parent as usize);
let mut child_bits = child as usize;
let mut parent_ptr = parent as *mut usize;
unsafe { parent_ptr.write(child_bits) };
if register {
add_root(parent);
}
child = std::ptr::null_mut::<u8>();
parent = std::ptr::null_mut::<u8>();
child_bits = 0;
parent_ptr = std::ptr::null_mut::<usize>();
std::hint::black_box(child);
std::hint::black_box(parent);
std::hint::black_box(child_bits);
std::hint::black_box(parent_ptr);
(parent_addr, child_addr)
}
#[test]
#[ignore = "not flaky on its own -- never observed to fail by itself. \
Gated only to stay paired with the sweep-asserting control \
test that does flake: this one asserts an object SURVIVED, \
which also passes against a collector that frees nothing at \
all, so running it while its control is ignored would leave \
a green assertion proving nothing. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn a_registered_root_keeps_its_transitive_children_alive() {
let (parent_addr, child_addr) = setup_parent_and_child(true);
std::hint::black_box(parent_addr);
std::hint::black_box(child_addr);
collect();
assert!(
object_info(reveal(child_addr)).is_some(),
"a child reachable only through a registered root was swept"
);
remove_root(reveal(parent_addr) as *mut u8);
}
#[test]
#[ignore = "flaky: asserts an unreachable object was SWEPT, and the \
conservative stack scan intermittently retains it instead \
-- a stale stack word in an already-returned frame is read \
as a root, in debug as well as release, at default test \
parallelism. Mechanism identified, not fixed; two remedies \
were each measured to make it worse. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn an_unregistered_parent_and_child_are_swept() {
let (_parent_addr, child_addr) = setup_parent_and_child(false);
std::hint::black_box(child_addr);
collect();
assert!(
object_info(reveal(child_addr)).is_none(),
"a child of an unregistered, unreachable parent survived; \
setup_parent_and_child's same-frame hiding is not sound here"
);
}
#[test]
#[ignore = "not flaky on its own -- never observed to fail by itself. \
Gated only to stay paired with the sweep-asserting control \
test that does flake: this one asserts an object SURVIVED, \
which also passes against a collector that frees nothing at \
all, so running it while its control is ignored would leave \
a green assertion proving nothing. See \
docs/adr/0010-conservative-scan-root-test-gating.md. \
Reachable with `cargo test -- --ignored`."]
fn the_registry_survives_more_than_one_collection() {
let mut obj = alloc(64, true);
add_root(obj);
let hidden = hide(obj as usize);
obj = std::ptr::null_mut::<u8>();
std::hint::black_box(obj);
std::hint::black_box(hidden);
collect();
collect();
collect();
let addr = reveal(hidden);
assert!(
object_info(addr).is_some(),
"the registry did not survive repeated collections; it is probably \
sharing the scratch ROOTS buffer"
);
remove_root(addr as *mut u8);
}
}
}