Skip to main content

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 dashmap::DashMap;
8use once_cell::sync::Lazy;
9use rapidhash::fast::RapidHasher;
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/// Recursively hash a serde_json::Value without serializing to string.
39/// Uses type discriminants to avoid hash collisions between different JSON types.
40#[inline]
41fn hash_value(value: &serde_json::Value, hasher: &mut RapidHasher) {
42    match value {
43        serde_json::Value::Null => 0u8.hash(hasher),
44        serde_json::Value::Bool(b) => {
45            1u8.hash(hasher);
46            b.hash(hasher);
47        }
48        serde_json::Value::Number(n) => {
49            2u8.hash(hasher);
50            n.as_f64().unwrap_or(0.0).to_bits().hash(hasher);
51        }
52        serde_json::Value::String(s) => {
53            3u8.hash(hasher);
54            s.hash(hasher);
55        }
56        serde_json::Value::Array(arr) => {
57            4u8.hash(hasher);
58            arr.len().hash(hasher);
59            for item in arr {
60                hash_value(item, hasher);
61            }
62        }
63        serde_json::Value::Object(obj) => {
64            5u8.hash(hasher);
65            obj.len().hash(hasher);
66            for (k, v) in obj {
67                k.hash(hasher);
68                hash_value(v, hasher);
69            }
70        }
71    }
72}
73
74/// Thread-safe global store for compiled logic
75struct CompiledLogicStore {
76    /// Map from hash to (ID, CompiledLogic)
77    store: DashMap<u64, (CompiledLogicId, CompiledLogic)>,
78    /// Reverse map from ID to CompiledLogic for fast lookup
79    id_map: DashMap<u64, CompiledLogic>,
80    /// Next available ID
81    next_id: AtomicU64,
82}
83
84impl CompiledLogicStore {
85    /// Compile logic from a Value and return an ID
86    /// If the same logic was compiled before, returns the existing ID
87    fn compile_value(&self, logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
88        let mut hasher = RapidHasher::default();
89        hash_value(logic, &mut hasher);
90        let hash = hasher.finish();
91
92        // Optimistic check (read lock)
93        if let Some(entry) = self.store.get(&hash) {
94            return Ok(entry.0);
95        }
96
97        // Compile using the shared CompiledLogic::compile method
98        // We compile before acquiring write lock to avoid holding it during compilation
99        let compiled = CompiledLogic::compile(logic)?;
100
101        // Atomic check-and-insert using entry API
102        match self.store.entry(hash) {
103            dashmap::mapref::entry::Entry::Occupied(o) => {
104                // Another thread beat us to it, return existing ID
105                Ok(o.get().0)
106            }
107            dashmap::mapref::entry::Entry::Vacant(v) => {
108                // Generate new ID and insert
109                let id = CompiledLogicId(self.next_id.fetch_add(1, Ordering::SeqCst));
110                // Insert into id_map FIRST so it's available as soon as it's visible in store
111                self.id_map.insert(id.0, compiled.clone());
112                v.insert((id, compiled));
113                Ok(id)
114            }
115        }
116    }
117
118    /// Compile logic from a JSON string and return an ID
119    /// If the same logic was compiled before, returns the existing ID
120    fn compile(&self, logic_json: &str) -> Result<CompiledLogicId, String> {
121        // Parse JSON
122        let logic: serde_json::Value = serde_json::from_str(logic_json)
123            .map_err(|e| format!("Failed to parse logic JSON: {}", e))?;
124
125        // Use shared compile_value method
126        self.compile_value(&logic)
127    }
128
129    /// Get compiled logic by ID (O(1) lookup)
130    fn get(&self, id: CompiledLogicId) -> Option<CompiledLogic> {
131        self.id_map.get(&id.0).map(|v| v.clone())
132    }
133
134    /// Get statistics about the store
135    fn stats(&self) -> CompiledLogicStoreStats {
136        CompiledLogicStoreStats {
137            compiled_count: self.store.len(),
138            next_id: self.next_id.load(Ordering::SeqCst),
139        }
140    }
141
142    /// Clear all compiled logic (useful for testing)
143    #[allow(dead_code)]
144    fn clear(&self) {
145        self.store.clear();
146        self.id_map.clear();
147        self.next_id.store(1, Ordering::SeqCst);
148    }
149}
150
151/// Statistics about the compiled logic store
152#[derive(Debug, Clone)]
153pub struct CompiledLogicStoreStats {
154    /// Number of compiled logic expressions stored
155    pub compiled_count: usize,
156    /// Next ID that will be assigned
157    pub next_id: u64,
158}
159
160/// Compile logic from a JSON string and return a unique ID
161///
162/// The compiled logic is stored in a global thread-safe cache.
163/// If the same logic was compiled before, returns the existing ID.
164pub fn compile_logic(logic_json: &str) -> Result<CompiledLogicId, String> {
165    COMPILED_LOGIC_STORE.compile(logic_json)
166}
167
168/// Compile logic from a Value and return a unique ID
169///
170/// The compiled logic is stored in a global thread-safe cache.
171/// If the same logic was compiled before, returns the existing ID.
172pub fn compile_logic_value(logic: &serde_json::Value) -> Result<CompiledLogicId, String> {
173    COMPILED_LOGIC_STORE.compile_value(logic)
174}
175
176/// Get compiled logic by ID
177pub fn get_compiled_logic(id: CompiledLogicId) -> Option<CompiledLogic> {
178    COMPILED_LOGIC_STORE.get(id)
179}
180
181/// Get statistics about the global compiled logic store
182pub fn get_store_stats() -> CompiledLogicStoreStats {
183    COMPILED_LOGIC_STORE.stats()
184}
185
186/// Clear all compiled logic from the global store
187///
188/// **Warning**: This will invalidate all existing CompiledLogicIds
189#[cfg(test)]
190pub fn clear_store() {
191    COMPILED_LOGIC_STORE.clear()
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_compile_and_get() {
200        // Don't clear store to avoid breaking tests
201
202        let logic = r#"{"==": [{"var": "x"}, 10]}"#;
203        let id = compile_logic(logic).expect("Failed to compile");
204
205        let compiled = get_compiled_logic(id);
206        assert!(compiled.is_some());
207    }
208
209    #[test]
210    fn test_deduplication() {
211        let logic = r#"{"*": [{"var": "a"}, 2]}"#;
212
213        let id1 = compile_logic(logic).expect("Failed to compile");
214        let id2 = compile_logic(logic).expect("Failed to compile");
215
216        // Same logic should return same ID
217        assert_eq!(id1, id2);
218    }
219
220    #[test]
221    fn test_different_logic() {
222        let logic1 = r#"{"*": [{"var": "a"}, 2]}"#;
223        let logic2 = r#"{"*": [{"var": "b"}, 3]}"#;
224
225        let id1 = compile_logic(logic1).expect("Failed to compile");
226        let id2 = compile_logic(logic2).expect("Failed to compile");
227
228        // Different logic should return different IDs
229        assert_ne!(id1, id2);
230    }
231
232    #[test]
233    fn test_stats() {
234        // Check baseline
235        let stats_before = get_store_stats();
236
237        // Compile some logic to populate the store
238        let logic = r#"{"+": [1, 2, 3]}"#;
239        let _ = compile_logic(logic).expect("Failed to compile");
240
241        let stats_after = get_store_stats();
242        // Should have at least one more (or same if already existed from other tests, but likely unique)
243        // With tests, exact count is hard.
244        // Just verify stats are accessible.
245        assert!(stats_after.compiled_count >= stats_before.compiled_count);
246        assert!(stats_after.next_id >= stats_before.next_id);
247    }
248}