Skip to main content

oxiz_solver/conflict/
explanation_cache.rs

1//! Explanation Cache for Conflict Analysis.
2//!
3//! Caches theory explanations to avoid redundant computation during
4//! conflict analysis and proof generation.
5//!
6//! ## Features
7//!
8//! - **LRU eviction**: Maintains cache size with least-recently-used policy
9//! - **Explanation reuse**: Avoids recomputing identical explanations
10//! - **Proof sharing**: Shares proof terms across multiple conflicts
11//!
12//! ## References
13//!
14//! - Z3's `smt/smt_theory.cpp` explanation caching
15
16use oxiz_sat::Lit;
17use rustc_hash::FxHashMap;
18use std::collections::VecDeque;
19
20/// Cache key (theory conflict identifier).
21pub type CacheKey = u64;
22
23/// Explanation for a theory conflict.
24#[derive(Debug, Clone)]
25pub struct Explanation {
26    /// Literals in the explanation.
27    pub literals: Vec<Lit>,
28    /// Proof term (optional).
29    pub proof: Option<String>,
30    /// Timestamp (for LRU).
31    timestamp: u64,
32}
33
34/// Configuration for explanation cache.
35#[derive(Debug, Clone)]
36pub struct CacheConfig {
37    /// Maximum cache entries.
38    pub max_entries: usize,
39    /// Enable proof caching.
40    pub cache_proofs: bool,
41    /// Enable LRU eviction.
42    pub use_lru: bool,
43}
44
45impl Default for CacheConfig {
46    fn default() -> Self {
47        Self {
48            max_entries: 10_000,
49            cache_proofs: true,
50            use_lru: true,
51        }
52    }
53}
54
55/// Statistics for explanation cache.
56#[derive(Debug, Clone, Default)]
57pub struct CacheStats {
58    /// Cache hits.
59    pub hits: u64,
60    /// Cache misses.
61    pub misses: u64,
62    /// Cache evictions.
63    pub evictions: u64,
64    /// Total explanations cached.
65    pub cached: u64,
66}
67
68impl CacheStats {
69    /// Get hit rate (0.0 - 1.0).
70    pub fn hit_rate(&self) -> f64 {
71        let total = self.hits + self.misses;
72        if total == 0 {
73            0.0
74        } else {
75            self.hits as f64 / total as f64
76        }
77    }
78}
79
80/// Explanation cache with LRU eviction.
81#[derive(Debug)]
82pub struct ExplanationCache {
83    /// Cache map.
84    cache: FxHashMap<CacheKey, Explanation>,
85    /// LRU queue (most recent at back).
86    lru_queue: VecDeque<CacheKey>,
87    /// Current timestamp.
88    timestamp: u64,
89    /// Configuration.
90    config: CacheConfig,
91    /// Statistics.
92    stats: CacheStats,
93}
94
95impl ExplanationCache {
96    /// Create a new explanation cache.
97    pub fn new(config: CacheConfig) -> Self {
98        Self {
99            cache: FxHashMap::default(),
100            lru_queue: VecDeque::new(),
101            timestamp: 0,
102            config,
103            stats: CacheStats::default(),
104        }
105    }
106
107    /// Create with default configuration.
108    pub fn default_config() -> Self {
109        Self::new(CacheConfig::default())
110    }
111
112    /// Insert an explanation into the cache.
113    pub fn insert(&mut self, key: CacheKey, literals: Vec<Lit>, proof: Option<String>) {
114        // Check if we need to evict
115        if self.cache.len() >= self.config.max_entries && !self.cache.contains_key(&key) {
116            self.evict_lru();
117        }
118
119        let explanation = Explanation {
120            literals,
121            proof: if self.config.cache_proofs {
122                proof
123            } else {
124                None
125            },
126            timestamp: self.timestamp,
127        };
128
129        self.cache.insert(key, explanation);
130        if self.config.use_lru {
131            self.lru_queue.push_back(key);
132        }
133        self.stats.cached += 1;
134        self.timestamp += 1;
135    }
136
137    /// Get an explanation from the cache.
138    pub fn get(&mut self, key: CacheKey) -> Option<&Explanation> {
139        if let Some(explanation) = self.cache.get_mut(&key) {
140            // Update timestamp for LRU
141            explanation.timestamp = self.timestamp;
142            self.timestamp += 1;
143            self.stats.hits += 1;
144
145            // Move to back of LRU queue
146            if self.config.use_lru {
147                self.lru_queue.retain(|&k| k != key);
148                self.lru_queue.push_back(key);
149            }
150
151            Some(explanation)
152        } else {
153            self.stats.misses += 1;
154            None
155        }
156    }
157
158    /// Evict the least-recently-used entry.
159    fn evict_lru(&mut self) {
160        if let Some(key) = self.lru_queue.pop_front() {
161            self.cache.remove(&key);
162            self.stats.evictions += 1;
163        }
164    }
165
166    /// Clear the cache.
167    pub fn clear(&mut self) {
168        self.cache.clear();
169        self.lru_queue.clear();
170        self.timestamp = 0;
171    }
172
173    /// Get cache size.
174    pub fn len(&self) -> usize {
175        self.cache.len()
176    }
177
178    /// Check if cache is empty.
179    pub fn is_empty(&self) -> bool {
180        self.cache.is_empty()
181    }
182
183    /// Get statistics.
184    pub fn stats(&self) -> &CacheStats {
185        &self.stats
186    }
187
188    /// Reset statistics.
189    pub fn reset_stats(&mut self) {
190        self.stats = CacheStats::default();
191    }
192}
193
194impl Default for ExplanationCache {
195    fn default() -> Self {
196        Self::default_config()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_cache_creation() {
206        let cache = ExplanationCache::default_config();
207        assert!(cache.is_empty());
208        assert_eq!(cache.len(), 0);
209    }
210
211    #[test]
212    fn test_insert_and_get() {
213        use oxiz_sat::Var;
214        let mut cache = ExplanationCache::default_config();
215        let key = 42;
216        let literals = vec![Lit::pos(Var::new(0)), Lit::neg(Var::new(1))];
217
218        cache.insert(key, literals.clone(), None);
219        assert_eq!(cache.len(), 1);
220
221        let retrieved = cache.get(key).unwrap();
222        assert_eq!(retrieved.literals.len(), 2);
223        assert_eq!(cache.stats().hits, 1);
224    }
225
226    #[test]
227    fn test_cache_miss() {
228        let mut cache = ExplanationCache::default_config();
229        let result = cache.get(999);
230        assert!(result.is_none());
231        assert_eq!(cache.stats().misses, 1);
232    }
233
234    #[test]
235    fn test_lru_eviction() {
236        let config = CacheConfig {
237            max_entries: 2,
238            ..Default::default()
239        };
240        let mut cache = ExplanationCache::new(config);
241
242        // Insert 3 entries (should evict first)
243        cache.insert(1, vec![], None);
244        cache.insert(2, vec![], None);
245        cache.insert(3, vec![], None);
246
247        assert_eq!(cache.len(), 2);
248        assert_eq!(cache.stats().evictions, 1);
249
250        // First entry should be evicted
251        assert!(cache.get(1).is_none());
252        assert!(cache.get(2).is_some());
253        assert!(cache.get(3).is_some());
254    }
255
256    #[test]
257    fn test_hit_rate() {
258        let mut cache = ExplanationCache::default_config();
259        cache.insert(1, vec![], None);
260
261        cache.get(1); // hit
262        cache.get(2); // miss
263        cache.get(1); // hit
264
265        assert_eq!(cache.stats().hits, 2);
266        assert_eq!(cache.stats().misses, 1);
267        assert!((cache.stats().hit_rate() - 0.666).abs() < 0.01);
268    }
269
270    #[test]
271    fn test_clear() {
272        let mut cache = ExplanationCache::default_config();
273        cache.insert(1, vec![], None);
274        cache.insert(2, vec![], None);
275
276        cache.clear();
277        assert!(cache.is_empty());
278    }
279
280    #[test]
281    fn test_proof_caching() {
282        let mut cache = ExplanationCache::default_config();
283        let key = 100;
284        let proof = Some("(proof-term ...)".to_string());
285
286        cache.insert(key, vec![], proof.clone());
287
288        let retrieved = cache.get(key).unwrap();
289        assert_eq!(retrieved.proof, proof);
290    }
291
292    #[test]
293    fn test_stats() {
294        let mut cache = ExplanationCache::default_config();
295        cache.insert(1, vec![], None);
296        cache.get(1);
297
298        assert_eq!(cache.stats().cached, 1);
299        assert_eq!(cache.stats().hits, 1);
300
301        cache.reset_stats();
302        assert_eq!(cache.stats().cached, 0);
303        assert_eq!(cache.stats().hits, 0);
304    }
305}