use crate::inference_cache::InferenceCache;
use crate::ir::{KnowledgeBase, Predicate, Rule, Term};
use crate::reasoning::{InferenceEngine, Proof, Substitution};
use ipfrs_core::{Block, Cid, Result};
use ipfrs_storage::traits::BlockStore;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct TensorLogicStoreStats {
pub rule_count: usize,
pub fact_count: usize,
pub cid_indexed_rules: usize,
}
pub struct TensorLogicPersistenceConfig {
pub snapshot_path: std::path::PathBuf,
pub auto_save: bool,
pub snapshot_interval: std::time::Duration,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KnowledgeBaseSnapshot {
pub version: u32,
pub rules: Vec<RuleSnapshot>,
pub facts: Vec<FactSnapshot>,
pub created_at: u64,
pub rule_count: usize,
pub fact_count: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RuleSnapshot {
pub head_predicate: String,
pub head_args: Vec<String>,
pub body_goals: Vec<String>,
pub cid: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FactSnapshot {
pub predicate: String,
pub args: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum TensorLogicError {
#[error("Snapshot IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Snapshot serialization error: {0}")]
Serialization(String),
#[error("Lock poisoned: {0}")]
LockPoisoned(String),
}
pub struct TensorLogicStore<S: BlockStore> {
store: Arc<S>,
knowledge_base: std::sync::RwLock<KnowledgeBase>,
engine: InferenceEngine,
dirty: AtomicBool,
inference_times: Mutex<VecDeque<Duration>>,
kb_version: AtomicU64,
inference_cache: Mutex<InferenceCache>,
}
impl<S: BlockStore> TensorLogicStore<S> {
pub fn new(store: Arc<S>) -> Result<Self> {
Ok(Self {
store,
knowledge_base: std::sync::RwLock::new(KnowledgeBase::new()),
engine: InferenceEngine::new(),
dirty: AtomicBool::new(false),
inference_times: Mutex::new(VecDeque::with_capacity(100)),
kb_version: AtomicU64::new(0),
inference_cache: Mutex::new(InferenceCache::new(1024)),
})
}
pub fn kb_version(&self) -> u64 {
self.kb_version.load(Ordering::Acquire)
}
pub fn inference_cache_stats(&self) -> crate::inference_cache::CacheStats {
self.inference_cache.lock().stats()
}
fn bump_kb_version_and_invalidate(&self) {
let old_version = self.kb_version.fetch_add(1, Ordering::AcqRel);
self.inference_cache
.lock()
.invalidate_for_kb_version(old_version);
}
pub async fn store_term(&self, term: &Term) -> Result<Cid> {
let json = serde_json::to_vec(term)
.map_err(|e| ipfrs_core::Error::Serialization(format!("Term serialization: {}", e)))?;
let block = Block::new(json.into())?;
let cid = *block.cid();
self.store.put(&block).await?;
Ok(cid)
}
pub async fn get_term(&self, cid: &Cid) -> Result<Option<Term>> {
match self.store.get(cid).await? {
Some(block) => {
let term = serde_json::from_slice(block.data()).map_err(|e| {
ipfrs_core::Error::Deserialization(format!("Term deserialization: {}", e))
})?;
Ok(Some(term))
}
None => Ok(None),
}
}
pub async fn store_predicate(&self, predicate: &Predicate) -> Result<Cid> {
let json = serde_json::to_vec(predicate).map_err(|e| {
ipfrs_core::Error::Serialization(format!("Predicate serialization: {}", e))
})?;
let block = Block::new(json.into())?;
let cid = *block.cid();
self.store.put(&block).await?;
Ok(cid)
}
pub async fn get_predicate(&self, cid: &Cid) -> Result<Option<Predicate>> {
match self.store.get(cid).await? {
Some(block) => {
let predicate = serde_json::from_slice(block.data()).map_err(|e| {
ipfrs_core::Error::Deserialization(format!("Predicate deserialization: {}", e))
})?;
Ok(Some(predicate))
}
None => Ok(None),
}
}
pub async fn store_rule(&self, rule: &Rule) -> Result<Cid> {
let json = serde_json::to_vec(rule)
.map_err(|e| ipfrs_core::Error::Serialization(format!("Rule serialization: {}", e)))?;
let block = Block::new(json.into())?;
let cid = *block.cid();
self.store.put(&block).await?;
Ok(cid)
}
pub async fn get_rule(&self, cid: &Cid) -> Result<Option<Rule>> {
match self.store.get(cid).await? {
Some(block) => {
let rule = serde_json::from_slice(block.data()).map_err(|e| {
ipfrs_core::Error::Deserialization(format!("Rule deserialization: {}", e))
})?;
Ok(Some(rule))
}
None => Ok(None),
}
}
pub async fn has(&self, cid: &Cid) -> Result<bool> {
self.store.has(cid).await
}
pub async fn delete(&self, cid: &Cid) -> Result<()> {
self.store.delete(cid).await
}
pub fn add_fact(&self, fact: Predicate) -> Result<()> {
let mut kb = self.knowledge_base.write().map_err(|_| {
ipfrs_core::Error::Storage("KB write lock poisoned in add_fact".to_string())
})?;
kb.add_fact(fact);
self.dirty.store(true, Ordering::Release);
drop(kb);
self.bump_kb_version_and_invalidate();
Ok(())
}
pub fn add_rule(&self, rule: Rule) -> Result<()> {
let mut kb = self.knowledge_base.write().map_err(|_| {
ipfrs_core::Error::Storage("KB write lock poisoned in add_rule".to_string())
})?;
kb.add_rule(rule);
self.dirty.store(true, Ordering::Release);
drop(kb);
self.bump_kb_version_and_invalidate();
Ok(())
}
pub fn retract_fact(&self, fact: &Predicate) -> Result<bool> {
let mut kb = self.knowledge_base.write().map_err(|_| {
ipfrs_core::Error::Storage("KB write lock poisoned in retract_fact".to_string())
})?;
let before = kb.facts.len();
kb.facts.retain(|f| f != fact);
let removed = kb.facts.len() < before;
if removed {
self.dirty.store(true, Ordering::Release);
drop(kb);
self.bump_kb_version_and_invalidate();
}
Ok(removed)
}
pub fn infer(&self, goal: &Predicate) -> Result<Vec<Substitution>> {
let t0 = Instant::now();
let kb = self.knowledge_base.read().map_err(|_| {
ipfrs_core::Error::Storage("KB read lock poisoned in infer".to_string())
})?;
let result = self.engine.query(goal, &kb)?;
let elapsed = t0.elapsed();
drop(kb);
let mut times = self.inference_times.lock();
if times.len() >= 100 {
times.pop_front();
}
times.push_back(elapsed);
Ok(result)
}
pub fn avg_inference_ms(&self) -> Option<f64> {
let times = self.inference_times.lock();
if times.is_empty() {
return None;
}
let total_us: u128 = times.iter().map(|d| d.as_micros()).sum();
Some(total_us as f64 / times.len() as f64 / 1000.0)
}
pub fn prove(&self, goal: &Predicate) -> Result<Option<Proof>> {
let kb = self.knowledge_base.read().map_err(|_| {
ipfrs_core::Error::Storage("KB read lock poisoned in prove".to_string())
})?;
self.engine.prove(goal, &kb)
}
pub async fn store_proof(&self, proof: &Proof) -> Result<Cid> {
let json = serde_json::to_vec(proof)
.map_err(|e| ipfrs_core::Error::Serialization(format!("Proof serialization: {}", e)))?;
let block = Block::new(json.into())?;
let cid = *block.cid();
self.store.put(&block).await?;
Ok(cid)
}
pub async fn get_proof(&self, cid: &Cid) -> Result<Option<Proof>> {
match self.store.get(cid).await? {
Some(block) => {
let proof = serde_json::from_slice(block.data()).map_err(|e| {
ipfrs_core::Error::Deserialization(format!("Proof deserialization: {}", e))
})?;
Ok(Some(proof))
}
None => Ok(None),
}
}
pub fn verify_proof(&self, proof: &Proof) -> Result<bool> {
let kb = self.knowledge_base.read().map_err(|_| {
ipfrs_core::Error::Storage("KB read lock poisoned in verify_proof".to_string())
})?;
self.engine.verify(proof, &kb)
}
pub fn kb_stats(&self) -> crate::ir::KnowledgeBaseStats {
match self.knowledge_base.read() {
Ok(kb) => kb.stats(),
Err(_) => KnowledgeBase::new().stats(),
}
}
pub fn estimated_memory_bytes(&self) -> usize {
let stats = self.kb_stats();
stats.num_rules * 500 + stats.num_facts * 200
}
pub fn snapshot_kb(&self) -> Result<KnowledgeBase> {
let kb = self
.knowledge_base
.read()
.map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
Ok(kb.clone())
}
pub async fn store_rule_as_block(&self, rule: &Rule) -> Result<Cid> {
use crate::ipld_codec::{rule_to_block, rule_to_rule_ipld};
let rule_ipld = rule_to_rule_ipld(rule)?;
let block = rule_to_block(&rule_ipld)?;
let cid = *block.cid();
self.store.put(&block).await?;
Ok(cid)
}
pub async fn load_rule_from_block(&self, cid: &Cid) -> Result<Rule> {
use crate::ipld_codec::{block_to_rule, rule_ipld_to_rule};
let block = self
.store
.get(cid)
.await?
.ok_or_else(|| ipfrs_core::Error::BlockNotFound(cid.to_string()))?;
let rule_ipld = block_to_rule(&block)?;
rule_ipld_to_rule(&rule_ipld)
}
pub async fn store_kb_as_ipld(&self) -> Result<Cid> {
use crate::ipld_codec::{kb_to_block, predicate_to_fact_ipld, KnowledgeBaseIpld};
let (rules, facts) = {
let kb = self
.knowledge_base
.read()
.map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
(kb.rules.clone(), kb.facts.clone())
};
let mut rule_cids: Vec<String> = Vec::with_capacity(rules.len());
for rule in &rules {
let cid = self.store_rule_as_block(rule).await?;
rule_cids.push(cid.to_string());
}
let fact_iplds = facts
.iter()
.map(predicate_to_fact_ipld)
.collect::<std::result::Result<Vec<_>, _>>()?;
let kb_ipld = KnowledgeBaseIpld {
rules: rule_cids,
facts: fact_iplds,
version: "1.0.0".to_string(),
};
let root_block = kb_to_block(&kb_ipld)?;
let root_cid = *root_block.cid();
self.store.put(&root_block).await?;
Ok(root_cid)
}
pub async fn rule_exists_by_cid(&self, rule: &Rule) -> Result<bool> {
use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
let rule_ipld = rule_to_rule_ipld(rule)?;
let cid = rule_cid(&rule_ipld)?;
self.store.has(&cid).await
}
pub async fn kb_stats_with_cids(&self) -> Result<TensorLogicStoreStats> {
use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
use futures::future;
let (rules, facts) = {
let kb = self
.knowledge_base
.read()
.map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
(kb.rules.clone(), kb.facts.clone())
};
let has_futures: Vec<_> = rules
.iter()
.map(|rule| async move {
let rule_ipld = rule_to_rule_ipld(rule)?;
let cid = rule_cid(&rule_ipld)?;
self.store.has(&cid).await
})
.collect();
let has_results = future::join_all(has_futures).await;
let mut cid_indexed_rules = 0usize;
for res in has_results {
if res? {
cid_indexed_rules += 1;
}
}
Ok(TensorLogicStoreStats {
rule_count: rules.len(),
fact_count: facts.len(),
cid_indexed_rules,
})
}
pub async fn index_rules_by_predicate(
&self,
) -> Result<std::collections::HashMap<String, Vec<Cid>>> {
use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
let rules = {
let kb = self
.knowledge_base
.read()
.map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
kb.rules.clone()
};
let mut cid_map: std::collections::HashMap<usize, Cid> =
std::collections::HashMap::with_capacity(rules.len());
for (idx, rule) in rules.iter().enumerate() {
if let Ok(rule_ipld) = rule_to_rule_ipld(rule) {
if let Ok(cid) = rule_cid(&rule_ipld) {
cid_map.insert(idx, cid);
}
}
}
let kb = self
.knowledge_base
.read()
.map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
Ok(kb.index_rules_by_predicate(&cid_map))
}
pub fn save_snapshot(
&self,
path: &std::path::Path,
) -> std::result::Result<(), TensorLogicError> {
use std::io::Write;
let (rules, facts) = {
let kb = self
.knowledge_base
.read()
.map_err(|e| TensorLogicError::LockPoisoned(e.to_string()))?;
(kb.rules.clone(), kb.facts.clone())
};
let rule_snapshots: Vec<RuleSnapshot> = rules
.iter()
.map(|r| RuleSnapshot {
head_predicate: r.head.name.clone(),
head_args: r.head.args.iter().map(|a| format!("{:?}", a)).collect(),
body_goals: r
.body
.iter()
.map(|g| format!("{}({:?})", g.name, g.args))
.collect(),
cid: None,
})
.collect();
let fact_snapshots: Vec<FactSnapshot> = facts
.iter()
.map(|f| FactSnapshot {
predicate: f.name.clone(),
args: f.args.iter().map(|a| format!("{:?}", a)).collect(),
})
.collect();
let rule_count = rule_snapshots.len();
let fact_count = fact_snapshots.len();
let snapshot = KnowledgeBaseSnapshot {
version: 1,
rules: rule_snapshots,
facts: fact_snapshots,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
rule_count,
fact_count,
};
let json = serde_json::to_vec_pretty(&snapshot)
.map_err(|e| TensorLogicError::Serialization(e.to_string()))?;
let mut file = std::fs::File::create(path)?;
file.write_all(&json)?;
self.dirty.store(false, Ordering::Release);
Ok(())
}
pub fn load_snapshot(
&mut self,
path: &std::path::Path,
) -> std::result::Result<KnowledgeBaseSnapshot, TensorLogicError> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut buf = Vec::new();
file.read_to_end(&mut buf)?;
let snapshot: KnowledgeBaseSnapshot = serde_json::from_slice(&buf)
.map_err(|e| TensorLogicError::Serialization(e.to_string()))?;
self.dirty.store(false, Ordering::Release);
Ok(snapshot)
}
#[inline]
pub fn is_dirty(&self) -> bool {
self.dirty.load(Ordering::Acquire)
}
pub async fn save_kb<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
use std::fs::File;
use std::io::Write;
let kb = self.knowledge_base.read().map_err(|_| {
ipfrs_core::Error::Storage("KB read lock poisoned in save_kb".to_string())
})?;
let encoded =
oxicode::serde::encode_to_vec(&*kb, oxicode::config::standard()).map_err(|e| {
ipfrs_core::Error::Serialization(format!("Failed to serialize KB: {}", e))
})?;
let mut file = File::create(path.as_ref())
.map_err(|e| ipfrs_core::Error::Storage(format!("Failed to create KB file: {}", e)))?;
file.write_all(&encoded)
.map_err(|e| ipfrs_core::Error::Storage(format!("Failed to write KB file: {}", e)))?;
Ok(())
}
pub async fn load_kb<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
use std::fs::File;
use std::io::Read;
let mut file = File::open(path.as_ref())
.map_err(|e| ipfrs_core::Error::Storage(format!("Failed to open KB file: {}", e)))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(|e| ipfrs_core::Error::Storage(format!("Failed to read KB file: {}", e)))?;
let kb: KnowledgeBase =
oxicode::serde::decode_owned_from_slice(&buffer, oxicode::config::standard())
.map(|(v, _)| v)
.map_err(|e| {
ipfrs_core::Error::Deserialization(format!("Failed to deserialize KB: {}", e))
})?;
let mut guard = self.knowledge_base.write().map_err(|_| {
ipfrs_core::Error::Storage("KB write lock poisoned in load_kb".to_string())
})?;
*guard = kb;
drop(guard);
self.bump_kb_version_and_invalidate();
Ok(())
}
}
#[cfg(test)]
mod ipld_integration_tests {
use super::*;
use crate::ir::{Constant, Predicate, Rule, Term};
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
let path = std::env::temp_dir().join(format!("ipfrs-test-tl-ipld-{}", suffix));
let _ = std::fs::remove_dir_all(&path);
let config = BlockStoreConfig {
path,
cache_size: 32 * 1024 * 1024,
};
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
TensorLogicStore::new(store).expect("test: should succeed")
}
fn grandparent_rule() -> Rule {
let head = Predicate::new(
"grandparent".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
);
let body = vec![
Predicate::new(
"parent".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
),
Predicate::new(
"parent".to_string(),
vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
),
];
Rule::new(head, body)
}
#[tokio::test]
async fn test_store_and_load_rule_as_block() {
let tl = make_store("store-load");
let rule = grandparent_rule();
let cid = tl
.store_rule_as_block(&rule)
.await
.expect("test: should succeed");
let loaded = tl
.load_rule_from_block(&cid)
.await
.expect("test: should succeed");
assert_eq!(loaded.head.name, rule.head.name);
assert_eq!(loaded.head.args.len(), rule.head.args.len());
assert_eq!(loaded.body.len(), rule.body.len());
assert_eq!(loaded.body[0].name, rule.body[0].name);
}
#[tokio::test]
async fn test_rule_deduplication_by_cid() {
let tl = make_store("dedup");
let rule = grandparent_rule();
let exists_before = tl
.rule_exists_by_cid(&rule)
.await
.expect("test: should succeed");
assert!(!exists_before, "Rule must not exist before first store");
let cid1 = tl
.store_rule_as_block(&rule)
.await
.expect("test: should succeed");
let exists_after = tl
.rule_exists_by_cid(&rule)
.await
.expect("test: should succeed");
assert!(exists_after, "Rule must exist after first store");
let cid2 = tl
.store_rule_as_block(&rule)
.await
.expect("test: should succeed");
assert_eq!(cid1, cid2, "Storing same rule twice must yield same CID");
}
#[tokio::test]
async fn test_load_rule_from_block_not_found() {
use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
let tl = make_store("not-found");
let rule = grandparent_rule();
let rule_ipld = rule_to_rule_ipld(&rule).expect("test: should succeed");
let cid = rule_cid(&rule_ipld).expect("test: should succeed");
let result = tl.load_rule_from_block(&cid).await;
assert!(result.is_err(), "Loading unstored CID must return Err");
}
#[tokio::test]
async fn test_kb_snapshot_as_ipld() {
let tl = make_store("kb-snapshot");
let rule = grandparent_rule();
tl.add_rule(rule).expect("test: should succeed");
tl.add_fact(Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("alice".to_string())),
Term::Const(Constant::String("bob".to_string())),
],
))
.expect("test: should succeed");
tl.add_fact(Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("bob".to_string())),
Term::Const(Constant::String("charlie".to_string())),
],
))
.expect("test: should succeed");
let root_cid = tl.store_kb_as_ipld().await.expect("test: should succeed");
let root_block = tl.store.get(&root_cid).await.expect("test: should succeed");
assert!(root_block.is_some(), "Root KB block must be stored");
let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
assert_eq!(stats.rule_count, 1);
assert_eq!(stats.fact_count, 2);
assert_eq!(
stats.cid_indexed_rules, 1,
"After kb_snapshot, all rules should have CID blocks"
);
}
#[tokio::test]
async fn test_kb_stats_with_cids_no_blocks() {
let tl = make_store("stats-no-blocks");
tl.add_rule(grandparent_rule())
.expect("test: should succeed");
let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
assert_eq!(stats.rule_count, 1);
assert_eq!(stats.fact_count, 0);
assert_eq!(
stats.cid_indexed_rules, 0,
"Rules added to KB but not as blocks should not be counted"
);
}
#[tokio::test]
async fn test_multiple_rules_partial_cid_coverage() {
let tl = make_store("partial-cids");
let rule_a = grandparent_rule();
let rule_b = Rule::fact(Predicate::new(
"mortal".to_string(),
vec![Term::Var("X".to_string())],
));
tl.add_rule(rule_a.clone()).expect("test: should succeed");
tl.add_rule(rule_b.clone()).expect("test: should succeed");
tl.store_rule_as_block(&rule_a)
.await
.expect("test: should succeed");
let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
assert_eq!(stats.rule_count, 2);
assert_eq!(
stats.cid_indexed_rules, 1,
"Only one rule is block-stored; cid_indexed_rules must be 1"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Constant;
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
#[tokio::test]
async fn test_term_storage() {
let config = BlockStoreConfig {
path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-term"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
let term = Term::Const(Constant::String("Alice".to_string()));
let cid = tl_store
.store_term(&term)
.await
.expect("test: should succeed");
let retrieved = tl_store.get_term(&cid).await.expect("test: should succeed");
assert_eq!(retrieved, Some(term));
}
#[tokio::test]
async fn test_predicate_storage() {
let config = BlockStoreConfig {
path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-pred"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
let predicate = Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("Alice".to_string())),
Term::Const(Constant::String("Bob".to_string())),
],
);
let cid = tl_store
.store_predicate(&predicate)
.await
.expect("test: should succeed");
let retrieved = tl_store
.get_predicate(&cid)
.await
.expect("test: should succeed");
assert_eq!(retrieved, Some(predicate));
}
#[tokio::test]
async fn test_rule_storage() {
let config = BlockStoreConfig {
path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-rule"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
let rule = Rule::fact(Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("Alice".to_string())),
Term::Const(Constant::String("Bob".to_string())),
],
));
let cid = tl_store
.store_rule(&rule)
.await
.expect("test: should succeed");
let retrieved = tl_store.get_rule(&cid).await.expect("test: should succeed");
assert!(retrieved.is_some());
}
}
#[cfg(test)]
mod snapshot_tests {
use super::*;
use crate::ir::{Constant, Predicate, Rule, Term};
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
let path = std::env::temp_dir().join(format!("ipfrs-snap-test-{}", suffix));
let _ = std::fs::remove_dir_all(&path);
let config = BlockStoreConfig {
path,
cache_size: 8 * 1024 * 1024,
};
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
TensorLogicStore::new(store).expect("test: should succeed")
}
fn grandparent_rule() -> Rule {
let head = Predicate::new(
"grandparent".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
);
let body = vec![
Predicate::new(
"parent".to_string(),
vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
),
Predicate::new(
"parent".to_string(),
vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
),
];
Rule::new(head, body)
}
#[test]
fn test_snapshot_save_load_roundtrip() {
let store = make_store("roundtrip");
store
.add_rule(grandparent_rule())
.expect("test: should succeed");
store
.add_fact(Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("alice".to_string())),
Term::Const(Constant::String("bob".to_string())),
],
))
.expect("test: should succeed");
let snap_path = std::env::temp_dir().join("ipfrs-snap-roundtrip.json");
store
.save_snapshot(&snap_path)
.expect("save_snapshot failed");
assert!(!store.is_dirty(), "Dirty flag should be cleared after save");
let mut store2 = make_store("roundtrip-load");
let snapshot = store2
.load_snapshot(&snap_path)
.expect("load_snapshot failed");
assert_eq!(snapshot.version, 1);
assert_eq!(snapshot.rule_count, 1);
assert_eq!(snapshot.fact_count, 1);
assert_eq!(snapshot.rules[0].head_predicate, "grandparent");
assert_eq!(snapshot.facts[0].predicate, "parent");
}
#[test]
fn test_snapshot_empty_kb() {
let store = make_store("empty");
let snap_path = std::env::temp_dir().join("ipfrs-snap-empty.json");
store
.save_snapshot(&snap_path)
.expect("save empty snapshot");
let mut store2 = make_store("empty-load");
let snapshot = store2
.load_snapshot(&snap_path)
.expect("load empty snapshot");
assert_eq!(snapshot.rule_count, 0);
assert_eq!(snapshot.fact_count, 0);
assert!(snapshot.rules.is_empty());
assert!(snapshot.facts.is_empty());
}
#[test]
fn test_is_dirty_tracking() {
let store = make_store("dirty");
assert!(!store.is_dirty(), "Fresh store should not be dirty");
store
.add_fact(Predicate::new(
"test".to_string(),
vec![Term::Const(Constant::String("x".to_string()))],
))
.expect("test: should succeed");
assert!(store.is_dirty(), "Store should be dirty after add_fact");
let snap_path = std::env::temp_dir().join("ipfrs-snap-dirty.json");
store.save_snapshot(&snap_path).expect("save snapshot");
assert!(!store.is_dirty(), "Store should not be dirty after save");
store
.add_rule(grandparent_rule())
.expect("test: should succeed");
assert!(store.is_dirty(), "Store should be dirty after add_rule");
}
}
#[cfg(test)]
mod inference_tracking_tests {
use super::*;
use crate::ir::{Constant, Predicate, Term};
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
use std::sync::Arc;
fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
let path = std::env::temp_dir().join(format!("ipfrs-inf-track-{}", suffix));
let _ = std::fs::remove_dir_all(&path);
let config = BlockStoreConfig {
path,
cache_size: 8 * 1024 * 1024,
};
let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
TensorLogicStore::new(store).expect("test: should succeed")
}
fn parent_fact(a: &str, b: &str) -> Predicate {
Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String(a.to_string())),
Term::Const(Constant::String(b.to_string())),
],
)
}
#[test]
fn test_inference_time_tracking() {
let store = make_store("infer-time");
assert!(
store.avg_inference_ms().is_none(),
"avg_inference_ms should be None before any infer() call"
);
store
.add_fact(parent_fact("alice", "bob"))
.expect("test: should succeed");
let goal = Predicate::new(
"parent".to_string(),
vec![
Term::Const(Constant::String("alice".to_string())),
Term::Var("X".to_string()),
],
);
let solutions = store.infer(&goal).expect("test: should succeed");
assert!(!solutions.is_empty(), "should find at least one solution");
let avg = store.avg_inference_ms();
assert!(
avg.is_some(),
"avg_inference_ms should be Some after infer() call"
);
assert!(
avg.expect("test: should succeed") >= 0.0,
"avg_inference_ms should be non-negative"
);
}
#[test]
fn test_memory_bytes_nonzero() {
let store = make_store("mem-bytes");
assert_eq!(
store.estimated_memory_bytes(),
0,
"empty KB should report 0 estimated bytes"
);
store
.add_fact(parent_fact("alice", "bob"))
.expect("test: should succeed");
let estimate = store.estimated_memory_bytes();
assert!(
estimate > 0,
"estimated_memory_bytes should be > 0 after adding a fact (got {})",
estimate
);
}
}