Skip to main content

sim_lib_gc_tracing/
heap.rs

1use sim_lib_mutation::{
2    ArenaError, HardCappedRetainPolicy, ManagedArena, ManagedHandle, ManagedId, ManagedObject,
3    RootedHandle, TeardownReceipt,
4};
5
6use crate::{CollectionError, CollectionLimits, CollectionReceipt, collect};
7
8/// Explicit reclamation policy for a managed heap.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ManagedHeapPolicy {
11    /// Reclaim unreachable objects with the bounded tracing collector.
12    Tracing(CollectionLimits),
13    /// Retain every object until explicit heap teardown.
14    Retain,
15}
16
17/// A dependency-correct managed heap composed from an arena and reclamation policy.
18///
19/// The wrapper is generic over the guest payload and lives beside the collector,
20/// so guest runtimes do not need to duplicate policy or lifecycle behavior.
21pub struct ManagedHeap<T: ManagedObject> {
22    arena: ManagedArena<T>,
23    policy: ManagedHeapPolicy,
24}
25
26impl<T: ManagedObject> ManagedHeap<T> {
27    /// Creates a heap which uses bounded tracing collection.
28    pub fn tracing(cap: usize, limits: CollectionLimits) -> Result<Self, ArenaError> {
29        Ok(Self {
30            arena: ManagedArena::new(HardCappedRetainPolicy::new(cap)?),
31            policy: ManagedHeapPolicy::Tracing(limits),
32        })
33    }
34
35    /// Creates a heap which retains objects until explicit teardown.
36    pub fn retaining(cap: usize) -> Result<Self, ArenaError> {
37        Ok(Self {
38            arena: ManagedArena::new(HardCappedRetainPolicy::new(cap)?),
39            policy: ManagedHeapPolicy::Retain,
40        })
41    }
42
43    /// Allocates a value after checking the arena capacity and identity space.
44    pub fn allocate(&mut self, value: T) -> Result<ManagedHandle, ArenaError> {
45        self.arena.allocate(value)
46    }
47
48    /// Returns a shared value reference after validating its handle.
49    pub fn get(&self, handle: ManagedHandle) -> Result<&T, ArenaError> {
50        self.arena.get(handle)
51    }
52
53    /// Returns a mutable value reference after validating its handle.
54    pub fn get_mut(&mut self, handle: ManagedHandle) -> Result<&mut T, ArenaError> {
55        self.arena.get_mut(handle)
56    }
57
58    /// Resolves a live managed identity to its generation-checked handle.
59    pub fn handle(&self, id: ManagedId) -> Result<ManagedHandle, ArenaError> {
60        self.arena.handle(id)
61    }
62
63    /// Registers a validated handle as a tracing root.
64    pub fn root(&mut self, handle: ManagedHandle) -> Result<RootedHandle, ArenaError> {
65        self.arena.root(handle)
66    }
67
68    /// Releases one matching root registration.
69    pub fn release_root(&mut self, rooted: RootedHandle) -> Result<ManagedHandle, ArenaError> {
70        self.arena.release_root(rooted)
71    }
72
73    /// Returns the number of live managed allocations.
74    pub fn live_len(&self) -> usize {
75        self.arena.len()
76    }
77
78    /// Returns the selected reclamation policy.
79    pub const fn policy(&self) -> ManagedHeapPolicy {
80        self.policy
81    }
82
83    /// Describes the cycle-reclamation gap when retention is selected.
84    pub const fn cycle_leak_gap(&self) -> Option<&'static str> {
85        match self.policy {
86            ManagedHeapPolicy::Tracing(_) => None,
87            ManagedHeapPolicy::Retain => {
88                Some("unreachable strong cycles are retained until heap teardown")
89            }
90        }
91    }
92
93    /// Runs the configured reclamation policy at a safepoint.
94    ///
95    /// Retaining heaps return `None` without mutating the arena.
96    pub fn collect(&mut self) -> Result<Option<CollectionReceipt>, CollectionError> {
97        match self.policy {
98            ManagedHeapPolicy::Tracing(limits) => collect(&mut self.arena, limits).map(Some),
99            ManagedHeapPolicy::Retain => Ok(None),
100        }
101    }
102
103    /// Removes every object and root, returning deterministic teardown evidence.
104    pub fn teardown(&mut self) -> TeardownReceipt {
105        self.arena.teardown()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use sim_lib_mutation::{EdgeId, EdgeVisitor, ManagedId};
112
113    use super::*;
114
115    #[derive(Default)]
116    struct Node(Vec<ManagedId>);
117
118    impl ManagedObject for Node {
119        fn trace_edges(&self, visitor: &mut dyn EdgeVisitor) {
120            for (edge, target) in self.0.iter().copied().enumerate() {
121                visitor.strong(EdgeId(edge as u32), target);
122            }
123        }
124
125        fn clear_weak_edge(&mut self, _: EdgeId, _: ManagedId) -> bool {
126            false
127        }
128    }
129
130    fn limits() -> CollectionLimits {
131        CollectionLimits {
132            objects: 8,
133            edges: 8,
134            stack: 8,
135            work: 32,
136            clears: 8,
137            finalizers: 0,
138        }
139    }
140
141    #[test]
142    fn tracing_and_retaining_policies_match_guest_heap_behavior() {
143        let tracing = ManagedHeap::<Node>::tracing(8, limits()).unwrap();
144        assert_eq!(tracing.policy(), ManagedHeapPolicy::Tracing(limits()));
145        assert_eq!(tracing.cycle_leak_gap(), None);
146
147        let mut retaining = ManagedHeap::<Node>::retaining(8).unwrap();
148        retaining.allocate(Node::default()).unwrap();
149        assert_eq!(retaining.policy(), ManagedHeapPolicy::Retain);
150        assert!(retaining.cycle_leak_gap().unwrap().contains("cycles"));
151        assert_eq!(retaining.collect().unwrap(), None);
152        assert_eq!(retaining.live_len(), 1);
153    }
154
155    #[test]
156    fn tracing_reclaims_cycles_and_checked_access_rejects_stale_handles() {
157        let mut heap = ManagedHeap::tracing(8, limits()).unwrap();
158        let first = heap.allocate(Node::default()).unwrap();
159        let second = heap.allocate(Node::default()).unwrap();
160        assert_eq!(heap.handle(first.id()).unwrap(), first);
161        heap.get_mut(first).unwrap().0.push(second.id());
162        heap.get_mut(second).unwrap().0.push(first.id());
163
164        assert_eq!(
165            heap.collect().unwrap().unwrap().swept,
166            [first.id(), second.id()]
167        );
168        assert!(matches!(heap.get(first), Err(ArenaError::StaleHandle(id)) if id == first.id()));
169        assert!(
170            matches!(heap.handle(first.id()), Err(ArenaError::StaleHandle(id)) if id == first.id())
171        );
172        assert_eq!(heap.live_len(), 0);
173    }
174
175    #[test]
176    fn roots_survive_collection_and_teardown_reports_all_state() {
177        let mut heap = ManagedHeap::tracing(8, limits()).unwrap();
178        let handle = heap.allocate(Node::default()).unwrap();
179        let rooted = heap.root(handle).unwrap();
180        assert!(heap.collect().unwrap().unwrap().swept.is_empty());
181
182        let receipt = heap.teardown();
183        assert_eq!(receipt.objects, [handle.id()]);
184        assert_eq!(receipt.roots, [rooted.root_id()]);
185        assert_eq!(heap.live_len(), 0);
186    }
187}