use std::collections::HashMap;
use std::time::Instant;
fn fnv1a(data: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut hash = FNV_OFFSET;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub fn hash_goal(goal_repr: &str) -> u64 {
fnv1a(goal_repr.as_bytes())
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct InferenceCacheKey {
pub goal_hash: u64,
pub kb_version: u64,
}
impl InferenceCacheKey {
pub fn new(goal_hash: u64, kb_version: u64) -> Self {
Self {
goal_hash,
kb_version,
}
}
}
pub struct CachedResult {
pub bindings: Vec<HashMap<String, String>>,
pub computed_at: Instant,
pub proof_depth: usize,
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub entries: usize,
pub hit_rate: f64,
}
struct LruOrder {
order: Vec<InferenceCacheKey>,
}
impl LruOrder {
fn new() -> Self {
Self { order: Vec::new() }
}
fn touch(&mut self, key: &InferenceCacheKey) {
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
self.order.insert(0, key.clone());
}
fn remove(&mut self, key: &InferenceCacheKey) {
self.order.retain(|k| k != key);
}
fn pop_lru(&mut self) -> Option<InferenceCacheKey> {
self.order.pop()
}
}
pub struct InferenceCache {
entries: HashMap<InferenceCacheKey, CachedResult>,
lru: LruOrder,
max_entries: usize,
hit_count: u64,
miss_count: u64,
eviction_count: u64,
}
impl InferenceCache {
pub fn new(max_entries: usize) -> Self {
let max_entries = if max_entries == 0 { 1024 } else { max_entries };
Self {
entries: HashMap::new(),
lru: LruOrder::new(),
max_entries,
hit_count: 0,
miss_count: 0,
eviction_count: 0,
}
}
pub fn get(&mut self, key: &InferenceCacheKey) -> Option<&CachedResult> {
if self.entries.contains_key(key) {
self.hit_count += 1;
self.lru.touch(key);
self.entries.get(key)
} else {
self.miss_count += 1;
None
}
}
pub fn insert(&mut self, key: InferenceCacheKey, result: CachedResult) {
if self.entries.contains_key(&key) {
self.entries.insert(key.clone(), result);
self.lru.touch(&key);
return;
}
while self.entries.len() >= self.max_entries {
if let Some(lru_key) = self.lru.pop_lru() {
self.entries.remove(&lru_key);
self.eviction_count += 1;
} else {
break;
}
}
self.lru.touch(&key);
self.entries.insert(key, result);
}
pub fn invalidate_for_kb_version(&mut self, old_version: u64) {
let keys_to_remove: Vec<InferenceCacheKey> = self
.entries
.keys()
.filter(|k| k.kb_version == old_version)
.cloned()
.collect();
for key in &keys_to_remove {
self.entries.remove(key);
self.lru.remove(key);
}
}
pub fn hit_rate(&self) -> f64 {
let total = self.hit_count + self.miss_count;
if total == 0 {
0.0
} else {
self.hit_count as f64 / total as f64
}
}
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hit_count,
misses: self.miss_count,
evictions: self.eviction_count,
entries: self.entries.len(),
hit_rate: self.hit_rate(),
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for InferenceCache {
fn default() -> Self {
Self::new(1024)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_result(bindings: Vec<HashMap<String, String>>) -> CachedResult {
CachedResult {
bindings,
computed_at: Instant::now(),
proof_depth: 1,
}
}
#[test]
fn test_cache_hit() {
let mut cache = InferenceCache::new(1024);
let key = InferenceCacheKey::new(42, 1);
let mut binding = HashMap::new();
binding.insert("X".to_string(), "alice".to_string());
cache.insert(key.clone(), make_result(vec![binding.clone()]));
let result = cache.get(&key);
assert!(result.is_some());
let r = result.expect("test: should succeed");
assert_eq!(r.bindings.len(), 1);
assert_eq!(r.bindings[0].get("X").map(|s| s.as_str()), Some("alice"));
}
#[test]
fn test_cache_miss() {
let mut cache = InferenceCache::new(1024);
let key = InferenceCacheKey::new(99, 1);
let result = cache.get(&key);
assert!(result.is_none());
assert_eq!(cache.stats().misses, 1);
assert_eq!(cache.stats().hits, 0);
}
#[test]
fn test_cache_invalidation() {
let mut cache = InferenceCache::new(1024);
let key1 = InferenceCacheKey::new(1, 1);
let key2 = InferenceCacheKey::new(2, 1);
let key3 = InferenceCacheKey::new(3, 2);
cache.insert(key1.clone(), make_result(vec![]));
cache.insert(key2.clone(), make_result(vec![]));
cache.insert(key3.clone(), make_result(vec![]));
assert_eq!(cache.len(), 3);
cache.invalidate_for_kb_version(1);
assert_eq!(cache.len(), 1);
assert!(cache.get(&key1).is_none());
assert!(cache.get(&key2).is_none());
let stats_before = cache.stats();
let _ = cache.get(&key3);
let stats_after = cache.stats();
assert_eq!(stats_after.hits, stats_before.hits + 1);
}
#[test]
fn test_cache_hit_rate() {
let mut cache = InferenceCache::new(1024);
let key = InferenceCacheKey::new(7, 1);
cache.insert(key.clone(), make_result(vec![]));
cache.get(&key);
cache.get(&key);
cache.get(&key);
let missing = InferenceCacheKey::new(999, 1);
cache.get(&missing);
let rate = cache.hit_rate();
assert!((rate - 0.75).abs() < 1e-9, "expected 0.75 but got {}", rate);
}
#[test]
fn test_kb_version_bumps_on_rule_add() {
use crate::ir::{Predicate, Rule, Term};
use crate::storage::TensorLogicStore;
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
use std::sync::Arc;
let path = std::env::temp_dir().join("ipfrs-test-kb-version-bump");
let _ = std::fs::remove_dir_all(&path);
let config = BlockStoreConfig {
path,
cache_size: 32 * 1024 * 1024,
};
let store = Arc::new(SledBlockStore::new(config).expect("sled store"));
let tl_store = TensorLogicStore::new(store).expect("tensorlogic store");
let version_before = tl_store.kb_version();
let rule = Rule::new(
Predicate::new(
"ancestor".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
),
vec![Predicate::new(
"parent".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
)],
);
tl_store.add_rule(rule).expect("add_rule");
let version_after = tl_store.kb_version();
assert!(
version_after > version_before,
"kb_version should increment after add_rule: before={}, after={}",
version_before,
version_after,
);
}
}