Skip to main content

Heap

Struct Heap 

Source
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 to None instead 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>

Source

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());
Source

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);
Source

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());
Source

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());
Source

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));
Source

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));
Source

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));
Source

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);
Source

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());
Source

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);
Source

pub fn collect<I>(&mut self, roots: I) -> CollectStats
where I: IntoIterator<Item = Gc<T>>, T: Trace,

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());

Trait Implementations§

Source§

impl<T> Debug for Heap<T>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Shows the heap’s shape — live objects, free slots, capacity — not its contents. A heap can hold millions of objects, and dumping them is rarely what a debug print wants; this also keeps Heap<T>: Debug free of a T: Debug bound.

Source§

impl<T> Default for Heap<T>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<T> Freeze for Heap<T>

§

impl<T> RefUnwindSafe for Heap<T>
where T: RefUnwindSafe,

§

impl<T> Send for Heap<T>
where T: Send,

§

impl<T> Sync for Heap<T>
where T: Sync,

§

impl<T> Unpin for Heap<T>
where T: Unpin,

§

impl<T> UnsafeUnpin for Heap<T>

§

impl<T> UnwindSafe for Heap<T>
where T: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.