gc_lang/trace.rs
1//! Reachability tracing: the [`Trace`] contract and the [`Tracer`] sink.
2
3extern crate alloc;
4
5use alloc::vec::Vec;
6
7use crate::Gc;
8
9/// Reports the outgoing [`Gc`] edges of a heap object so the collector can follow
10/// them.
11///
12/// A mark-and-sweep collector keeps an object alive by reaching it from a root.
13/// "Reaching" means walking the graph of handles one object holds to the next, and
14/// only the object itself knows which handles those are — they might be struct
15/// fields, vector elements, map values, or buried in an enum variant. `Trace` is
16/// how the object hands that list to the collector: [`Heap::collect`] calls
17/// [`trace`](Trace::trace) on every object it visits, and the object calls
18/// [`Tracer::mark`] once per handle it stores.
19///
20/// The contract is small but load-bearing:
21///
22/// - **Mark every owned handle.** A handle you hold but do not mark is invisible to
23/// the collector; the object it points at can be swept while you still hold a
24/// handle to it. That is not unsound — the stale handle simply resolves to `None`
25/// afterwards — but it is almost never what you want. Marking is how you say
26/// "keep this alive".
27/// - **Marking more than you own is safe.** Extra marks at worst keep an object
28/// alive one cycle longer. The collector already ignores handles that do not name
29/// a live object, so a stale handle you mark costs nothing.
30/// - **`trace` must not allocate into the heap or mutate the object.** It is a
31/// read-only enumeration; it takes `&self` for exactly that reason.
32///
33/// Leaf objects that hold no handles implement `trace` as an empty body.
34///
35/// [`Heap::collect`]: crate::Heap::collect
36///
37/// # Examples
38///
39/// A cons-cell value type for a small interpreter. Each variant reports the handles
40/// it owns and nothing else:
41///
42/// ```
43/// use gc_lang::{Gc, Trace, Tracer};
44///
45/// enum Value {
46/// Nil,
47/// Int(i64),
48/// Pair(Gc<Value>, Gc<Value>),
49/// List(Vec<Gc<Value>>),
50/// }
51///
52/// impl Trace for Value {
53/// fn trace(&self, tracer: &mut Tracer<'_>) {
54/// match self {
55/// // Leaves own no handles.
56/// Value::Nil | Value::Int(_) => {}
57/// // A pair owns its two children.
58/// Value::Pair(car, cdr) => {
59/// tracer.mark(*car);
60/// tracer.mark(*cdr);
61/// }
62/// // A list owns each element.
63/// Value::List(items) => {
64/// for item in items {
65/// tracer.mark(*item);
66/// }
67/// }
68/// }
69/// }
70/// }
71/// ```
72pub trait Trace {
73 /// Reports each [`Gc`] handle this object owns by calling [`Tracer::mark`] on it.
74 ///
75 /// Called by the collector during the mark phase. See the [trait
76 /// documentation](Trace) for the contract this method must uphold.
77 fn trace(&self, tracer: &mut Tracer<'_>);
78}
79
80/// The sink a [`Trace`] implementation reports its outgoing edges to.
81///
82/// A `Tracer` is handed to [`Trace::trace`] during a collection. Its one job is to
83/// receive handles: each call to [`mark`](Tracer::mark) records that the object
84/// being traced points at another, so the collector can visit it in turn. It holds
85/// a borrow of the collector's work queue and nothing else, so recording an edge is
86/// a single push — no allocation on the steady-state path, since the queue is
87/// reused across collections.
88///
89/// You never construct a `Tracer`; the heap builds one and passes it in.
90pub struct Tracer<'a> {
91 /// The collector's mark-phase work queue, as `(index, generation)` pairs. The
92 /// generation travels with the index so a stale handle can be rejected at pop
93 /// time rather than silently resolving to whatever now occupies the slot.
94 worklist: &'a mut Vec<(u32, u32)>,
95}
96
97impl<'a> Tracer<'a> {
98 /// Wraps the collector's work queue. Internal: only [`Heap::collect`] builds a
99 /// tracer.
100 ///
101 /// [`Heap::collect`]: crate::Heap::collect
102 #[inline]
103 pub(crate) fn new(worklist: &'a mut Vec<(u32, u32)>) -> Self {
104 Self { worklist }
105 }
106
107 /// Records that the object being traced holds `handle`, so the collector will
108 /// visit — and thereby keep alive — the object it names.
109 ///
110 /// Call this once for every handle the object owns. Marking a handle that does
111 /// not name a live object is harmless: the collector rejects it when it reaches
112 /// the front of the queue. The generic parameter lets a value mix handles into
113 /// several heaps; each is validated against its own heap when that heap collects.
114 ///
115 /// # Examples
116 ///
117 /// ```
118 /// use gc_lang::{Gc, Trace, Tracer};
119 ///
120 /// struct Cell {
121 /// next: Option<Gc<Cell>>,
122 /// }
123 ///
124 /// impl Trace for Cell {
125 /// fn trace(&self, tracer: &mut Tracer<'_>) {
126 /// if let Some(next) = self.next {
127 /// tracer.mark(next);
128 /// }
129 /// }
130 /// }
131 /// ```
132 #[inline]
133 pub fn mark<T>(&mut self, handle: Gc<T>) {
134 self.worklist.push((handle.index(), handle.generation()));
135 }
136}