1use std::collections::HashMap;
8use std::time::Instant;
9
10fn fnv1a(data: &[u8]) -> u64 {
12 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
13 const FNV_PRIME: u64 = 1_099_511_628_211;
14 let mut hash = FNV_OFFSET;
15 for &byte in data {
16 hash ^= byte as u64;
17 hash = hash.wrapping_mul(FNV_PRIME);
18 }
19 hash
20}
21
22pub fn hash_goal(goal_repr: &str) -> u64 {
24 fnv1a(goal_repr.as_bytes())
25}
26
27#[derive(Debug, Clone, Hash, Eq, PartialEq)]
29pub struct InferenceCacheKey {
30 pub goal_hash: u64,
32 pub kb_version: u64,
34}
35
36impl InferenceCacheKey {
37 pub fn new(goal_hash: u64, kb_version: u64) -> Self {
39 Self {
40 goal_hash,
41 kb_version,
42 }
43 }
44}
45
46pub struct CachedResult {
48 pub bindings: Vec<HashMap<String, String>>,
50 pub computed_at: Instant,
52 pub proof_depth: usize,
54}
55
56#[derive(Debug, Clone)]
58pub struct CacheStats {
59 pub hits: u64,
61 pub misses: u64,
63 pub evictions: u64,
65 pub entries: usize,
67 pub hit_rate: f64,
69}
70
71struct LruOrder {
74 order: Vec<InferenceCacheKey>,
75}
76
77impl LruOrder {
78 fn new() -> Self {
79 Self { order: Vec::new() }
80 }
81
82 fn touch(&mut self, key: &InferenceCacheKey) {
84 if let Some(pos) = self.order.iter().position(|k| k == key) {
85 self.order.remove(pos);
86 }
87 self.order.insert(0, key.clone());
88 }
89
90 fn remove(&mut self, key: &InferenceCacheKey) {
92 self.order.retain(|k| k != key);
93 }
94
95 fn pop_lru(&mut self) -> Option<InferenceCacheKey> {
97 self.order.pop()
98 }
99}
100
101pub struct InferenceCache {
104 entries: HashMap<InferenceCacheKey, CachedResult>,
105 lru: LruOrder,
106 max_entries: usize,
107 hit_count: u64,
108 miss_count: u64,
109 eviction_count: u64,
110}
111
112impl InferenceCache {
113 pub fn new(max_entries: usize) -> Self {
115 let max_entries = if max_entries == 0 { 1024 } else { max_entries };
116 Self {
117 entries: HashMap::new(),
118 lru: LruOrder::new(),
119 max_entries,
120 hit_count: 0,
121 miss_count: 0,
122 eviction_count: 0,
123 }
124 }
125
126 pub fn get(&mut self, key: &InferenceCacheKey) -> Option<&CachedResult> {
128 if self.entries.contains_key(key) {
129 self.hit_count += 1;
130 self.lru.touch(key);
131 self.entries.get(key)
132 } else {
133 self.miss_count += 1;
134 None
135 }
136 }
137
138 pub fn insert(&mut self, key: InferenceCacheKey, result: CachedResult) {
140 if self.entries.contains_key(&key) {
141 self.entries.insert(key.clone(), result);
143 self.lru.touch(&key);
144 return;
145 }
146
147 while self.entries.len() >= self.max_entries {
149 if let Some(lru_key) = self.lru.pop_lru() {
150 self.entries.remove(&lru_key);
151 self.eviction_count += 1;
152 } else {
153 break;
154 }
155 }
156
157 self.lru.touch(&key);
158 self.entries.insert(key, result);
159 }
160
161 pub fn invalidate_for_kb_version(&mut self, old_version: u64) {
163 let keys_to_remove: Vec<InferenceCacheKey> = self
164 .entries
165 .keys()
166 .filter(|k| k.kb_version == old_version)
167 .cloned()
168 .collect();
169
170 for key in &keys_to_remove {
171 self.entries.remove(key);
172 self.lru.remove(key);
173 }
174 }
175
176 pub fn hit_rate(&self) -> f64 {
178 let total = self.hit_count + self.miss_count;
179 if total == 0 {
180 0.0
181 } else {
182 self.hit_count as f64 / total as f64
183 }
184 }
185
186 pub fn stats(&self) -> CacheStats {
188 CacheStats {
189 hits: self.hit_count,
190 misses: self.miss_count,
191 evictions: self.eviction_count,
192 entries: self.entries.len(),
193 hit_rate: self.hit_rate(),
194 }
195 }
196
197 pub fn len(&self) -> usize {
199 self.entries.len()
200 }
201
202 pub fn is_empty(&self) -> bool {
204 self.entries.is_empty()
205 }
206}
207
208impl Default for InferenceCache {
209 fn default() -> Self {
210 Self::new(1024)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 fn make_result(bindings: Vec<HashMap<String, String>>) -> CachedResult {
219 CachedResult {
220 bindings,
221 computed_at: Instant::now(),
222 proof_depth: 1,
223 }
224 }
225
226 #[test]
227 fn test_cache_hit() {
228 let mut cache = InferenceCache::new(1024);
229 let key = InferenceCacheKey::new(42, 1);
230 let mut binding = HashMap::new();
231 binding.insert("X".to_string(), "alice".to_string());
232 cache.insert(key.clone(), make_result(vec![binding.clone()]));
233
234 let result = cache.get(&key);
235 assert!(result.is_some());
236 let r = result.expect("test: should succeed");
237 assert_eq!(r.bindings.len(), 1);
238 assert_eq!(r.bindings[0].get("X").map(|s| s.as_str()), Some("alice"));
239 }
240
241 #[test]
242 fn test_cache_miss() {
243 let mut cache = InferenceCache::new(1024);
244 let key = InferenceCacheKey::new(99, 1);
245
246 let result = cache.get(&key);
247 assert!(result.is_none());
248 assert_eq!(cache.stats().misses, 1);
249 assert_eq!(cache.stats().hits, 0);
250 }
251
252 #[test]
253 fn test_cache_invalidation() {
254 let mut cache = InferenceCache::new(1024);
255
256 let key1 = InferenceCacheKey::new(1, 1);
258 let key2 = InferenceCacheKey::new(2, 1);
259 let key3 = InferenceCacheKey::new(3, 2);
261
262 cache.insert(key1.clone(), make_result(vec![]));
263 cache.insert(key2.clone(), make_result(vec![]));
264 cache.insert(key3.clone(), make_result(vec![]));
265
266 assert_eq!(cache.len(), 3);
267
268 cache.invalidate_for_kb_version(1);
270
271 assert_eq!(cache.len(), 1);
272 assert!(cache.get(&key1).is_none());
273 assert!(cache.get(&key2).is_none());
274 let stats_before = cache.stats();
276 let _ = cache.get(&key3);
277 let stats_after = cache.stats();
278 assert_eq!(stats_after.hits, stats_before.hits + 1);
279 }
280
281 #[test]
282 fn test_cache_hit_rate() {
283 let mut cache = InferenceCache::new(1024);
284 let key = InferenceCacheKey::new(7, 1);
285 cache.insert(key.clone(), make_result(vec![]));
286
287 cache.get(&key);
289 cache.get(&key);
290 cache.get(&key);
291
292 let missing = InferenceCacheKey::new(999, 1);
294 cache.get(&missing);
295
296 let rate = cache.hit_rate();
297 assert!((rate - 0.75).abs() < 1e-9, "expected 0.75 but got {}", rate);
299 }
300
301 #[test]
302 fn test_kb_version_bumps_on_rule_add() {
303 use crate::ir::{Predicate, Rule, Term};
304 use crate::storage::TensorLogicStore;
305 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
306 use std::sync::Arc;
307
308 let path = std::env::temp_dir().join("ipfrs-test-kb-version-bump");
309 let _ = std::fs::remove_dir_all(&path);
310 let config = BlockStoreConfig {
311 path,
312 cache_size: 32 * 1024 * 1024,
313 };
314 let store = Arc::new(SledBlockStore::new(config).expect("sled store"));
315 let tl_store = TensorLogicStore::new(store).expect("tensorlogic store");
316
317 let version_before = tl_store.kb_version();
318
319 let rule = Rule::new(
320 Predicate::new(
321 "ancestor".to_string(),
322 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
323 ),
324 vec![Predicate::new(
325 "parent".to_string(),
326 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
327 )],
328 );
329
330 tl_store.add_rule(rule).expect("add_rule");
331
332 let version_after = tl_store.kb_version();
333 assert!(
334 version_after > version_before,
335 "kb_version should increment after add_rule: before={}, after={}",
336 version_before,
337 version_after,
338 );
339 }
340}