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