Skip to main content

sim_lib_lang_python/
managed.rs

1use sim_lib_gc_tracing::{CollectionError, CollectionLimits, CollectionReceipt, collect};
2use sim_lib_mutation::{
3    EdgeId, EdgeVisitor, HardCappedRetainPolicy, ManagedArena, ManagedHandle, ManagedId,
4    ManagedObject,
5};
6
7/// Language-visible role of a managed Python allocation.
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub enum PythonManagedKind {
10    /// An instance or class object.
11    #[default]
12    Instance,
13    /// A closure environment.
14    Closure,
15    /// A suspended or executing frame.
16    Frame,
17    /// An exception, traceback, or exception group.
18    Exception,
19    /// A mutable container.
20    Container,
21}
22
23/// Cyclic mutable Python payload held exclusively in the shared managed arena.
24#[derive(Clone, Debug, Default)]
25pub struct PythonManagedObject {
26    /// Language-visible allocation role; collection does not special-case it.
27    pub kind: PythonManagedKind,
28    /// Strong links to other Python objects.
29    pub edges: Vec<ManagedId>,
30}
31impl ManagedObject for PythonManagedObject {
32    fn trace_edges(&self, visitor: &mut dyn EdgeVisitor) {
33        for (i, target) in self.edges.iter().copied().enumerate() {
34            visitor.strong(EdgeId(i as u32), target);
35        }
36    }
37    fn clear_weak_edge(&mut self, _: EdgeId, _: ManagedId) -> bool {
38        false
39    }
40    fn clear_ephemeron_edge(&mut self, _: EdgeId, _: ManagedId, _: ManagedId) -> bool {
41        false
42    }
43}
44
45/// Explicit reclaim policy. Tracing is the standard; retention is opt-in and inspectable.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum PythonHeapPolicy {
48    /// Run the shared bounded tracing collector.
49    Tracing(CollectionLimits),
50    /// Retain until teardown; strong cycles leak by contract.
51    Retain,
52}
53
54/// Python managed heap composed from the shared arena and optional collector.
55pub struct PythonHeap {
56    arena: ManagedArena<PythonManagedObject>,
57    policy: PythonHeapPolicy,
58}
59impl PythonHeap {
60    /// Create the standard tracing heap.
61    pub fn standard(
62        cap: usize,
63        limits: CollectionLimits,
64    ) -> Result<Self, sim_lib_mutation::ArenaError> {
65        Ok(Self {
66            arena: ManagedArena::new(HardCappedRetainPolicy::new(cap)?),
67            policy: PythonHeapPolicy::Tracing(limits),
68        })
69    }
70    /// Create the explicit no-collector heap whose cycle leak is reported by `cycle_leak_gap`.
71    pub fn retaining(cap: usize) -> Result<Self, sim_lib_mutation::ArenaError> {
72        Ok(Self {
73            arena: ManagedArena::new(HardCappedRetainPolicy::new(cap)?),
74            policy: PythonHeapPolicy::Retain,
75        })
76    }
77    /// Allocate a cyclic-capable value only in the managed arena.
78    pub fn allocate(
79        &mut self,
80        value: PythonManagedObject,
81    ) -> Result<ManagedHandle, sim_lib_mutation::ArenaError> {
82        self.arena.allocate(value)
83    }
84    /// Add a strong language edge between two managed values.
85    pub fn connect(
86        &mut self,
87        from: ManagedHandle,
88        to: ManagedHandle,
89    ) -> Result<(), sim_lib_mutation::ArenaError> {
90        self.arena.get_mut(from)?.edges.push(to.id());
91        Ok(())
92    }
93    /// Return the number of live managed allocations.
94    pub fn live_len(&self) -> usize {
95        self.arena.len()
96    }
97    /// Return selected policy.
98    pub const fn policy(&self) -> PythonHeapPolicy {
99        self.policy
100    }
101    /// Return the explicit retention gap, if selected.
102    pub const fn cycle_leak_gap(&self) -> Option<&'static str> {
103        match self.policy {
104            PythonHeapPolicy::Retain => {
105                Some("unreachable strong cycles are retained until heap teardown")
106            }
107            PythonHeapPolicy::Tracing(_) => None,
108        }
109    }
110    /// Run a safepoint. Retention performs no implicit collection.
111    pub fn collect(&mut self) -> Result<Option<CollectionReceipt>, CollectionError> {
112        match self.policy {
113            PythonHeapPolicy::Tracing(limits) => collect(&mut self.arena, limits).map(Some),
114            PythonHeapPolicy::Retain => Ok(None),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn limits() -> CollectionLimits {
124        CollectionLimits {
125            objects: 8,
126            edges: 8,
127            stack: 8,
128            work: 32,
129            clears: 8,
130            finalizers: 0,
131        }
132    }
133
134    #[test]
135    fn tracing_is_standard_and_retention_is_explicit() {
136        let standard = PythonHeap::standard(8, limits()).unwrap();
137        assert!(matches!(standard.policy(), PythonHeapPolicy::Tracing(_)));
138        assert_eq!(standard.cycle_leak_gap(), None);
139        let retaining = PythonHeap::retaining(8).unwrap();
140        assert_eq!(retaining.policy(), PythonHeapPolicy::Retain);
141        assert!(retaining.cycle_leak_gap().unwrap().contains("cycles"));
142    }
143
144    #[test]
145    fn unreachable_python_objects_are_reclaimed_by_shared_collector() {
146        let mut heap = PythonHeap::standard(8, limits()).unwrap();
147        heap.allocate(PythonManagedObject::default()).unwrap();
148        let receipt = heap.collect().unwrap().unwrap();
149        assert_eq!(receipt.swept.len(), 1);
150    }
151
152    #[test]
153    fn heterogeneous_language_cycle_is_reclaimed_without_observable_mutation() {
154        let mut heap = PythonHeap::standard(8, limits()).unwrap();
155        let kinds = [
156            PythonManagedKind::Instance,
157            PythonManagedKind::Closure,
158            PythonManagedKind::Frame,
159            PythonManagedKind::Exception,
160            PythonManagedKind::Container,
161        ];
162        let handles: Vec<_> = kinds
163            .into_iter()
164            .map(|kind| {
165                heap.allocate(PythonManagedObject {
166                    kind,
167                    edges: vec![],
168                })
169                .unwrap()
170            })
171            .collect();
172        for pair in handles.windows(2) {
173            heap.connect(pair[0], pair[1]).unwrap();
174        }
175        heap.connect(handles[4], handles[0]).unwrap();
176        let visible_result = 42;
177        let receipt = heap.collect().unwrap().unwrap();
178        assert_eq!(receipt.swept.len(), 5);
179        assert_eq!(heap.live_len(), 0);
180        assert_eq!(visible_result, 42);
181    }
182}