pub struct Heap<T> { /* private fields */ }Expand description
A garbage-collected heap of T objects, reclaimed by tracing mark-and-sweep.
A Heap<T> is the object store for an interpreted-language runtime: allocate a
value with alloc and hold the returned Gc<T> handle, wire
objects to each other by storing handles inside them, and periodically call
collect with the runtime’s roots to reclaim everything no
longer reachable. Cycles are collected — reachability, not reference counting,
decides what lives — so a runtime can build arbitrary object graphs without
leaking them.
The design is deliberately narrow and entirely safe (#![forbid(unsafe_code)]):
- Handles, not pointers. A
Gc<T>is an index plus a generation stamp, so objects can point at one another freely without borrows, and a handle to a collected object resolves toNoneinstead of dangling. - Slots are reused. Sweeping a dead object returns its slot to a free list; the next allocation reuses it and bumps its generation. Steady-state allocate/collect loops do not grow the backing store.
- Scratch is pooled. The mark queue and the mark bitset are retained between collections, so a collection allocates nothing on the steady-state path.
T must implement Trace to be collected, so the collector can follow the
handles each object owns. Resolution (get) and allocation do not
require Trace; only collect does.
§Examples
Allocate a small graph, drop a root, and collect the unreachable part:
use gc_lang::{Gc, Heap, Trace, Tracer};
struct Node {
edges: Vec<Gc<Node>>,
}
impl Trace for Node {
fn trace(&self, tracer: &mut Tracer<'_>) {
for &e in &self.edges {
tracer.mark(e);
}
}
}
let mut heap = Heap::new();
let leaf = heap.alloc(Node { edges: vec![] });
let root = heap.alloc(Node { edges: vec![leaf] });
let orphan = heap.alloc(Node { edges: vec![] });
assert_eq!(heap.len(), 3);
// Collect with `root` as the only root: `root` and `leaf` survive, `orphan` does not.
let stats = heap.collect([root]);
assert_eq!(stats.freed, 1);
assert_eq!(heap.len(), 2);
assert!(heap.get(orphan).is_none());
assert!(heap.get(leaf).is_some());Implementations§
Source§impl<T> Heap<T>
impl<T> Heap<T>
Sourcepub const fn new() -> Self
pub const fn new() -> Self
Creates an empty heap. const, so it can initialise a static.
No allocation happens until the first value is added.
§Examples
use gc_lang::Heap;
let heap: Heap<u32> = Heap::new();
assert!(heap.is_empty());Sourcepub fn with_capacity(capacity: usize) -> Self
pub fn with_capacity(capacity: usize) -> Self
Creates an empty heap with room for capacity objects preallocated.
A hint only: it reserves backing storage so the first capacity allocations
do not reallocate. Sizing it to the runtime’s expected live-object count
keeps allocation off the reallocation path during warm-up.
§Examples
use gc_lang::Heap;
let heap: Heap<u64> = Heap::with_capacity(1024);
assert!(heap.capacity() >= 1024);Sourcepub fn alloc(&mut self, value: T) -> Gc<T>
pub fn alloc(&mut self, value: T) -> Gc<T>
Allocates value and returns a stable Gc<T> handle to it.
This is the hot path. It reuses a slot freed by an earlier collection when one is available, and only grows the backing store otherwise. The handle stays valid until the object it names is collected.
§Panics
Panics only if the heap has already exhausted its slot space — more than
u32::MAX slots that were never reclaimed, an unreachable ceiling for a
heap that collects. Use try_alloc for an explicit
non-panicking path.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Leaf;
impl Trace for Leaf {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
let handle = heap.alloc(Leaf);
assert!(heap.get(handle).is_some());Sourcepub fn try_alloc(&mut self, value: T) -> Result<Gc<T>, GcError>
pub fn try_alloc(&mut self, value: T) -> Result<Gc<T>, GcError>
Allocates value, returning its Gc<T> handle or an error if the heap’s
slot space is exhausted.
The non-panicking counterpart to alloc: identical on
success, but it returns GcError::CapacityExhausted instead of panicking
at the slot-space ceiling. Prefer it when a runtime allocates in response to
untrusted input whose volume it does not control.
§Errors
Returns GcError::CapacityExhausted when every one of the u32::MAX + 1
slot indices is in use and no slot is free. The heap is left unchanged;
running a collection to reclaim dead slots is the way to recover.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Leaf;
impl Trace for Leaf {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
let handle = heap.try_alloc(Leaf)?;
assert!(heap.get(handle).is_some());Sourcepub fn get(&self, handle: Gc<T>) -> Option<&T>
pub fn get(&self, handle: Gc<T>) -> Option<&T>
Borrows the object behind handle, or None if the handle does not name a
live object in this heap.
Resolution is a direct slot lookup, not a search. The None case covers both
an out-of-range handle and a stale one — a handle whose object was collected
and whose slot has since moved on to a new generation — so resolving a
handle never reads an unrelated value.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Payload(u32);
impl Trace for Payload {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
let handle = heap.alloc(Payload(7));
assert_eq!(heap.get(handle).map(|p| p.0), Some(7));Sourcepub fn get_mut(&mut self, handle: Gc<T>) -> Option<&mut T>
pub fn get_mut(&mut self, handle: Gc<T>) -> Option<&mut T>
Mutably borrows the object behind handle, or None if the handle does not
name a live object in this heap.
The mutating counterpart to get, with the same staleness
guarantees. Use it to update an object in place — including rewiring the
handles it holds, which is how a runtime mutates its object graph.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Cell(i64);
impl Trace for Cell {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
let handle = heap.alloc(Cell(0));
if let Some(cell) = heap.get_mut(handle) {
cell.0 = 42;
}
assert_eq!(heap.get(handle).map(|c| c.0), Some(42));Sourcepub fn contains(&self, handle: Gc<T>) -> bool
pub fn contains(&self, handle: Gc<T>) -> bool
Returns true if handle names a live object in this heap.
Equivalent to heap.get(handle).is_some(), without producing a borrow.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Leaf;
impl Trace for Leaf {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
let handle = heap.alloc(Leaf);
assert!(heap.contains(handle));Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
Returns the number of live objects in the heap.
This is the occupied-slot count, not the backing-store size; freed slots awaiting reuse are not counted.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Leaf;
impl Trace for Leaf {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
assert_eq!(heap.len(), 0);
heap.alloc(Leaf);
assert_eq!(heap.len(), 1);Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Returns true if the heap holds no live objects.
§Examples
use gc_lang::{Heap, Trace, Tracer};
struct Leaf;
impl Trace for Leaf {
fn trace(&self, _: &mut Tracer<'_>) {}
}
let mut heap = Heap::new();
assert!(heap.is_empty());
heap.alloc(Leaf);
assert!(!heap.is_empty());Sourcepub fn capacity(&self) -> usize
pub fn capacity(&self) -> usize
Returns the number of slots the backing store can hold before it must grow.
Reflects allocated capacity, including slots currently free. It never decreases across a collection: sweeping returns slots to the free list rather than releasing memory, so the store stays sized to the high-water mark.
§Examples
use gc_lang::Heap;
let heap: Heap<u64> = Heap::with_capacity(64);
assert!(heap.capacity() >= 64);Sourcepub fn collect<I>(&mut self, roots: I) -> CollectStats
pub fn collect<I>(&mut self, roots: I) -> CollectStats
Reclaims every object not reachable from roots, returning what the pass did.
This is a tracing mark-and-sweep collection in two phases. Mark: starting
from each root, the collector follows the handles every object reports through
Trace::trace, visiting each reachable object exactly once — so cycles
terminate and shared subgraphs are not re-scanned. Sweep: every object
that was not marked is dropped, its slot’s generation is advanced, and the
slot is returned to the free list for reuse.
roots is the set of handles the runtime considers live from the outside: an
interpreter’s value stack, its global environment, VM registers. Anything
reachable from a root survives; anything else — including whole unreachable
cycles — is reclaimed. A root handle that is already stale is ignored, so it
is safe to pass a conservative, slightly-oversized root set.
The cost is O(reachable) to mark plus O(slots) to sweep. The mark queue
and mark bitset are retained between calls, so a steady-state collection does
not allocate.
§Examples
Two objects in a cycle, unreachable from any root, are still collected:
use gc_lang::{Gc, Heap, Trace, Tracer};
struct Node {
link: Option<Gc<Node>>,
}
impl Trace for Node {
fn trace(&self, tracer: &mut Tracer<'_>) {
if let Some(link) = self.link {
tracer.mark(link);
}
}
}
let mut heap = Heap::new();
let a = heap.alloc(Node { link: None });
let b = heap.alloc(Node { link: Some(a) });
heap.get_mut(a).unwrap().link = Some(b); // a <-> b cycle, no external root
let stats = heap.collect([]); // empty root set
assert_eq!(stats.freed, 2);
assert!(heap.is_empty());