extern crate alloc;
use alloc::vec::Vec;
use core::fmt;
use crate::{Gc, GcError, Trace, Tracer};
struct Slot<T> {
value: Option<T>,
generation: u32,
}
pub struct Heap<T> {
slots: Vec<Slot<T>>,
free: Vec<u32>,
live: usize,
worklist: Vec<(u32, u32)>,
marks: Vec<u64>,
}
impl<T> Heap<T> {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
slots: Vec::new(),
free: Vec::new(),
live: 0,
worklist: Vec::new(),
marks: Vec::new(),
}
}
#[inline]
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
slots: Vec::with_capacity(capacity),
free: Vec::new(),
live: 0,
worklist: Vec::new(),
marks: Vec::new(),
}
}
#[inline]
pub fn alloc(&mut self, value: T) -> Gc<T> {
match self.try_alloc(value) {
Ok(handle) => handle,
Err(_) => panic!("heap is full: cannot address beyond u32::MAX slots"),
}
}
#[inline]
pub fn try_alloc(&mut self, value: T) -> Result<Gc<T>, GcError> {
if let Some(index) = self.free.pop() {
let slot = &mut self.slots[index as usize];
slot.value = Some(value);
let generation = slot.generation;
self.live += 1;
return Ok(Gc::new(index, generation));
}
let index = u32::try_from(self.slots.len()).map_err(|_| GcError::CapacityExhausted)?;
self.slots.push(Slot {
value: Some(value),
generation: 0,
});
self.live += 1;
Ok(Gc::new(index, 0))
}
#[inline]
#[must_use]
pub fn get(&self, handle: Gc<T>) -> Option<&T> {
let slot = self.slots.get(handle.index() as usize)?;
if slot.generation == handle.generation() {
slot.value.as_ref()
} else {
None
}
}
#[inline]
pub fn get_mut(&mut self, handle: Gc<T>) -> Option<&mut T> {
let slot = self.slots.get_mut(handle.index() as usize)?;
if slot.generation == handle.generation() {
slot.value.as_mut()
} else {
None
}
}
#[inline]
#[must_use]
pub fn contains(&self, handle: Gc<T>) -> bool {
match self.slots.get(handle.index() as usize) {
Some(slot) => slot.generation == handle.generation() && slot.value.is_some(),
None => false,
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.live
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.live == 0
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.slots.capacity()
}
pub fn collect<I>(&mut self, roots: I) -> CollectStats
where
I: IntoIterator<Item = Gc<T>>,
T: Trace,
{
let mut work = core::mem::take(&mut self.worklist);
work.clear();
for root in roots {
work.push((root.index(), root.generation()));
}
self.reset_marks();
while let Some((index, generation)) = work.pop() {
let i = index as usize;
let current = matches!(
self.slots.get(i),
Some(slot) if slot.generation == generation && slot.value.is_some()
);
if !current || bit_is_set(&self.marks, i) {
continue;
}
set_bit(&mut self.marks, i);
if let Some(value) = self.slots[i].value.as_ref() {
value.trace(&mut Tracer::new(&mut work));
}
}
let mut freed = 0usize;
for i in 0..self.slots.len() {
let marked = bit_is_set(&self.marks, i);
let slot = &mut self.slots[i];
if slot.value.is_some() && !marked {
slot.value = None; slot.generation = slot.generation.wrapping_add(1);
self.free.push(i as u32);
freed += 1;
}
}
self.live -= freed;
self.worklist = work;
CollectStats {
live: self.live,
freed,
}
}
#[inline]
fn reset_marks(&mut self) {
let words = self.slots.len().div_ceil(64);
self.marks.clear();
self.marks.resize(words, 0);
}
}
#[inline]
fn bit_is_set(marks: &[u64], i: usize) -> bool {
(marks[i >> 6] >> (i & 63)) & 1 == 1
}
#[inline]
fn set_bit(marks: &mut [u64], i: usize) {
marks[i >> 6] |= 1u64 << (i & 63);
}
impl<T> Default for Heap<T> {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl<T> fmt::Debug for Heap<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Heap")
.field("live", &self.live)
.field("free", &self.free.len())
.field("capacity", &self.slots.capacity())
.finish()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct CollectStats {
pub live: usize,
pub freed: usize,
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use super::*;
struct Node {
edges: Vec<Gc<Node>>,
}
impl Node {
fn leaf() -> Self {
Self { edges: Vec::new() }
}
fn with(edges: Vec<Gc<Node>>) -> Self {
Self { edges }
}
}
impl Trace for Node {
fn trace(&self, tracer: &mut Tracer<'_>) {
for &edge in &self.edges {
tracer.mark(edge);
}
}
}
#[test]
fn test_alloc_then_get_round_trips() {
let mut heap = Heap::new();
let handle = heap.alloc(Node::leaf());
assert!(heap.get(handle).is_some());
assert!(heap.contains(handle));
assert_eq!(heap.len(), 1);
assert!(!heap.is_empty());
}
#[test]
fn test_unreachable_object_is_collected() {
let mut heap = Heap::new();
let keep = heap.alloc(Node::leaf());
let drop = heap.alloc(Node::leaf());
let stats = heap.collect([keep]);
assert_eq!(stats.freed, 1);
assert_eq!(stats.live, 1);
assert!(heap.get(keep).is_some());
assert!(heap.get(drop).is_none());
}
#[test]
fn test_reachable_subgraph_survives() {
let mut heap = Heap::new();
let leaf = heap.alloc(Node::leaf());
let mid = heap.alloc(Node::with(vec![leaf]));
let root = heap.alloc(Node::with(vec![mid]));
let orphan = heap.alloc(Node::leaf());
let stats = heap.collect([root]);
assert_eq!(stats.freed, 1); assert!(heap.get(root).is_some());
assert!(heap.get(mid).is_some());
assert!(heap.get(leaf).is_some());
assert!(heap.get(orphan).is_none());
}
#[test]
fn test_cycle_with_no_root_is_collected() {
let mut heap = Heap::new();
let a = heap.alloc(Node::leaf());
let b = heap.alloc(Node::with(vec![a]));
heap.get_mut(a).expect("a is live").edges.push(b); let stats = heap.collect([]);
assert_eq!(stats.freed, 2);
assert!(heap.is_empty());
}
#[test]
fn test_self_cycle_is_collected() {
let mut heap = Heap::new();
let a = heap.alloc(Node::leaf());
heap.get_mut(a).expect("a is live").edges.push(a); let stats = heap.collect([]);
assert_eq!(stats.freed, 1);
assert!(heap.get(a).is_none());
}
#[test]
fn test_freed_slot_is_reused_and_old_handle_goes_stale() {
let mut heap = Heap::new();
let first = heap.alloc(Node::leaf());
let _ = heap.collect([]); assert!(heap.get(first).is_none());
let second = heap.alloc(Node::leaf());
assert_eq!(first.index(), second.index());
assert_ne!(first.generation(), second.generation());
assert!(heap.get(second).is_some());
assert!(heap.get(first).is_none()); }
#[test]
fn test_capacity_does_not_grow_across_steady_state_loop() {
let mut heap: Heap<Node> = Heap::with_capacity(4);
let baseline = heap.capacity();
for _ in 0..1000 {
let a = heap.alloc(Node::leaf());
let b = heap.alloc(Node::leaf());
let _ = heap.alloc(Node::with(vec![a, b]));
let _ = heap.collect([]); }
assert!(heap.is_empty());
assert_eq!(
heap.capacity(),
baseline,
"slots should be reused, not grown"
);
}
#[test]
fn test_collect_twice_is_idempotent_on_survivors() {
let mut heap = Heap::new();
let root = heap.alloc(Node::leaf());
let s1 = heap.collect([root]);
let s2 = heap.collect([root]);
assert_eq!(s1.live, 1);
assert_eq!(s2.freed, 0);
assert_eq!(s2.live, 1);
assert!(heap.get(root).is_some());
}
#[test]
fn test_stale_root_is_ignored() {
let mut heap = Heap::new();
let gone = heap.alloc(Node::leaf());
let _ = heap.collect([]); let live = heap.alloc(Node::leaf());
let stats = heap.collect([gone, live]);
assert_eq!(stats.live, 1);
assert!(heap.get(live).is_some());
}
#[test]
fn test_out_of_range_handle_resolves_to_none() {
let mut big = Heap::new();
let mut last = big.alloc(Node::leaf());
for _ in 0..50 {
last = big.alloc(Node::leaf());
}
let small: Heap<Node> = Heap::new();
assert!(small.get(last).is_none());
assert!(!small.contains(last));
}
#[test]
fn test_empty_heap_collect_is_a_noop() {
let mut heap: Heap<Node> = Heap::new();
let stats = heap.collect([]);
assert_eq!(stats.freed, 0);
assert_eq!(stats.live, 0);
}
#[test]
fn test_debug_reports_shape_not_contents() {
let mut heap = Heap::new();
let _ = heap.alloc(Node::leaf());
let text = alloc::format!("{heap:?}");
assert!(text.contains("live"), "{text}");
assert!(text.contains("capacity"), "{text}");
}
}