1use sim_lib_standard_core::LanguageProfile;
10
11#[cfg(feature = "standard-gc-tracing")]
13pub use sim_lib_gc_tracing::{ManagedHeap, ManagedHeapPolicy};
14pub use sim_lib_mutation::{
16 EdgeId, EdgeLimits, HardCappedRetainPolicy, ManagedArena, ManagedHandle, ManagedId,
17 ManagedNode, ManagedObject, ManagedRole, RoleBearingManagedObject,
18};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum CollectorPolicy {
23 #[cfg(feature = "standard-gc-tracing")]
25 Tracing,
26 #[cfg(feature = "standard-gc-retain")]
28 RetainCycles,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct CollectorInspection {
34 pub policy: &'static str,
36 pub reclaims_cycles: bool,
38 pub scope: &'static str,
40}
41
42pub 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
52pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum ManagedAllocation {
73 Acyclic,
75 Cyclic,
77}
78
79pub 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}