Overview
gc-lang gives an interpreted-language runtime one thing, done carefully: a heap it
can allocate objects into and periodically sweep, reclaiming everything no longer
reachable — cycles included. Allocate a value into a Heap<T> and hold the
returned Gc<T> handle; objects store handles to point at one another; when the
runtime hands its roots to collect, the collector traces the reachable graph and
frees the rest.
It is a tracing mark-and-sweep collector, and it is entirely safe — the crate
is #![forbid(unsafe_code)]. Two design choices carry that:
- Reachability, not reference counting. What lives is decided by tracing from roots, so unreachable cycles are reclaimed. A runtime can build arbitrary object graphs — back-edges, doubly-linked structures, self-references — without leaking.
- Handles, not pointers. A
Gc<T>is a slot index plus a generation stamp, not an address. Objects wire to each other by handle, so the graph never fights the borrow checker, and a handle to a collected object resolves toNoneinstead of dangling. There is no use-after-free to have.
It owns object storage and reclamation only — no value representation, no interpreter, no policy on when to collect. The runtime keeps that control.
Features
- Cycle-collecting — tracing reachability reclaims what reference counting cannot.
- Safe by construction —
#![forbid(unsafe_code)]; a stale handle reads as absent, never dangles. - Generational handles —
Gc<T>isCopy, eight bytes, andEq/Ord/Hashfor anyT. - Slot reuse — sweeping returns slots to a free list; steady-state loops never grow the store.
- Allocation-free collection — the mark queue and mark bitset are pooled across passes.
no_std— needs onlyalloc; the defaultstdfeature changes nothing in the public surface.- Zero runtime dependencies — the collector is self-contained.
Installation
[]
= "1.0"
Or from the terminal:
no_std (needs a global allocator in your target):
[]
= { = "1.0", = false }
Quick Start
use ;
// The runtime's value type. Compound variants own handles to other values;
// `trace` reports them so the collector can follow them.
let mut heap = new;
let one = heap.alloc;
let two = heap.alloc;
let pair = heap.alloc;
let unused = heap.alloc;
// Collect with `pair` as the only root: `pair`, `one`, `two` survive; `unused` does not.
let stats = heap.collect;
assert_eq!;
assert!;
assert!;
How It Works
A collection is two phases:
- Mark. Starting from each root handle, the collector calls
Trace::traceon every object it reaches and follows the handles that object reports. Each object is visited once, so cycles terminate and shared subgraphs are not re-scanned. Marks live in a packed bitset, one bit per slot. - Sweep. Every object not marked is dropped, its slot's generation is advanced — which invalidates any outstanding handle to it — and the slot is returned to a free list for the next allocation to reuse.
The generation stamp is the safety mechanism: because a reused slot carries a new
generation, a handle to a collected object no longer matches whatever now lives there.
It resolves to None, never to an unrelated value.
The cost is O(reachable) to mark plus O(slots) to sweep. The mark queue and the
mark bitset are retained between calls, so a steady-state collection allocates nothing.
Examples
Runnable examples live in examples/:
Performance
Latest local Criterion means (release build, WSL2 Ubuntu on Windows x86_64). Numbers
vary by CPU and environment; reproduce with cargo bench.
| Operation | Cost | Notes |
|---|---|---|
Handle resolution (get) |
~0.6 ns | direct slot lookup + generation check |
| Allocation, reused slot | ~2.3 ns | free-list fast path |
| Allocation, fresh slot | ~6.7 ns | grows the backing store |
| Collection | ~12 ns/obj | linear in reachable + swept objects |
| Steady-state alloc + collect | ~28 ns | per 3-object allocate-then-reclaim cycle |
Collection scales linearly with heap size: ~10.8 µs for 1,000 reachable nodes, ~1.24 ms for 100,000.
Cross-Platform
Pure safe Rust on alloc, with no platform-specific code paths. The full suite runs
on Linux, macOS, and Windows (x86_64 and ARM64) across the CI matrix on stable and the
1.85 MSRV.
Testing
The property suite checks the collector's core invariant — an object is live after a collection if and only if it was reachable from a root — against an independent breadth-first walk over arbitrary generated graphs, cycles and shared subgraphs included.
Status
Stable — 1.0.0 is the API freeze. The public surface is frozen and will not break
until 2.0; 1.x releases are additive only. See the
SemVer promise, docs/API.md, the
ROADMAP, and CHANGELOG.md.
Contributing
Engineering standards live in REPS.md; the phase plan is in
dev/ROADMAP.md. Before a PR, all of the following must be clean: