json_eval_rs/rlogic/
compiled_logic_store.rs

1//! Global storage for compiled logic expressions
2//!
3//! This module provides a thread-safe global store for compiled logic that can be shared
4//! across different JSONEval instances and across FFI boundaries.
5
6use super::CompiledLogic;
7use ahash::AHasher;
8use dashmap::DashMap;
9use once_cell::sync::Lazy;
10use std::hash::{Hash, Hasher};
11use std::sync::atomic::{AtomicU64, Ordering};
12
13/// Unique identifier for a compiled logic expression
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct CompiledLogicId(u64);
16
17impl CompiledLogicId {
18    /// Get the underlying u64 value
19    pub fn as_u64(&self) -> u64 {
20        self.0
21    }
22
23    /// Create from u64 value
24    pub fn from_u64(id: u64) -> Self {
25        Self(id)
26    }
27}
28
29/// Global storage for compiled logic expressions
30static COMPILED_LOGIC_STORE: Lazy<CompiledLogicStore> = Lazy::new(|| {
31    CompiledLogicStore {
32        store: DashMap::new(),
33        id_map: DashMap::new(),
34        next_id: AtomicU64::new(1), // Start from 1, 0 reserved for invalid
35    }
36});
37
38/// Thread-safe global store for compiled logic
39struct CompiledLogicStore {
40    /// Map from hash to (ID, CompiledLogic)
41    store: DashMap<u64, (CompiledLogicId, CompiledLogic)>,
42    /// Reverse map from ID to CompiledLogic for fast lookup
43    id_map: DashMap<u64, CompiledLogic>,
44    /// Next available ID
45    next_id: AtomicU64,
46}
47
48impl CompiledLogicStore {
49    /// Compile logic from a Value and return an ID
50    /// If the same logic was compiled before, returns the existing ID
51    fn compile_value(&self, logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
52        // Hash the logic value for deduplication
53        let logic_str = serde_json::to_string(logic)
54            .map_err(|e| format!("Failed to serialize logic: {}", e))?;
55        let mut hasher = AHasher::default();
56        logic_str.hash(&mut hasher);
57        let hash = hasher.finish();
58
59        // Optimistic check (read lock)
60        if let Some(entry) = self.store.get(&hash) {
61            return Ok(entry.0);
62        }
63
64        // Compile using the shared CompiledLogic::compile method
65        // We compile before acquiring write lock to avoid holding it during compilation
66        let compiled = CompiledLogic::compile(logic)?;
67
68        // Atomic check-and-insert using entry API
69        match self.store.entry(hash) {
70            dashmap::mapref::entry::Entry::Occupied(o) => {
71                // Another thread beat us to it, return existing ID
72                Ok(o.get().0)
73            }
74            dashmap::mapref::entry::Entry::Vacant(v) => {
75                // Generate new ID and insert
76                let id = CompiledLogicId(self.next_id.fetch_add(1, Ordering::SeqCst));
77                // Insert into id_map FIRST so it's available as soon as it's visible in store
78                self.id_map.insert(id.0, compiled.clone());
79                v.insert((id, compiled));
80                Ok(id)
81            }
82        }
83    }
84
85    /// Compile logic from a JSON string and return an ID
86    /// If the same logic was compiled before, returns the existing ID
87    fn compile(&self, logic_json: &str) -> Result<CompiledLogicId, String> {
88        // Parse JSON
89        let logic: serde_json::Value = serde_json::from_str(logic_json)
90            .map_err(|e| format!("Failed to parse logic JSON: {}", e))?;
91
92        // Use shared compile_value method
93        self.compile_value(&logic)
94    }
95
96    /// Get compiled logic by ID (O(1) lookup)
97    fn get(&self, id: CompiledLogicId) -> Option<CompiledLogic> {
98        self.id_map.get(&id.0).map(|v| v.clone())
99    }
100
101    /// Get statistics about the store
102    fn stats(&self) -> CompiledLogicStoreStats {
103        CompiledLogicStoreStats {
104            compiled_count: self.store.len(),
105            next_id: self.next_id.load(Ordering::SeqCst),
106        }
107    }
108
109    /// Clear all compiled logic (useful for testing)
110    #[allow(dead_code)]
111    fn clear(&self) {
112        self.store.clear();
113        self.id_map.clear();
114        self.next_id.store(1, Ordering::SeqCst);
115    }
116}
117
118/// Statistics about the compiled logic store
119#[derive(Debug, Clone)]
120pub struct CompiledLogicStoreStats {
121    /// Number of compiled logic expressions stored
122    pub compiled_count: usize,
123    /// Next ID that will be assigned
124    pub next_id: u64,
125}
126
127/// Compile logic from a JSON string and return a unique ID
128///
129/// The compiled logic is stored in a global thread-safe cache.
130/// If the same logic was compiled before, returns the existing ID.
131pub fn compile_logic(logic_json: &str) -> Result<CompiledLogicId, String> {
132    COMPILED_LOGIC_STORE.compile(logic_json)
133}
134
135/// Compile logic from a Value and return a unique ID
136///
137/// The compiled logic is stored in a global thread-safe cache.
138/// If the same logic was compiled before, returns the existing ID.
139pub fn compile_logic_value(logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
140    COMPILED_LOGIC_STORE.compile_value(logic)
141}
142
143/// Get compiled logic by ID
144pub fn get_compiled_logic(id: CompiledLogicId) -> Option<CompiledLogic> {
145    COMPILED_LOGIC_STORE.get(id)
146}
147
148/// Get statistics about the global compiled logic store
149pub fn get_store_stats() -> CompiledLogicStoreStats {
150    COMPILED_LOGIC_STORE.stats()
151}
152
153/// Clear all compiled logic from the global store
154///
155/// **Warning**: This will invalidate all existing CompiledLogicIds
156#[cfg(test)]
157pub fn clear_store() {
158    COMPILED_LOGIC_STORE.clear()
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_compile_and_get() {
167        // Don't clear store to avoid breaking parallel tests
168        
169        let logic = r#"{"==": [{"var": "x"}, 10]}"#;
170        let id = compile_logic(logic).expect("Failed to compile");
171
172        let compiled = get_compiled_logic(id);
173        assert!(compiled.is_some());
174    }
175
176    #[test]
177    fn test_deduplication() {
178        let logic = r#"{"*": [{"var": "a"}, 2]}"#;
179
180        let id1 = compile_logic(logic).expect("Failed to compile");
181        let id2 = compile_logic(logic).expect("Failed to compile");
182
183        // Same logic should return same ID
184        assert_eq!(id1, id2);
185    }
186
187    #[test]
188    fn test_different_logic() {
189        let logic1 = r#"{"*": [{"var": "a"}, 2]}"#;
190        let logic2 = r#"{"*": [{"var": "b"}, 3]}"#;
191
192        let id1 = compile_logic(logic1).expect("Failed to compile");
193        let id2 = compile_logic(logic2).expect("Failed to compile");
194
195        // Different logic should return different IDs
196        assert_ne!(id1, id2);
197    }
198
199    #[test]
200    fn test_stats() {
201        // Check baseline
202        let stats_before = get_store_stats();
203        
204        // Compile some logic to populate the store
205        let logic = r#"{"+": [1, 2, 3]}"#;
206        let _ = compile_logic(logic).expect("Failed to compile");
207
208        let stats_after = get_store_stats();
209        // Should have at least one more (or same if already existed from other tests, but likely unique)
210        // With parallel tests, exact count is hard. 
211        // Just verify stats are accessible.
212        assert!(stats_after.compiled_count >= stats_before.compiled_count);
213        assert!(stats_after.next_id >= stats_before.next_id);
214    }
215}