1use sim_lib_standard_core::LanguageProfile;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum CollectorPolicy {
14 #[cfg(feature = "standard-gc-tracing")]
16 Tracing,
17 #[cfg(feature = "standard-gc-retain")]
19 RetainCycles,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub struct CollectorInspection {
25 pub policy: &'static str,
27 pub reclaims_cycles: bool,
29 pub scope: &'static str,
31}
32
33pub 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
43pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum ManagedAllocation {
64 Acyclic,
66 Cyclic,
68}
69
70pub 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}