1use crate::inference_cache::InferenceCache;
6use crate::ir::{KnowledgeBase, Predicate, Rule, Term};
7use crate::reasoning::{InferenceEngine, Proof, Substitution};
8use ipfrs_core::{Block, Cid, Result};
9use ipfrs_storage::traits::BlockStore;
10use parking_lot::Mutex;
11use serde::{Deserialize, Serialize};
12use serde_json;
13use std::collections::VecDeque;
14use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone)]
22pub struct TensorLogicStoreStats {
23 pub rule_count: usize,
25 pub fact_count: usize,
27 pub cid_indexed_rules: usize,
30}
31
32pub struct TensorLogicPersistenceConfig {
34 pub snapshot_path: std::path::PathBuf,
36 pub auto_save: bool,
38 pub snapshot_interval: std::time::Duration,
40}
41
42#[derive(Debug, Serialize, Deserialize)]
46pub struct KnowledgeBaseSnapshot {
47 pub version: u32,
49 pub rules: Vec<RuleSnapshot>,
51 pub facts: Vec<FactSnapshot>,
53 pub created_at: u64,
55 pub rule_count: usize,
57 pub fact_count: usize,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
63pub struct RuleSnapshot {
64 pub head_predicate: String,
66 pub head_args: Vec<String>,
68 pub body_goals: Vec<String>,
70 pub cid: Option<String>,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
76pub struct FactSnapshot {
77 pub predicate: String,
79 pub args: Vec<String>,
81}
82
83#[derive(Debug, thiserror::Error)]
85pub enum TensorLogicError {
86 #[error("Snapshot IO error: {0}")]
87 Io(#[from] std::io::Error),
88 #[error("Snapshot serialization error: {0}")]
89 Serialization(String),
90 #[error("Lock poisoned: {0}")]
91 LockPoisoned(String),
92}
93
94pub struct TensorLogicStore<S: BlockStore> {
98 store: Arc<S>,
100 knowledge_base: std::sync::RwLock<KnowledgeBase>,
102 engine: InferenceEngine,
104 dirty: AtomicBool,
106 inference_times: Mutex<VecDeque<Duration>>,
111 kb_version: AtomicU64,
113 inference_cache: Mutex<InferenceCache>,
115}
116
117impl<S: BlockStore> TensorLogicStore<S> {
118 pub fn new(store: Arc<S>) -> Result<Self> {
120 Ok(Self {
121 store,
122 knowledge_base: std::sync::RwLock::new(KnowledgeBase::new()),
123 engine: InferenceEngine::new(),
124 dirty: AtomicBool::new(false),
125 inference_times: Mutex::new(VecDeque::with_capacity(100)),
126 kb_version: AtomicU64::new(0),
127 inference_cache: Mutex::new(InferenceCache::new(1024)),
128 })
129 }
130
131 pub fn kb_version(&self) -> u64 {
133 self.kb_version.load(Ordering::Acquire)
134 }
135
136 pub fn inference_cache_stats(&self) -> crate::inference_cache::CacheStats {
138 self.inference_cache.lock().stats()
139 }
140
141 fn bump_kb_version_and_invalidate(&self) {
143 let old_version = self.kb_version.fetch_add(1, Ordering::AcqRel);
144 self.inference_cache
145 .lock()
146 .invalidate_for_kb_version(old_version);
147 }
148
149 pub async fn store_term(&self, term: &Term) -> Result<Cid> {
151 let json = serde_json::to_vec(term)
152 .map_err(|e| ipfrs_core::Error::Serialization(format!("Term serialization: {}", e)))?;
153
154 let block = Block::new(json.into())?;
155 let cid = *block.cid();
156
157 self.store.put(&block).await?;
158
159 Ok(cid)
160 }
161
162 pub async fn get_term(&self, cid: &Cid) -> Result<Option<Term>> {
164 match self.store.get(cid).await? {
165 Some(block) => {
166 let term = serde_json::from_slice(block.data()).map_err(|e| {
167 ipfrs_core::Error::Deserialization(format!("Term deserialization: {}", e))
168 })?;
169 Ok(Some(term))
170 }
171 None => Ok(None),
172 }
173 }
174
175 pub async fn store_predicate(&self, predicate: &Predicate) -> Result<Cid> {
177 let json = serde_json::to_vec(predicate).map_err(|e| {
178 ipfrs_core::Error::Serialization(format!("Predicate serialization: {}", e))
179 })?;
180
181 let block = Block::new(json.into())?;
182 let cid = *block.cid();
183
184 self.store.put(&block).await?;
185
186 Ok(cid)
187 }
188
189 pub async fn get_predicate(&self, cid: &Cid) -> Result<Option<Predicate>> {
191 match self.store.get(cid).await? {
192 Some(block) => {
193 let predicate = serde_json::from_slice(block.data()).map_err(|e| {
194 ipfrs_core::Error::Deserialization(format!("Predicate deserialization: {}", e))
195 })?;
196 Ok(Some(predicate))
197 }
198 None => Ok(None),
199 }
200 }
201
202 pub async fn store_rule(&self, rule: &Rule) -> Result<Cid> {
204 let json = serde_json::to_vec(rule)
205 .map_err(|e| ipfrs_core::Error::Serialization(format!("Rule serialization: {}", e)))?;
206
207 let block = Block::new(json.into())?;
208 let cid = *block.cid();
209
210 self.store.put(&block).await?;
211
212 Ok(cid)
213 }
214
215 pub async fn get_rule(&self, cid: &Cid) -> Result<Option<Rule>> {
217 match self.store.get(cid).await? {
218 Some(block) => {
219 let rule = serde_json::from_slice(block.data()).map_err(|e| {
220 ipfrs_core::Error::Deserialization(format!("Rule deserialization: {}", e))
221 })?;
222 Ok(Some(rule))
223 }
224 None => Ok(None),
225 }
226 }
227
228 pub async fn has(&self, cid: &Cid) -> Result<bool> {
230 self.store.has(cid).await
231 }
232
233 pub async fn delete(&self, cid: &Cid) -> Result<()> {
235 self.store.delete(cid).await
236 }
237
238 pub fn add_fact(&self, fact: Predicate) -> Result<()> {
240 let mut kb = self.knowledge_base.write().map_err(|_| {
241 ipfrs_core::Error::Storage("KB write lock poisoned in add_fact".to_string())
242 })?;
243 kb.add_fact(fact);
244 self.dirty.store(true, Ordering::Release);
245 drop(kb);
246 self.bump_kb_version_and_invalidate();
247 Ok(())
248 }
249
250 pub fn add_rule(&self, rule: Rule) -> Result<()> {
252 let mut kb = self.knowledge_base.write().map_err(|_| {
253 ipfrs_core::Error::Storage("KB write lock poisoned in add_rule".to_string())
254 })?;
255 kb.add_rule(rule);
256 self.dirty.store(true, Ordering::Release);
257 drop(kb);
258 self.bump_kb_version_and_invalidate();
259 Ok(())
260 }
261
262 pub fn retract_fact(&self, fact: &Predicate) -> Result<bool> {
264 let mut kb = self.knowledge_base.write().map_err(|_| {
265 ipfrs_core::Error::Storage("KB write lock poisoned in retract_fact".to_string())
266 })?;
267 let before = kb.facts.len();
268 kb.facts.retain(|f| f != fact);
269 let removed = kb.facts.len() < before;
270 if removed {
271 self.dirty.store(true, Ordering::Release);
272 drop(kb);
273 self.bump_kb_version_and_invalidate();
274 }
275 Ok(removed)
276 }
277
278 pub fn infer(&self, goal: &Predicate) -> Result<Vec<Substitution>> {
284 let t0 = Instant::now();
285 let kb = self.knowledge_base.read().map_err(|_| {
286 ipfrs_core::Error::Storage("KB read lock poisoned in infer".to_string())
287 })?;
288 let result = self.engine.query(goal, &kb)?;
289 let elapsed = t0.elapsed();
290 drop(kb);
291 let mut times = self.inference_times.lock();
292 if times.len() >= 100 {
293 times.pop_front();
294 }
295 times.push_back(elapsed);
296 Ok(result)
297 }
298
299 pub fn avg_inference_ms(&self) -> Option<f64> {
303 let times = self.inference_times.lock();
304 if times.is_empty() {
305 return None;
306 }
307 let total_us: u128 = times.iter().map(|d| d.as_micros()).sum();
308 Some(total_us as f64 / times.len() as f64 / 1000.0)
309 }
310
311 pub fn prove(&self, goal: &Predicate) -> Result<Option<Proof>> {
313 let kb = self.knowledge_base.read().map_err(|_| {
314 ipfrs_core::Error::Storage("KB read lock poisoned in prove".to_string())
315 })?;
316 self.engine.prove(goal, &kb)
317 }
318
319 pub async fn store_proof(&self, proof: &Proof) -> Result<Cid> {
321 let json = serde_json::to_vec(proof)
322 .map_err(|e| ipfrs_core::Error::Serialization(format!("Proof serialization: {}", e)))?;
323
324 let block = Block::new(json.into())?;
325 let cid = *block.cid();
326
327 self.store.put(&block).await?;
328
329 Ok(cid)
330 }
331
332 pub async fn get_proof(&self, cid: &Cid) -> Result<Option<Proof>> {
334 match self.store.get(cid).await? {
335 Some(block) => {
336 let proof = serde_json::from_slice(block.data()).map_err(|e| {
337 ipfrs_core::Error::Deserialization(format!("Proof deserialization: {}", e))
338 })?;
339 Ok(Some(proof))
340 }
341 None => Ok(None),
342 }
343 }
344
345 pub fn verify_proof(&self, proof: &Proof) -> Result<bool> {
347 let kb = self.knowledge_base.read().map_err(|_| {
348 ipfrs_core::Error::Storage("KB read lock poisoned in verify_proof".to_string())
349 })?;
350 self.engine.verify(proof, &kb)
351 }
352
353 pub fn kb_stats(&self) -> crate::ir::KnowledgeBaseStats {
355 match self.knowledge_base.read() {
356 Ok(kb) => kb.stats(),
357 Err(_) => KnowledgeBase::new().stats(),
358 }
359 }
360
361 pub fn estimated_memory_bytes(&self) -> usize {
367 let stats = self.kb_stats();
368 stats.num_rules * 500 + stats.num_facts * 200
369 }
370
371 pub fn snapshot_kb(&self) -> Result<KnowledgeBase> {
376 let kb = self
377 .knowledge_base
378 .read()
379 .map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
380 Ok(kb.clone())
381 }
382
383 pub async fn store_rule_as_block(&self, rule: &Rule) -> Result<Cid> {
397 use crate::ipld_codec::{rule_to_block, rule_to_rule_ipld};
398
399 let rule_ipld = rule_to_rule_ipld(rule)?;
400 let block = rule_to_block(&rule_ipld)?;
401 let cid = *block.cid();
402
403 self.store.put(&block).await?;
404
405 Ok(cid)
406 }
407
408 pub async fn load_rule_from_block(&self, cid: &Cid) -> Result<Rule> {
413 use crate::ipld_codec::{block_to_rule, rule_ipld_to_rule};
414
415 let block = self
416 .store
417 .get(cid)
418 .await?
419 .ok_or_else(|| ipfrs_core::Error::BlockNotFound(cid.to_string()))?;
420
421 let rule_ipld = block_to_rule(&block)?;
422 rule_ipld_to_rule(&rule_ipld)
423 }
424
425 pub async fn store_kb_as_ipld(&self) -> Result<Cid> {
434 use crate::ipld_codec::{kb_to_block, predicate_to_fact_ipld, KnowledgeBaseIpld};
435
436 let (rules, facts) = {
438 let kb = self
439 .knowledge_base
440 .read()
441 .map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
442 (kb.rules.clone(), kb.facts.clone())
443 };
444
445 let mut rule_cids: Vec<String> = Vec::with_capacity(rules.len());
450 for rule in &rules {
451 let cid = self.store_rule_as_block(rule).await?;
452 rule_cids.push(cid.to_string());
453 }
454
455 let fact_iplds = facts
457 .iter()
458 .map(predicate_to_fact_ipld)
459 .collect::<std::result::Result<Vec<_>, _>>()?;
460
461 let kb_ipld = KnowledgeBaseIpld {
462 rules: rule_cids,
463 facts: fact_iplds,
464 version: "1.0.0".to_string(),
465 };
466
467 let root_block = kb_to_block(&kb_ipld)?;
468 let root_cid = *root_block.cid();
469 self.store.put(&root_block).await?;
470
471 Ok(root_cid)
472 }
473
474 pub async fn rule_exists_by_cid(&self, rule: &Rule) -> Result<bool> {
482 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
483
484 let rule_ipld = rule_to_rule_ipld(rule)?;
485 let cid = rule_cid(&rule_ipld)?;
486 self.store.has(&cid).await
487 }
488
489 pub async fn kb_stats_with_cids(&self) -> Result<TensorLogicStoreStats> {
497 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
498 use futures::future;
499
500 let (rules, facts) = {
502 let kb = self
503 .knowledge_base
504 .read()
505 .map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
506 (kb.rules.clone(), kb.facts.clone())
507 };
508
509 let has_futures: Vec<_> = rules
511 .iter()
512 .map(|rule| async move {
513 let rule_ipld = rule_to_rule_ipld(rule)?;
514 let cid = rule_cid(&rule_ipld)?;
515 self.store.has(&cid).await
516 })
517 .collect();
518
519 let has_results = future::join_all(has_futures).await;
520
521 let mut cid_indexed_rules = 0usize;
522 for res in has_results {
523 if res? {
524 cid_indexed_rules += 1;
525 }
526 }
527
528 Ok(TensorLogicStoreStats {
529 rule_count: rules.len(),
530 fact_count: facts.len(),
531 cid_indexed_rules,
532 })
533 }
534
535 pub async fn index_rules_by_predicate(
546 &self,
547 ) -> Result<std::collections::HashMap<String, Vec<Cid>>> {
548 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
549
550 let rules = {
551 let kb = self
552 .knowledge_base
553 .read()
554 .map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
555 kb.rules.clone()
556 };
557
558 let mut cid_map: std::collections::HashMap<usize, Cid> =
559 std::collections::HashMap::with_capacity(rules.len());
560
561 for (idx, rule) in rules.iter().enumerate() {
562 if let Ok(rule_ipld) = rule_to_rule_ipld(rule) {
563 if let Ok(cid) = rule_cid(&rule_ipld) {
564 cid_map.insert(idx, cid);
565 }
566 }
567 }
568
569 let kb = self
571 .knowledge_base
572 .read()
573 .map_err(|_| ipfrs_core::Error::Storage("KB lock poisoned".to_string()))?;
574
575 Ok(kb.index_rules_by_predicate(&cid_map))
576 }
577
578 pub fn save_snapshot(
586 &self,
587 path: &std::path::Path,
588 ) -> std::result::Result<(), TensorLogicError> {
589 use std::io::Write;
590
591 let (rules, facts) = {
592 let kb = self
593 .knowledge_base
594 .read()
595 .map_err(|e| TensorLogicError::LockPoisoned(e.to_string()))?;
596 (kb.rules.clone(), kb.facts.clone())
597 };
598
599 let rule_snapshots: Vec<RuleSnapshot> = rules
600 .iter()
601 .map(|r| RuleSnapshot {
602 head_predicate: r.head.name.clone(),
603 head_args: r.head.args.iter().map(|a| format!("{:?}", a)).collect(),
604 body_goals: r
605 .body
606 .iter()
607 .map(|g| format!("{}({:?})", g.name, g.args))
608 .collect(),
609 cid: None,
610 })
611 .collect();
612
613 let fact_snapshots: Vec<FactSnapshot> = facts
614 .iter()
615 .map(|f| FactSnapshot {
616 predicate: f.name.clone(),
617 args: f.args.iter().map(|a| format!("{:?}", a)).collect(),
618 })
619 .collect();
620
621 let rule_count = rule_snapshots.len();
622 let fact_count = fact_snapshots.len();
623
624 let snapshot = KnowledgeBaseSnapshot {
625 version: 1,
626 rules: rule_snapshots,
627 facts: fact_snapshots,
628 created_at: std::time::SystemTime::now()
629 .duration_since(std::time::UNIX_EPOCH)
630 .unwrap_or_default()
631 .as_secs(),
632 rule_count,
633 fact_count,
634 };
635
636 let json = serde_json::to_vec_pretty(&snapshot)
637 .map_err(|e| TensorLogicError::Serialization(e.to_string()))?;
638
639 let mut file = std::fs::File::create(path)?;
640 file.write_all(&json)?;
641
642 self.dirty.store(false, Ordering::Release);
643 Ok(())
644 }
645
646 pub fn load_snapshot(
656 &mut self,
657 path: &std::path::Path,
658 ) -> std::result::Result<KnowledgeBaseSnapshot, TensorLogicError> {
659 use std::io::Read;
660
661 let mut file = std::fs::File::open(path)?;
662 let mut buf = Vec::new();
663 file.read_to_end(&mut buf)?;
664
665 let snapshot: KnowledgeBaseSnapshot = serde_json::from_slice(&buf)
666 .map_err(|e| TensorLogicError::Serialization(e.to_string()))?;
667
668 self.dirty.store(false, Ordering::Release);
669 Ok(snapshot)
670 }
671
672 #[inline]
674 pub fn is_dirty(&self) -> bool {
675 self.dirty.load(Ordering::Acquire)
676 }
677
678 pub async fn save_kb<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
686 use std::fs::File;
687 use std::io::Write;
688
689 let kb = self.knowledge_base.read().map_err(|_| {
690 ipfrs_core::Error::Storage("KB read lock poisoned in save_kb".to_string())
691 })?;
692
693 let encoded =
695 oxicode::serde::encode_to_vec(&*kb, oxicode::config::standard()).map_err(|e| {
696 ipfrs_core::Error::Serialization(format!("Failed to serialize KB: {}", e))
697 })?;
698
699 let mut file = File::create(path.as_ref())
701 .map_err(|e| ipfrs_core::Error::Storage(format!("Failed to create KB file: {}", e)))?;
702
703 file.write_all(&encoded)
704 .map_err(|e| ipfrs_core::Error::Storage(format!("Failed to write KB file: {}", e)))?;
705
706 Ok(())
707 }
708
709 pub async fn load_kb<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
716 use std::fs::File;
717 use std::io::Read;
718
719 let mut file = File::open(path.as_ref())
721 .map_err(|e| ipfrs_core::Error::Storage(format!("Failed to open KB file: {}", e)))?;
722
723 let mut buffer = Vec::new();
724 file.read_to_end(&mut buffer)
725 .map_err(|e| ipfrs_core::Error::Storage(format!("Failed to read KB file: {}", e)))?;
726
727 let kb: KnowledgeBase =
729 oxicode::serde::decode_owned_from_slice(&buffer, oxicode::config::standard())
730 .map(|(v, _)| v)
731 .map_err(|e| {
732 ipfrs_core::Error::Deserialization(format!("Failed to deserialize KB: {}", e))
733 })?;
734
735 let mut guard = self.knowledge_base.write().map_err(|_| {
737 ipfrs_core::Error::Storage("KB write lock poisoned in load_kb".to_string())
738 })?;
739 *guard = kb;
740 drop(guard);
741 self.bump_kb_version_and_invalidate();
742
743 Ok(())
744 }
745}
746
747#[cfg(test)]
748mod ipld_integration_tests {
749 use super::*;
750 use crate::ir::{Constant, Predicate, Rule, Term};
751 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
752
753 fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
754 let path = std::env::temp_dir().join(format!("ipfrs-test-tl-ipld-{}", suffix));
755 let _ = std::fs::remove_dir_all(&path);
756 let config = BlockStoreConfig {
757 path,
758 cache_size: 32 * 1024 * 1024,
759 };
760 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
761 TensorLogicStore::new(store).expect("test: should succeed")
762 }
763
764 fn grandparent_rule() -> Rule {
765 let head = Predicate::new(
767 "grandparent".to_string(),
768 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
769 );
770 let body = vec![
771 Predicate::new(
772 "parent".to_string(),
773 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
774 ),
775 Predicate::new(
776 "parent".to_string(),
777 vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
778 ),
779 ];
780 Rule::new(head, body)
781 }
782
783 #[tokio::test]
784 async fn test_store_and_load_rule_as_block() {
785 let tl = make_store("store-load");
786 let rule = grandparent_rule();
787
788 let cid = tl
789 .store_rule_as_block(&rule)
790 .await
791 .expect("test: should succeed");
792
793 let loaded = tl
795 .load_rule_from_block(&cid)
796 .await
797 .expect("test: should succeed");
798 assert_eq!(loaded.head.name, rule.head.name);
799 assert_eq!(loaded.head.args.len(), rule.head.args.len());
800 assert_eq!(loaded.body.len(), rule.body.len());
801 assert_eq!(loaded.body[0].name, rule.body[0].name);
802 }
803
804 #[tokio::test]
805 async fn test_rule_deduplication_by_cid() {
806 let tl = make_store("dedup");
807 let rule = grandparent_rule();
808
809 let exists_before = tl
811 .rule_exists_by_cid(&rule)
812 .await
813 .expect("test: should succeed");
814 assert!(!exists_before, "Rule must not exist before first store");
815
816 let cid1 = tl
818 .store_rule_as_block(&rule)
819 .await
820 .expect("test: should succeed");
821
822 let exists_after = tl
824 .rule_exists_by_cid(&rule)
825 .await
826 .expect("test: should succeed");
827 assert!(exists_after, "Rule must exist after first store");
828
829 let cid2 = tl
831 .store_rule_as_block(&rule)
832 .await
833 .expect("test: should succeed");
834 assert_eq!(cid1, cid2, "Storing same rule twice must yield same CID");
835 }
836
837 #[tokio::test]
838 async fn test_load_rule_from_block_not_found() {
839 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
840
841 let tl = make_store("not-found");
842 let rule = grandparent_rule();
843
844 let rule_ipld = rule_to_rule_ipld(&rule).expect("test: should succeed");
846 let cid = rule_cid(&rule_ipld).expect("test: should succeed");
847
848 let result = tl.load_rule_from_block(&cid).await;
849 assert!(result.is_err(), "Loading unstored CID must return Err");
850 }
851
852 #[tokio::test]
853 async fn test_kb_snapshot_as_ipld() {
854 let tl = make_store("kb-snapshot");
855
856 let rule = grandparent_rule();
858 tl.add_rule(rule).expect("test: should succeed");
859 tl.add_fact(Predicate::new(
860 "parent".to_string(),
861 vec![
862 Term::Const(Constant::String("alice".to_string())),
863 Term::Const(Constant::String("bob".to_string())),
864 ],
865 ))
866 .expect("test: should succeed");
867 tl.add_fact(Predicate::new(
868 "parent".to_string(),
869 vec![
870 Term::Const(Constant::String("bob".to_string())),
871 Term::Const(Constant::String("charlie".to_string())),
872 ],
873 ))
874 .expect("test: should succeed");
875
876 let root_cid = tl.store_kb_as_ipld().await.expect("test: should succeed");
878
879 let root_block = tl.store.get(&root_cid).await.expect("test: should succeed");
881 assert!(root_block.is_some(), "Root KB block must be stored");
882
883 let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
885 assert_eq!(stats.rule_count, 1);
886 assert_eq!(stats.fact_count, 2);
887 assert_eq!(
888 stats.cid_indexed_rules, 1,
889 "After kb_snapshot, all rules should have CID blocks"
890 );
891 }
892
893 #[tokio::test]
894 async fn test_kb_stats_with_cids_no_blocks() {
895 let tl = make_store("stats-no-blocks");
896
897 tl.add_rule(grandparent_rule())
899 .expect("test: should succeed");
900
901 let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
902 assert_eq!(stats.rule_count, 1);
903 assert_eq!(stats.fact_count, 0);
904 assert_eq!(
905 stats.cid_indexed_rules, 0,
906 "Rules added to KB but not as blocks should not be counted"
907 );
908 }
909
910 #[tokio::test]
911 async fn test_multiple_rules_partial_cid_coverage() {
912 let tl = make_store("partial-cids");
913
914 let rule_a = grandparent_rule();
915 let rule_b = Rule::fact(Predicate::new(
916 "mortal".to_string(),
917 vec![Term::Var("X".to_string())],
918 ));
919
920 tl.add_rule(rule_a.clone()).expect("test: should succeed");
921 tl.add_rule(rule_b.clone()).expect("test: should succeed");
922
923 tl.store_rule_as_block(&rule_a)
925 .await
926 .expect("test: should succeed");
927
928 let stats = tl.kb_stats_with_cids().await.expect("test: should succeed");
929 assert_eq!(stats.rule_count, 2);
930 assert_eq!(
931 stats.cid_indexed_rules, 1,
932 "Only one rule is block-stored; cid_indexed_rules must be 1"
933 );
934 }
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940 use crate::ir::Constant;
941 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
942
943 #[tokio::test]
944 async fn test_term_storage() {
945 let config = BlockStoreConfig {
946 path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-term"),
947 cache_size: 100 * 1024 * 1024,
948 };
949 let _ = std::fs::remove_dir_all(&config.path);
950 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
951 let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
952
953 let term = Term::Const(Constant::String("Alice".to_string()));
954 let cid = tl_store
955 .store_term(&term)
956 .await
957 .expect("test: should succeed");
958
959 let retrieved = tl_store.get_term(&cid).await.expect("test: should succeed");
960 assert_eq!(retrieved, Some(term));
961 }
962
963 #[tokio::test]
964 async fn test_predicate_storage() {
965 let config = BlockStoreConfig {
966 path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-pred"),
967 cache_size: 100 * 1024 * 1024,
968 };
969 let _ = std::fs::remove_dir_all(&config.path);
970 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
971 let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
972
973 let predicate = Predicate::new(
974 "parent".to_string(),
975 vec![
976 Term::Const(Constant::String("Alice".to_string())),
977 Term::Const(Constant::String("Bob".to_string())),
978 ],
979 );
980
981 let cid = tl_store
982 .store_predicate(&predicate)
983 .await
984 .expect("test: should succeed");
985 let retrieved = tl_store
986 .get_predicate(&cid)
987 .await
988 .expect("test: should succeed");
989 assert_eq!(retrieved, Some(predicate));
990 }
991
992 #[tokio::test]
993 async fn test_rule_storage() {
994 let config = BlockStoreConfig {
995 path: std::path::PathBuf::from("/tmp/ipfrs-test-tensorlogic-rule"),
996 cache_size: 100 * 1024 * 1024,
997 };
998 let _ = std::fs::remove_dir_all(&config.path);
999 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
1000 let tl_store = TensorLogicStore::new(store).expect("test: should succeed");
1001
1002 let rule = Rule::fact(Predicate::new(
1003 "parent".to_string(),
1004 vec![
1005 Term::Const(Constant::String("Alice".to_string())),
1006 Term::Const(Constant::String("Bob".to_string())),
1007 ],
1008 ));
1009
1010 let cid = tl_store
1011 .store_rule(&rule)
1012 .await
1013 .expect("test: should succeed");
1014 let retrieved = tl_store.get_rule(&cid).await.expect("test: should succeed");
1015 assert!(retrieved.is_some());
1016 }
1017}
1018
1019#[cfg(test)]
1020mod snapshot_tests {
1021 use super::*;
1022 use crate::ir::{Constant, Predicate, Rule, Term};
1023 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
1024
1025 fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
1026 let path = std::env::temp_dir().join(format!("ipfrs-snap-test-{}", suffix));
1027 let _ = std::fs::remove_dir_all(&path);
1028 let config = BlockStoreConfig {
1029 path,
1030 cache_size: 8 * 1024 * 1024,
1031 };
1032 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
1033 TensorLogicStore::new(store).expect("test: should succeed")
1034 }
1035
1036 fn grandparent_rule() -> Rule {
1037 let head = Predicate::new(
1038 "grandparent".to_string(),
1039 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
1040 );
1041 let body = vec![
1042 Predicate::new(
1043 "parent".to_string(),
1044 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
1045 ),
1046 Predicate::new(
1047 "parent".to_string(),
1048 vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
1049 ),
1050 ];
1051 Rule::new(head, body)
1052 }
1053
1054 #[test]
1055 fn test_snapshot_save_load_roundtrip() {
1056 let store = make_store("roundtrip");
1057
1058 store
1059 .add_rule(grandparent_rule())
1060 .expect("test: should succeed");
1061 store
1062 .add_fact(Predicate::new(
1063 "parent".to_string(),
1064 vec![
1065 Term::Const(Constant::String("alice".to_string())),
1066 Term::Const(Constant::String("bob".to_string())),
1067 ],
1068 ))
1069 .expect("test: should succeed");
1070
1071 let snap_path = std::env::temp_dir().join("ipfrs-snap-roundtrip.json");
1072 store
1073 .save_snapshot(&snap_path)
1074 .expect("save_snapshot failed");
1075
1076 assert!(!store.is_dirty(), "Dirty flag should be cleared after save");
1078
1079 let mut store2 = make_store("roundtrip-load");
1081 let snapshot = store2
1082 .load_snapshot(&snap_path)
1083 .expect("load_snapshot failed");
1084
1085 assert_eq!(snapshot.version, 1);
1086 assert_eq!(snapshot.rule_count, 1);
1087 assert_eq!(snapshot.fact_count, 1);
1088 assert_eq!(snapshot.rules[0].head_predicate, "grandparent");
1089 assert_eq!(snapshot.facts[0].predicate, "parent");
1090 }
1091
1092 #[test]
1093 fn test_snapshot_empty_kb() {
1094 let store = make_store("empty");
1095 let snap_path = std::env::temp_dir().join("ipfrs-snap-empty.json");
1096
1097 store
1098 .save_snapshot(&snap_path)
1099 .expect("save empty snapshot");
1100 let mut store2 = make_store("empty-load");
1101 let snapshot = store2
1102 .load_snapshot(&snap_path)
1103 .expect("load empty snapshot");
1104
1105 assert_eq!(snapshot.rule_count, 0);
1106 assert_eq!(snapshot.fact_count, 0);
1107 assert!(snapshot.rules.is_empty());
1108 assert!(snapshot.facts.is_empty());
1109 }
1110
1111 #[test]
1112 fn test_is_dirty_tracking() {
1113 let store = make_store("dirty");
1114
1115 assert!(!store.is_dirty(), "Fresh store should not be dirty");
1117
1118 store
1120 .add_fact(Predicate::new(
1121 "test".to_string(),
1122 vec![Term::Const(Constant::String("x".to_string()))],
1123 ))
1124 .expect("test: should succeed");
1125 assert!(store.is_dirty(), "Store should be dirty after add_fact");
1126
1127 let snap_path = std::env::temp_dir().join("ipfrs-snap-dirty.json");
1129 store.save_snapshot(&snap_path).expect("save snapshot");
1130 assert!(!store.is_dirty(), "Store should not be dirty after save");
1131
1132 store
1134 .add_rule(grandparent_rule())
1135 .expect("test: should succeed");
1136 assert!(store.is_dirty(), "Store should be dirty after add_rule");
1137 }
1138}
1139
1140#[cfg(test)]
1141mod inference_tracking_tests {
1142 use super::*;
1143 use crate::ir::{Constant, Predicate, Term};
1144 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
1145 use std::sync::Arc;
1146
1147 fn make_store(suffix: &str) -> TensorLogicStore<SledBlockStore> {
1148 let path = std::env::temp_dir().join(format!("ipfrs-inf-track-{}", suffix));
1149 let _ = std::fs::remove_dir_all(&path);
1150 let config = BlockStoreConfig {
1151 path,
1152 cache_size: 8 * 1024 * 1024,
1153 };
1154 let store = Arc::new(SledBlockStore::new(config).expect("test: should succeed"));
1155 TensorLogicStore::new(store).expect("test: should succeed")
1156 }
1157
1158 fn parent_fact(a: &str, b: &str) -> Predicate {
1159 Predicate::new(
1160 "parent".to_string(),
1161 vec![
1162 Term::Const(Constant::String(a.to_string())),
1163 Term::Const(Constant::String(b.to_string())),
1164 ],
1165 )
1166 }
1167
1168 #[test]
1169 fn test_inference_time_tracking() {
1170 let store = make_store("infer-time");
1171
1172 assert!(
1174 store.avg_inference_ms().is_none(),
1175 "avg_inference_ms should be None before any infer() call"
1176 );
1177
1178 store
1180 .add_fact(parent_fact("alice", "bob"))
1181 .expect("test: should succeed");
1182
1183 let goal = Predicate::new(
1184 "parent".to_string(),
1185 vec![
1186 Term::Const(Constant::String("alice".to_string())),
1187 Term::Var("X".to_string()),
1188 ],
1189 );
1190 let solutions = store.infer(&goal).expect("test: should succeed");
1191 assert!(!solutions.is_empty(), "should find at least one solution");
1192
1193 let avg = store.avg_inference_ms();
1195 assert!(
1196 avg.is_some(),
1197 "avg_inference_ms should be Some after infer() call"
1198 );
1199 assert!(
1200 avg.expect("test: should succeed") >= 0.0,
1201 "avg_inference_ms should be non-negative"
1202 );
1203 }
1204
1205 #[test]
1206 fn test_memory_bytes_nonzero() {
1207 let store = make_store("mem-bytes");
1208
1209 assert_eq!(
1211 store.estimated_memory_bytes(),
1212 0,
1213 "empty KB should report 0 estimated bytes"
1214 );
1215
1216 store
1218 .add_fact(parent_fact("alice", "bob"))
1219 .expect("test: should succeed");
1220 let estimate = store.estimated_memory_bytes();
1221 assert!(
1222 estimate > 0,
1223 "estimated_memory_bytes should be > 0 after adding a fact (got {})",
1224 estimate
1225 );
1226 }
1227}