Skip to main content

gc_lang/
handle.rs

1//! The typed handle into a garbage-collected heap.
2
3use core::cmp::Ordering;
4use core::fmt;
5use core::hash::{Hash, Hasher};
6use core::marker::PhantomData;
7
8/// A small, copyable, type-tagged handle to one object in a [`Heap`](crate::Heap).
9///
10/// A `Gc<T>` is how a garbage-collected object refers to another. It is eight bytes
11/// — a slot index plus a generation stamp — so passing one is about as cheap as
12/// passing a `u64`, and unlike a `&T` it carries no borrow. That is what lets the
13/// runtime build cyclic object graphs: a node stores `Gc<T>` handles to its
14/// neighbours, edges point in every direction, and the borrow checker never enters
15/// the picture. Resolving a handle is a direct slot lookup through
16/// [`Heap::get`](crate::Heap::get) / [`get_mut`](crate::Heap::get_mut).
17///
18/// The generation stamp is what makes a handle safe to hold across a collection. A
19/// slot's generation advances every time the slot is reclaimed and reused, so a
20/// handle to an object that was collected no longer matches the object now living
21/// in that slot: it resolves to `None` rather than silently aliasing an unrelated
22/// value. A handle is therefore never a dangling pointer — at worst it is a stale
23/// handle that reads as absent.
24///
25/// The `T` tag is compile-time only and occupies no space: it stops a `Gc<Value>`
26/// from being resolved against a `Heap<Node>`. `Gc<T>` is `Copy`, `Eq`, `Ord`, and
27/// `Hash` for **every** `T` — the tag never adds a bound — so it works as a map key
28/// regardless of what it points at. There is no public constructor: a `Gc` can only
29/// come from [`Heap::alloc`](crate::Heap::alloc) / [`try_alloc`](crate::Heap::try_alloc).
30///
31/// # Examples
32///
33/// ```
34/// use gc_lang::{Heap, Trace, Tracer};
35///
36/// struct Node;
37/// impl Trace for Node {
38///     fn trace(&self, _: &mut Tracer<'_>) {}
39/// }
40///
41/// let mut heap = Heap::new();
42/// let handle = heap.alloc(Node);
43///
44/// // It is Copy and eight bytes wide, whatever it points at.
45/// let also = handle;
46/// assert_eq!(handle, also);
47/// assert_eq!(core::mem::size_of_val(&handle), 8);
48/// ```
49pub struct Gc<T> {
50    index: u32,
51    generation: u32,
52    /// Compile-time type tag. `fn() -> T` keeps `Gc<T>` `Copy`, `Send`, and `Sync`
53    /// for any `T`, and covariant in `T`, without ever borrowing or owning a `T`.
54    marker: PhantomData<fn() -> T>,
55}
56
57impl<T> Gc<T> {
58    /// Wraps a raw `(index, generation)` pair. Internal: only a heap mints handles,
59    /// so the type tag always matches the heap the handle indexes.
60    #[inline]
61    pub(crate) const fn new(index: u32, generation: u32) -> Self {
62        Self {
63            index,
64            generation,
65            marker: PhantomData,
66        }
67    }
68
69    /// Returns the slot index this handle addresses.
70    #[inline]
71    pub(crate) const fn index(self) -> u32 {
72        self.index
73    }
74
75    /// Returns the generation stamp this handle was minted with.
76    #[inline]
77    pub(crate) const fn generation(self) -> u32 {
78        self.generation
79    }
80}
81
82// The trait impls are written by hand rather than derived: a derive would bound
83// each impl on `T` (e.g. `T: Clone`), but a handle must be `Copy`, comparable, and
84// hashable for every `T`, since `T` is only a compile-time tag.
85
86impl<T> Clone for Gc<T> {
87    #[inline]
88    fn clone(&self) -> Self {
89        *self
90    }
91}
92
93impl<T> Copy for Gc<T> {}
94
95impl<T> PartialEq for Gc<T> {
96    #[inline]
97    fn eq(&self, other: &Self) -> bool {
98        self.index == other.index && self.generation == other.generation
99    }
100}
101
102impl<T> Eq for Gc<T> {}
103
104impl<T> PartialOrd for Gc<T> {
105    #[inline]
106    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
107        Some(self.cmp(other))
108    }
109}
110
111impl<T> Ord for Gc<T> {
112    #[inline]
113    fn cmp(&self, other: &Self) -> Ordering {
114        // Index first, then generation: handles into the same slot from different
115        // eras stay distinct and adjacent under an ordering.
116        self.index
117            .cmp(&other.index)
118            .then(self.generation.cmp(&other.generation))
119    }
120}
121
122impl<T> Hash for Gc<T> {
123    #[inline]
124    fn hash<H: Hasher>(&self, state: &mut H) {
125        self.index.hash(state);
126        self.generation.hash(state);
127    }
128}
129
130impl<T> fmt::Debug for Gc<T> {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        // `index@generation` reads as "the object in slot N, era G".
133        write!(f, "Gc({}@{})", self.index, self.generation)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    #![allow(clippy::unwrap_used, clippy::expect_used)]
140
141    extern crate alloc;
142    use alloc::collections::BTreeSet;
143    use alloc::format;
144
145    use crate::{Heap, Trace, Tracer};
146
147    struct Leaf;
148    impl Trace for Leaf {
149        fn trace(&self, _: &mut Tracer<'_>) {}
150    }
151
152    #[test]
153    fn test_handle_traits_do_not_depend_on_the_tag() {
154        // A tag that is neither `Clone` nor `Eq` must not stop `Gc` from being
155        // `Copy`, `Eq`, and `Debug` — the tag is compile-time only.
156        struct NotClone;
157        impl Trace for NotClone {
158            fn trace(&self, _: &mut Tracer<'_>) {}
159        }
160        let mut heap = Heap::<NotClone>::new();
161        let handle = heap.alloc(NotClone);
162        let copy = handle; // Copy
163        assert_eq!(handle, copy); // Eq
164        assert!(format!("{handle:?}").starts_with("Gc")); // Debug
165    }
166
167    #[test]
168    fn test_distinct_allocations_have_distinct_handles() {
169        let mut heap = Heap::new();
170        let handles: BTreeSet<_> = (0..16).map(|_| heap.alloc(Leaf)).collect();
171        assert_eq!(handles.len(), 16); // all distinct, usable as ordered-set keys
172    }
173
174    #[test]
175    fn test_handle_is_eight_bytes_for_any_element() {
176        assert_eq!(core::mem::size_of::<crate::Gc<u8>>(), 8);
177        assert_eq!(core::mem::size_of::<crate::Gc<[u128; 4]>>(), 8);
178    }
179}