Skip to main content

sim/
gc.rs

1//! Standard-distribution managed-object collection policy.
2//!
3//! `standard` selects tracing collection. Retention is available only through
4//! the explicit `standard-gc-retain` feature for minimal and test closures; it
5//! never reclaims cycles and fails closed at the arena's hard object cap.
6
7// conformance: SDK builds expose and enforce their selected collection policy.
8
9use sim_lib_standard_core::LanguageProfile;
10
11/// The managed-object policy selected by this SDK build.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum CollectorPolicy {
14    /// Bounded stop-the-world tracing, including unreachable-cycle reclamation.
15    #[cfg(feature = "standard-gc-tracing")]
16    Tracing,
17    /// Hard-capped retention for explicit minimal/test builds; cycles leak.
18    #[cfg(feature = "standard-gc-retain")]
19    RetainCycles,
20}
21
22/// Stable inspection projection for the selected distribution policy.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct CollectorInspection {
25    /// Machine-readable policy name.
26    pub policy: &'static str,
27    /// Whether unreachable cycles are reclaimed.
28    pub reclaims_cycles: bool,
29    /// Intended distribution scope.
30    pub scope: &'static str,
31}
32
33/// Returns the policy selected by the feature closure.
34pub const fn selected_policy() -> CollectorPolicy {
35    #[cfg(feature = "standard-gc-tracing")]
36    return CollectorPolicy::Tracing;
37    #[cfg(all(not(feature = "standard-gc-tracing"), feature = "standard-gc-retain"))]
38    return CollectorPolicy::RetainCycles;
39    #[cfg(not(any(feature = "standard-gc-tracing", feature = "standard-gc-retain")))]
40    compile_error!("standard-mutation requires an explicit collector policy");
41}
42
43/// Projects the selected policy through the SDK's ordinary inspection API.
44pub const fn inspect_selected_policy() -> CollectorInspection {
45    match selected_policy() {
46        #[cfg(feature = "standard-gc-tracing")]
47        CollectorPolicy::Tracing => CollectorInspection {
48            policy: "gc/tracing",
49            reclaims_cycles: true,
50            scope: "standard-production",
51        },
52        #[cfg(feature = "standard-gc-retain")]
53        CollectorPolicy::RetainCycles => CollectorInspection {
54            policy: "gc/retain-hard-capped",
55            reclaims_cycles: false,
56            scope: "explicit-minimal-or-test",
57        },
58    }
59}
60
61/// Declares whether a guest profile allocates managed objects that may cycle.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum ManagedAllocation {
64    /// The profile does not allocate managed cyclic graphs.
65    Acyclic,
66    /// The profile may allocate reference cycles and therefore needs reclamation.
67    Cyclic,
68}
69
70/// Admits a guest profile to a production distribution.
71///
72/// Cyclic profiles fail closed unless the build selected a reclaiming collector;
73/// retention is never silently treated as production garbage collection.
74pub fn require_production_collector(
75    profile: &LanguageProfile,
76    allocation: ManagedAllocation,
77) -> sim_kernel::Result<CollectorInspection> {
78    for (field, symbol) in [
79        ("reader", &profile.reader),
80        ("lowering", &profile.lowering),
81        ("eval policy", &profile.eval_policy),
82    ] {
83        if symbol.namespace.as_deref() == Some("standard/unspecified") {
84            return Err(sim_kernel::Error::Eval(format!(
85                "guest profile {} has no declared {field}",
86                profile.symbol
87            )));
88        }
89    }
90    if profile.organs.is_empty()
91        || profile.capabilities.is_empty()
92        || profile.unsupported_forms.is_empty()
93    {
94        return Err(sim_kernel::Error::Eval(format!(
95            "guest profile {} has incomplete production evidence",
96            profile.symbol
97        )));
98    }
99    let inspection = inspect_selected_policy();
100    if allocation == ManagedAllocation::Cyclic && !inspection.reclaims_cycles {
101        return Err(sim_kernel::Error::Eval(format!(
102            "production guest profile {} allocates managed cycles but selected policy {} does not reclaim them",
103            profile.symbol, inspection.policy
104        )));
105    }
106    Ok(inspection)
107}
108
109#[cfg(test)]
110mod tests {
111    use sim_kernel::{CapabilityName, Symbol};
112    #[cfg(feature = "standard-gc-tracing")]
113    use sim_lib_mutation::{
114        EdgeId, EdgeVisitor, HardCappedRetainPolicy, ManagedArena, ManagedId, ManagedObject,
115    };
116
117    use super::*;
118
119    #[cfg(feature = "standard-gc-tracing")]
120    #[derive(Clone, Default)]
121    struct Node(Vec<ManagedId>);
122    #[cfg(feature = "standard-gc-tracing")]
123    impl ManagedObject for Node {
124        fn trace_edges(&self, visitor: &mut dyn EdgeVisitor) {
125            for (edge, target) in self.0.iter().copied().enumerate() {
126                visitor.strong(EdgeId(edge as u32), target);
127            }
128        }
129        fn clear_weak_edge(&mut self, _: EdgeId, _: ManagedId) -> bool {
130            false
131        }
132    }
133
134    fn cyclic_guest() -> LanguageProfile {
135        LanguageProfile::new(Symbol::qualified("lang", "cyclic-specimen/v1"))
136            .with_reader(Symbol::qualified("codec", "lisp"))
137            .with_lowering(Symbol::qualified("lower", "cyclic"))
138            .with_eval_policy(Symbol::qualified("eval", "eager"))
139            .with_organ(sim_lib_standard_core::OrganUse::new(Symbol::qualified(
140                "organ", "mutation",
141            )))
142            .requiring(CapabilityName::new("managed.allocate"))
143            .with_unsupported_form(Symbol::qualified("gap", "native-finalizer"))
144    }
145
146    #[test]
147    #[cfg(feature = "standard-gc-tracing")]
148    fn standard_tracing_reclaims_cycle_while_explicit_retention_hits_cap() {
149        let inspection =
150            require_production_collector(&cyclic_guest(), ManagedAllocation::Cyclic).unwrap();
151        assert_eq!(inspection.policy, "gc/tracing");
152
153        let mut traced = ManagedArena::new(HardCappedRetainPolicy::new(2).unwrap());
154        let a = traced.allocate(Node::default()).unwrap();
155        let b = traced.allocate(Node(vec![a.id()])).unwrap();
156        traced.get_mut(a).unwrap().0.push(b.id());
157        let receipt = sim_lib_gc_tracing::collect(
158            &mut traced,
159            sim_lib_gc_tracing::CollectionLimits {
160                objects: 2,
161                edges: 2,
162                stack: 2,
163                work: 16,
164                clears: 0,
165                finalizers: 0,
166            },
167        )
168        .unwrap();
169        assert_eq!(receipt.swept, vec![a.id(), b.id()]);
170        assert!(traced.is_empty());
171
172        let mut retained = ManagedArena::new(HardCappedRetainPolicy::new(2).unwrap());
173        retained.allocate(Node::default()).unwrap();
174        retained.allocate(Node::default()).unwrap();
175        assert!(matches!(
176            retained.allocate(Node::default()),
177            Err(sim_lib_mutation::ArenaError::CapacityExceeded { cap: 2 })
178        ));
179    }
180
181    #[test]
182    #[cfg(all(feature = "standard-gc-retain", not(feature = "standard-gc-tracing")))]
183    fn explicit_retention_cannot_admit_a_cyclic_production_guest() {
184        let error = require_production_collector(&cyclic_guest(), ManagedAllocation::Cyclic)
185            .expect_err("retain-only policy must fail closed");
186        assert!(error.to_string().contains("does not reclaim"));
187        assert_eq!(inspect_selected_policy().scope, "explicit-minimal-or-test");
188    }
189}