1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use tokio::sync::Mutex as TokioMutex;
6
7use crate::report::result::Failure;
8use crate::vm::Vm;
9use crate::vm::context::Scope;
10use relux_core::diagnostics::EffectId as DiagEffectId;
11use relux_core::pure::Env;
12use relux_ir::IrCleanupBlock;
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
17pub struct EffectInstanceKey {
18 pub effect_id: DiagEffectId,
19 pub evaluated_overlay: String,
20}
21
22impl EffectInstanceKey {
23 pub fn from_expects(
30 effect_id: DiagEffectId,
31 expect_names: &[&str],
32 evaluated_overlay: &Env,
33 ) -> Self {
34 let identity: String = expect_names
35 .iter()
36 .map(|name| {
37 let val = evaluated_overlay.get(name).unwrap_or("");
38 format!("{name}\0{val}")
39 })
40 .collect::<Vec<_>>()
41 .join("\0");
42 Self {
43 effect_id,
44 evaluated_overlay: identity,
45 }
46 }
47}
48
49pub struct EffectHandle {
52 pub scope: Scope,
53 pub shells: HashMap<String, Arc<TokioMutex<Vm>>>,
55 pub exposed: HashSet<String>,
57 pub dependencies: Vec<EffectInstanceKey>,
58 pub cleanup: Option<IrCleanupBlock>,
59}
60
61impl EffectHandle {
62 pub fn exposed_shells(&self) -> HashMap<String, Arc<TokioMutex<Vm>>> {
64 self.shells
65 .iter()
66 .filter(|(name, _)| self.exposed.contains(name.as_str()))
67 .map(|(k, v)| (k.clone(), v.clone()))
68 .collect()
69 }
70}
71
72pub enum EffectSlot {
75 Empty,
76 Ready {
77 refcount: usize,
78 handle: EffectHandle,
79 },
80 Failed(Failure),
81}
82
83pub struct EffectRegistry {
86 slots: std::sync::Mutex<HashMap<EffectInstanceKey, Arc<TokioMutex<EffectSlot>>>>,
87 acquisition_order: std::sync::Mutex<Vec<EffectInstanceKey>>,
91}
92
93impl Default for EffectRegistry {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl EffectRegistry {
100 pub fn new() -> Self {
101 Self {
102 slots: std::sync::Mutex::new(HashMap::new()),
103 acquisition_order: std::sync::Mutex::new(Vec::new()),
104 }
105 }
106
107 pub fn slot(&self, key: &EffectInstanceKey) -> Arc<TokioMutex<EffectSlot>> {
110 self.slots
111 .lock()
112 .expect("slot map mutex poisoned")
113 .entry(key.clone())
114 .or_insert_with(|| Arc::new(TokioMutex::new(EffectSlot::Empty)))
115 .clone()
116 }
117
118 pub fn record_acquisition(&self, key: EffectInstanceKey) {
120 self.acquisition_order
121 .lock()
122 .expect("acquisition order mutex poisoned")
123 .push(key);
124 }
125
126 pub fn acquired_keys(&self) -> Vec<EffectInstanceKey> {
128 self.acquisition_order
129 .lock()
130 .expect("acquisition order mutex poisoned")
131 .clone()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn test_key(name: &str) -> EffectInstanceKey {
140 EffectInstanceKey {
141 effect_id: DiagEffectId {
142 module: relux_core::diagnostics::ModulePath("test.relux".into()),
143 name: relux_core::diagnostics::EffectName(name.to_string()),
144 },
145 evaluated_overlay: String::new(),
146 }
147 }
148
149 fn test_key_with_overlay(name: &str, overlay: &str) -> EffectInstanceKey {
150 EffectInstanceKey {
151 effect_id: DiagEffectId {
152 module: relux_core::diagnostics::ModulePath("test.relux".into()),
153 name: relux_core::diagnostics::EffectName(name.to_string()),
154 },
155 evaluated_overlay: overlay.to_string(),
156 }
157 }
158
159 #[test]
160 fn key_equality_same() {
161 let k1 = test_key("Db");
162 let k2 = test_key("Db");
163 assert_eq!(k1, k2);
164 }
165
166 #[test]
167 fn key_equality_different_name() {
168 let k1 = test_key("Db");
169 let k2 = test_key("Redis");
170 assert_ne!(k1, k2);
171 }
172
173 #[test]
174 fn key_equality_different_overlay() {
175 let k1 = test_key_with_overlay("Db", "PORT=5432");
176 let k2 = test_key_with_overlay("Db", "PORT=5433");
177 assert_ne!(k1, k2);
178 }
179
180 #[test]
181 fn key_hash_consistent() {
182 use std::collections::hash_map::DefaultHasher;
183 use std::hash::Hash;
184 use std::hash::Hasher;
185 let k1 = test_key("Db");
186 let k2 = test_key("Db");
187 let mut h1 = DefaultHasher::new();
188 let mut h2 = DefaultHasher::new();
189 k1.hash(&mut h1);
190 k2.hash(&mut h2);
191 assert_eq!(h1.finish(), h2.finish());
192 }
193
194 #[test]
195 fn registry_new_is_empty() {
196 let reg = EffectRegistry::new();
197 assert!(reg.slots.lock().unwrap().is_empty());
198 }
199
200 #[tokio::test]
201 async fn slot_creates_empty_on_first_access() {
202 let reg = EffectRegistry::new();
203 let key = test_key("Db");
204 let slot = reg.slot(&key);
205 let guard = slot.lock().await;
206 assert!(matches!(*guard, EffectSlot::Empty));
207 }
208
209 #[tokio::test]
210 async fn slot_returns_same_arc_for_same_key() {
211 let reg = EffectRegistry::new();
212 let key = test_key("Db");
213 let s1 = reg.slot(&key);
214 let s2 = reg.slot(&key);
215 assert!(Arc::ptr_eq(&s1, &s2));
216 }
217
218 #[tokio::test]
219 async fn slot_returns_different_arcs_for_different_keys() {
220 let reg = EffectRegistry::new();
221 let k1 = test_key("Db");
222 let k2 = test_key("Redis");
223 let s1 = reg.slot(&k1);
224 let s2 = reg.slot(&k2);
225 assert!(!Arc::ptr_eq(&s1, &s2));
226 }
227
228 #[test]
229 fn acquired_keys_empty_registry() {
230 let reg = EffectRegistry::new();
231 assert!(reg.acquired_keys().is_empty());
232 }
233
234 #[test]
235 fn acquired_keys_preserves_order_and_duplicates() {
236 let reg = EffectRegistry::new();
237 let k1 = test_key("Db");
238 let k2 = test_key("Redis");
239 reg.record_acquisition(k1.clone());
240 reg.record_acquisition(k2.clone());
241 reg.record_acquisition(k1.clone());
242 let keys = reg.acquired_keys();
243 assert_eq!(keys.len(), 3);
244 assert_eq!(keys[0].effect_id.name.0, "Db");
245 assert_eq!(keys[1].effect_id.name.0, "Redis");
246 assert_eq!(keys[2].effect_id.name.0, "Db");
247 }
248
249 #[test]
250 fn from_expects_no_collision_when_value_contains_separator() {
251 use std::collections::HashMap;
255 let effect_id = DiagEffectId {
256 module: relux_core::diagnostics::ModulePath("test.relux".into()),
257 name: relux_core::diagnostics::EffectName("E".to_string()),
258 };
259
260 let mut overlay1 = HashMap::new();
261 overlay1.insert("A".into(), "x,B=y".into());
262 let env1 = relux_core::pure::Env::from_map(overlay1);
263
264 let mut overlay2 = HashMap::new();
265 overlay2.insert("A".into(), "x".into());
266 overlay2.insert("B".into(), "y".into());
267 let env2 = relux_core::pure::Env::from_map(overlay2);
268
269 let expects = &["A"];
270 let k1 = EffectInstanceKey::from_expects(effect_id.clone(), expects, &env1);
271 let k2 = EffectInstanceKey::from_expects(effect_id, expects, &env2);
272 assert_ne!(
273 k1, k2,
274 "different expect values must produce different keys"
275 );
276 }
277
278 #[test]
279 fn from_expects_uses_only_expected_keys() {
280 use std::collections::HashMap;
283 let effect_id = DiagEffectId {
284 module: relux_core::diagnostics::ModulePath("test.relux".into()),
285 name: relux_core::diagnostics::EffectName("E".to_string()),
286 };
287
288 let mut overlay1 = HashMap::new();
289 overlay1.insert("PORT".into(), "5432".into());
290 overlay1.insert("EXTRA".into(), "foo".into());
291 let env1 = relux_core::pure::Env::from_map(overlay1);
292
293 let mut overlay2 = HashMap::new();
294 overlay2.insert("PORT".into(), "5432".into());
295 overlay2.insert("EXTRA".into(), "bar".into());
296 let env2 = relux_core::pure::Env::from_map(overlay2);
297
298 let expects = &["PORT"];
299 let k1 = EffectInstanceKey::from_expects(effect_id.clone(), expects, &env1);
300 let k2 = EffectInstanceKey::from_expects(effect_id, expects, &env2);
301 assert_eq!(
302 k1, k2,
303 "extra overlay keys beyond expects should not affect identity"
304 );
305 }
306
307 #[test]
308 fn from_expects_declaration_order_is_stable() {
309 use std::collections::HashMap;
310 let effect_id = DiagEffectId {
311 module: relux_core::diagnostics::ModulePath("test.relux".into()),
312 name: relux_core::diagnostics::EffectName("E".to_string()),
313 };
314
315 let mut overlay = HashMap::new();
316 overlay.insert("A".into(), "1".into());
317 overlay.insert("B".into(), "2".into());
318 let env = relux_core::pure::Env::from_map(overlay);
319
320 let k1 = EffectInstanceKey::from_expects(effect_id.clone(), &["A", "B"], &env);
322 let k2 = EffectInstanceKey::from_expects(effect_id, &["A", "B"], &env);
323 assert_eq!(k1, k2);
324 }
325
326 #[test]
327 fn from_expects_empty_expects_produces_equal_keys() {
328 use std::collections::HashMap;
329 let effect_id = DiagEffectId {
330 module: relux_core::diagnostics::ModulePath("test.relux".into()),
331 name: relux_core::diagnostics::EffectName("E".to_string()),
332 };
333
334 let mut overlay1 = HashMap::new();
335 overlay1.insert("X".into(), "1".into());
336 let env1 = relux_core::pure::Env::from_map(overlay1);
337 let env2 = relux_core::pure::Env::from_map(HashMap::new());
338
339 let expects: &[&str] = &[];
340 let k1 = EffectInstanceKey::from_expects(effect_id.clone(), expects, &env1);
341 let k2 = EffectInstanceKey::from_expects(effect_id, expects, &env2);
342 assert_eq!(
343 k1, k2,
344 "effects with no expects should always share identity"
345 );
346 }
347
348 #[test]
349 fn acquired_keys_not_recorded_for_failed_slots() {
350 let reg = EffectRegistry::new();
352 let key = test_key("Broken");
354 reg.slot(&key);
355 assert!(reg.acquired_keys().is_empty());
356 }
357}