1use crate::ir::{Predicate, Rule, Term};
10use crate::reasoning::{Proof, ProofRule};
11use crate::{deserialize_cid, serialize_cid};
12use ipfrs_core::Cid;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::hash::{Hash, Hasher};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProofFragment {
20 pub id: String,
22 pub conclusion: Predicate,
24 pub rule_applied: Option<RuleRef>,
26 pub premise_refs: Vec<ProofFragmentRef>,
28 pub substitution: Vec<(String, Term)>,
30 pub metadata: ProofMetadata,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
36pub struct ProofFragmentRef {
37 #[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
39 pub cid: Cid,
40 pub conclusion_hint: Option<String>,
42}
43
44impl ProofFragmentRef {
45 pub fn new(cid: Cid) -> Self {
47 Self {
48 cid,
49 conclusion_hint: None,
50 }
51 }
52
53 pub fn with_hint(cid: Cid, hint: String) -> Self {
55 Self {
56 cid,
57 conclusion_hint: Some(hint),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct RuleRef {
65 pub rule_id: String,
67 #[serde(
69 serialize_with = "serialize_cid_option",
70 deserialize_with = "deserialize_cid_option"
71 )]
72 pub rule_cid: Option<Cid>,
73 pub rule: Option<Rule>,
75}
76
77impl RuleRef {
78 pub fn from_rule(rule: &Rule) -> Self {
80 Self {
81 rule_id: rule.head.name.clone(),
82 rule_cid: None,
83 rule: Some(rule.clone()),
84 }
85 }
86
87 pub fn from_cid(rule_id: String, cid: Cid) -> Self {
89 Self {
90 rule_id,
91 rule_cid: Some(cid),
92 rule: None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
99pub struct ProofMetadata {
100 pub created_at: Option<u64>,
102 pub created_by: Option<String>,
104 pub complexity: Option<u32>,
106 pub depth: u32,
108 pub custom: HashMap<String, String>,
110}
111
112impl ProofMetadata {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn with_created_at(mut self, timestamp: u64) -> Self {
120 self.created_at = Some(timestamp);
121 self
122 }
123
124 pub fn with_created_by(mut self, creator: String) -> Self {
126 self.created_by = Some(creator);
127 self
128 }
129
130 pub fn with_complexity(mut self, complexity: u32) -> Self {
132 self.complexity = Some(complexity);
133 self
134 }
135
136 pub fn with_depth(mut self, depth: u32) -> Self {
138 self.depth = depth;
139 self
140 }
141}
142
143impl ProofFragment {
144 pub fn fact(conclusion: Predicate) -> Self {
146 Self {
147 id: generate_fragment_id(&conclusion),
148 conclusion,
149 rule_applied: None,
150 premise_refs: Vec::new(),
151 substitution: Vec::new(),
152 metadata: ProofMetadata::new(),
153 }
154 }
155
156 pub fn with_rule(
158 conclusion: Predicate,
159 rule: &Rule,
160 premises: Vec<ProofFragmentRef>,
161 substitution: Vec<(String, Term)>,
162 ) -> Self {
163 let depth = premises.len() as u32 + 1;
164
165 Self {
166 id: generate_fragment_id(&conclusion),
167 conclusion,
168 rule_applied: Some(RuleRef::from_rule(rule)),
169 premise_refs: premises,
170 substitution,
171 metadata: ProofMetadata::new().with_depth(depth),
172 }
173 }
174
175 #[inline]
177 pub fn is_fact(&self) -> bool {
178 self.premise_refs.is_empty() && self.rule_applied.is_none()
179 }
180
181 #[inline]
183 pub fn premise_count(&self) -> usize {
184 self.premise_refs.len()
185 }
186
187 pub fn to_proof(&self, subproofs: Vec<Proof>) -> Proof {
189 let rule = match &self.rule_applied {
190 Some(rule_ref) => {
191 if let Some(rule) = &rule_ref.rule {
192 Some(ProofRule {
193 head: rule.head.clone(),
194 body: rule.body.clone(),
195 is_fact: rule.body.is_empty(),
196 })
197 } else {
198 Some(ProofRule {
199 head: self.conclusion.clone(),
200 body: Vec::new(),
201 is_fact: true,
202 })
203 }
204 }
205 None => Some(ProofRule {
206 head: self.conclusion.clone(),
207 body: Vec::new(),
208 is_fact: true,
209 }),
210 };
211
212 Proof {
213 goal: self.conclusion.clone(),
214 rule,
215 subproofs,
216 }
217 }
218}
219
220fn generate_fragment_id(pred: &Predicate) -> String {
222 use std::collections::hash_map::DefaultHasher;
223
224 let mut hasher = DefaultHasher::new();
225 pred.name.hash(&mut hasher);
227 for arg in &pred.args {
229 arg.hash(&mut hasher);
230 }
231 format!("pf_{:016x}", hasher.finish())
232}
233
234pub struct ProofFragmentStore {
236 fragments: HashMap<String, ProofFragment>,
238 fragments_by_cid: HashMap<Cid, String>,
240 by_conclusion: HashMap<String, Vec<String>>,
242}
243
244impl ProofFragmentStore {
245 pub fn new() -> Self {
247 Self {
248 fragments: HashMap::new(),
249 fragments_by_cid: HashMap::new(),
250 by_conclusion: HashMap::new(),
251 }
252 }
253
254 pub fn add(&mut self, fragment: ProofFragment) -> String {
256 let id = fragment.id.clone();
257 let pred_name = fragment.conclusion.name.clone();
258
259 self.by_conclusion
260 .entry(pred_name)
261 .or_default()
262 .push(id.clone());
263
264 self.fragments.insert(id.clone(), fragment);
265 id
266 }
267
268 pub fn add_with_cid(&mut self, fragment: ProofFragment, cid: Cid) -> String {
270 let id = self.add(fragment);
271 self.fragments_by_cid.insert(cid, id.clone());
272 id
273 }
274
275 pub fn get(&self, id: &str) -> Option<&ProofFragment> {
277 self.fragments.get(id)
278 }
279
280 pub fn get_by_cid(&self, cid: &Cid) -> Option<&ProofFragment> {
282 self.fragments_by_cid
283 .get(cid)
284 .and_then(|id| self.fragments.get(id))
285 }
286
287 pub fn find_by_conclusion(&self, predicate_name: &str) -> Vec<&ProofFragment> {
289 self.by_conclusion
290 .get(predicate_name)
291 .map(|ids| ids.iter().filter_map(|id| self.fragments.get(id)).collect())
292 .unwrap_or_default()
293 }
294
295 pub fn fragment_ids(&self) -> Vec<&str> {
297 self.fragments.keys().map(|s| s.as_str()).collect()
298 }
299
300 pub fn len(&self) -> usize {
302 self.fragments.len()
303 }
304
305 pub fn is_empty(&self) -> bool {
307 self.fragments.is_empty()
308 }
309
310 pub fn remove(&mut self, id: &str) -> Option<ProofFragment> {
312 if let Some(fragment) = self.fragments.remove(id) {
313 if let Some(ids) = self.by_conclusion.get_mut(&fragment.conclusion.name) {
315 ids.retain(|i| i != id);
316 }
317 Some(fragment)
318 } else {
319 None
320 }
321 }
322
323 pub fn clear(&mut self) {
325 self.fragments.clear();
326 self.fragments_by_cid.clear();
327 self.by_conclusion.clear();
328 }
329}
330
331impl Default for ProofFragmentStore {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337pub struct ProofAssembler<'a> {
339 store: &'a ProofFragmentStore,
341 cache: HashMap<String, Proof>,
343}
344
345impl<'a> ProofAssembler<'a> {
346 pub fn new(store: &'a ProofFragmentStore) -> Self {
348 Self {
349 store,
350 cache: HashMap::new(),
351 }
352 }
353
354 pub fn assemble(&mut self, fragment_id: &str) -> Option<Proof> {
356 if let Some(proof) = self.cache.get(fragment_id) {
358 return Some(proof.clone());
359 }
360
361 let fragment = self.store.get(fragment_id)?;
363
364 let mut premises = Vec::new();
366 for premise_ref in &fragment.premise_refs {
367 if let Some(premise_fragment) = self.store.get_by_cid(&premise_ref.cid) {
369 if let Some(premise_proof) = self.assemble(&premise_fragment.id) {
370 premises.push(premise_proof);
371 } else {
372 return None; }
374 } else {
375 return None; }
377 }
378
379 let proof = fragment.to_proof(premises);
381
382 self.cache.insert(fragment_id.to_string(), proof.clone());
384 Some(proof)
385 }
386
387 #[allow(clippy::only_used_in_recursion)]
389 pub fn verify(&self, proof: &Proof) -> bool {
390 match &proof.rule {
392 Some(rule) if rule.is_fact => {
393 proof.subproofs.is_empty()
395 }
396 Some(rule) => {
397 if proof.subproofs.len() != rule.body.len() {
399 return false;
400 }
401
402 for subproof in &proof.subproofs {
404 if !self.verify(subproof) {
405 return false;
406 }
407 }
408
409 true
410 }
411 None => {
412 proof.subproofs.is_empty()
414 }
415 }
416 }
417}
418
419fn serialize_cid_option<S>(cid: &Option<Cid>, serializer: S) -> Result<S::Ok, S::Error>
421where
422 S: serde::Serializer,
423{
424 match cid {
425 Some(c) => serializer.serialize_some(&c.to_string()),
426 None => serializer.serialize_none(),
427 }
428}
429
430fn deserialize_cid_option<'de, D>(deserializer: D) -> Result<Option<Cid>, D::Error>
432where
433 D: serde::Deserializer<'de>,
434{
435 let opt: Option<String> = Option::deserialize(deserializer)?;
436 match opt {
437 Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
438 None => Ok(None),
439 }
440}
441
442pub struct ProofCompressor {
444 shared_cache: HashMap<String, String>,
446 stats: CompressionStats,
448}
449
450#[derive(Debug, Clone, Default)]
452pub struct CompressionStats {
453 pub original_count: usize,
455 pub compressed_count: usize,
457 pub shared_subproofs: usize,
459 pub size_reduction: usize,
461}
462
463impl CompressionStats {
464 pub fn compression_ratio(&self) -> f64 {
466 if self.original_count == 0 {
467 return 1.0;
468 }
469 self.compressed_count as f64 / self.original_count as f64
470 }
471
472 pub fn space_savings(&self) -> f64 {
474 if self.original_count == 0 {
475 return 0.0;
476 }
477 (1.0 - self.compression_ratio()) * 100.0
478 }
479}
480
481impl ProofCompressor {
482 pub fn new() -> Self {
484 Self {
485 shared_cache: HashMap::new(),
486 stats: CompressionStats::default(),
487 }
488 }
489
490 pub fn compress(&mut self, store: &mut ProofFragmentStore) -> CompressionStats {
497 self.stats = CompressionStats::default();
498 self.shared_cache.clear();
499
500 self.stats.original_count = store.len();
502
503 self.eliminate_common_subproofs(store);
505
506 self.remove_redundant_fragments(store);
508
509 self.stats.compressed_count = store.len();
511
512 self.stats.clone()
513 }
514
515 fn eliminate_common_subproofs(&mut self, store: &mut ProofFragmentStore) {
517 let mut conclusion_map: HashMap<String, Vec<String>> = HashMap::new();
518
519 for (id, fragment) in &store.fragments {
521 let conclusion_key = self.conclusion_key(&fragment.conclusion);
522 conclusion_map
523 .entry(conclusion_key)
524 .or_default()
525 .push(id.clone());
526 }
527
528 for (_conclusion, fragment_ids) in conclusion_map {
530 if fragment_ids.len() > 1 {
531 let canonical = fragment_ids[0].clone();
533
534 for dup_id in fragment_ids.iter().skip(1) {
535 self.shared_cache.insert(dup_id.clone(), canonical.clone());
536 self.stats.shared_subproofs += 1;
537 }
538 }
539 }
540
541 self.update_references(store);
543 }
544
545 fn update_references(&self, store: &mut ProofFragmentStore) {
547 let fragment_ids: Vec<String> = store.fragments.keys().cloned().collect();
549
550 for id in fragment_ids {
551 if let Some(fragment) = store.fragments.get_mut(&id) {
552 for _premise_ref in &mut fragment.premise_refs {
554 if let Some(canonical_id) = self.shared_cache.get(&id) {
555 let _ = canonical_id; }
559 }
560 }
561 }
562 }
563
564 fn remove_redundant_fragments(&mut self, store: &mut ProofFragmentStore) {
566 let referenced: std::collections::HashSet<String> = std::collections::HashSet::new();
568
569 #[allow(clippy::never_loop)]
570 for fragment in store.fragments.values() {
571 for _premise_ref in &fragment.premise_refs {
572 }
575 }
576
577 let to_remove: Vec<String> = self
579 .shared_cache
580 .keys()
581 .filter(|id| !referenced.contains(*id))
582 .cloned()
583 .collect();
584
585 for id in to_remove {
586 store.fragments.remove(&id);
587 self.stats.size_reduction += 100; }
589 }
590
591 fn conclusion_key(&self, conclusion: &Predicate) -> String {
593 format!(
594 "{}({})",
595 conclusion.name,
596 conclusion
597 .args
598 .iter()
599 .map(|t| format!("{:?}", t))
600 .collect::<Vec<_>>()
601 .join(",")
602 )
603 }
604
605 pub fn compute_delta(
609 &self,
610 base_proof: &ProofFragment,
611 new_proof: &ProofFragment,
612 ) -> Vec<ProofFragment> {
613 let mut delta = Vec::new();
614
615 if base_proof.conclusion.name != new_proof.conclusion.name
617 || base_proof.conclusion.args.len() != new_proof.conclusion.args.len()
618 {
619 delta.push(new_proof.clone());
620 return delta;
621 }
622
623 if base_proof.premise_refs.len() != new_proof.premise_refs.len() {
625 delta.push(new_proof.clone());
626 return delta;
627 }
628
629 delta
631 }
632
633 #[inline]
635 pub fn stats(&self) -> &CompressionStats {
636 &self.stats
637 }
638}
639
640impl Default for ProofCompressor {
641 fn default() -> Self {
642 Self::new()
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use crate::ir::Constant;
650
651 fn make_predicate(name: &str, args: Vec<&str>) -> Predicate {
652 Predicate::new(
653 name.to_string(),
654 args.into_iter()
655 .map(|a| Term::Const(Constant::String(a.to_string())))
656 .collect(),
657 )
658 }
659
660 #[test]
661 fn test_proof_fragment_fact() {
662 let pred = make_predicate("parent", vec!["alice", "bob"]);
663 let fragment = ProofFragment::fact(pred.clone());
664
665 assert!(fragment.is_fact());
666 assert_eq!(fragment.premise_count(), 0);
667 assert_eq!(fragment.conclusion.name, "parent");
668 }
669
670 #[test]
671 fn test_proof_fragment_store() {
672 let mut store = ProofFragmentStore::new();
673
674 let pred1 = make_predicate("parent", vec!["alice", "bob"]);
675 let pred2 = make_predicate("parent", vec!["bob", "carol"]);
676
677 let fragment1 = ProofFragment::fact(pred1);
678 let fragment2 = ProofFragment::fact(pred2);
679
680 let id1 = store.add(fragment1);
681 let id2 = store.add(fragment2);
682
683 assert_eq!(store.len(), 2);
684
685 let found = store.find_by_conclusion("parent");
686 assert_eq!(found.len(), 2);
687
688 assert!(store.get(&id1).is_some());
689 assert!(store.get(&id2).is_some());
690 }
691
692 #[test]
693 fn test_proof_assembly() {
694 let mut store = ProofFragmentStore::new();
695
696 let parent_ab = make_predicate("parent", vec!["alice", "bob"]);
700 let parent_bc = make_predicate("parent", vec!["bob", "carol"]);
701 let _grandparent = make_predicate("grandparent", vec!["alice", "carol"]);
702
703 let frag_ab = ProofFragment::fact(parent_ab);
705 let frag_bc = ProofFragment::fact(parent_bc);
706
707 let id_ab = store.add(frag_ab);
708 let _id_bc = store.add(frag_bc);
709
710 let assembler = ProofAssembler::new(&store);
714
715 let fact_fragment = store.get(&id_ab).unwrap();
717 let fact_proof = fact_fragment.to_proof(vec![]);
718 assert!(assembler.verify(&fact_proof));
719 }
720
721 #[test]
722 fn test_proof_metadata() {
723 let metadata = ProofMetadata::new()
724 .with_created_at(1234567890)
725 .with_created_by("test".to_string())
726 .with_complexity(5)
727 .with_depth(3);
728
729 assert_eq!(metadata.created_at, Some(1234567890));
730 assert_eq!(metadata.created_by, Some("test".to_string()));
731 assert_eq!(metadata.complexity, Some(5));
732 assert_eq!(metadata.depth, 3);
733 }
734
735 #[test]
736 fn test_proof_compression_basic() {
737 let mut compressor = ProofCompressor::new();
738 let mut store = ProofFragmentStore::new();
739
740 let pred1 = make_predicate("parent", vec!["alice", "bob"]);
742 let pred2 = make_predicate("parent", vec!["bob", "carol"]);
743 let pred3 = make_predicate("likes", vec!["alice", "pizza"]);
744
745 let fragment1 = ProofFragment::fact(pred1);
746 let fragment2 = ProofFragment::fact(pred2);
747 let fragment3 = ProofFragment::fact(pred3);
748
749 store.add(fragment1);
750 store.add(fragment2);
751 store.add(fragment3);
752
753 let initial_count = store.len();
754 assert!(initial_count > 0);
755
756 let stats = compressor.compress(&mut store);
758
759 assert_eq!(stats.original_count, initial_count);
761 assert!(stats.compressed_count <= initial_count);
762 }
763
764 #[test]
765 fn test_compression_stats() {
766 let stats = CompressionStats {
767 original_count: 100,
768 compressed_count: 60,
769 shared_subproofs: 40,
770 size_reduction: 4000,
771 };
772
773 assert!((stats.compression_ratio() - 0.6).abs() < 0.01);
774 assert!((stats.space_savings() - 40.0).abs() < 0.01);
775 }
776
777 #[test]
778 fn test_compression_stats_empty() {
779 let stats = CompressionStats::default();
780
781 assert_eq!(stats.compression_ratio(), 1.0);
782 assert_eq!(stats.space_savings(), 0.0);
783 }
784
785 #[test]
786 fn test_proof_delta_same() {
787 let compressor = ProofCompressor::new();
788
789 let pred = make_predicate("parent", vec!["alice", "bob"]);
790 let fragment1 = ProofFragment::fact(pred.clone());
791 let fragment2 = ProofFragment::fact(pred);
792
793 let delta = compressor.compute_delta(&fragment1, &fragment2);
794
795 assert_eq!(delta.len(), 0);
797 }
798
799 #[test]
800 fn test_proof_delta_different() {
801 let compressor = ProofCompressor::new();
802
803 let pred1 = make_predicate("parent", vec!["alice", "bob"]);
804 let pred2 = make_predicate("likes", vec!["bob", "pizza"]); let fragment1 = ProofFragment::fact(pred1);
807 let fragment2 = ProofFragment::fact(pred2);
808
809 let delta = compressor.compute_delta(&fragment1, &fragment2);
810
811 assert_eq!(delta.len(), 1);
813 }
814
815 #[test]
816 fn test_conclusion_key() {
817 let compressor = ProofCompressor::new();
818
819 let pred1 = make_predicate("parent", vec!["alice", "bob"]);
820 let pred2 = make_predicate("parent", vec!["alice", "bob"]);
821
822 let key1 = compressor.conclusion_key(&pred1);
823 let key2 = compressor.conclusion_key(&pred2);
824
825 assert_eq!(key1, key2);
827 }
828
829 #[test]
830 fn test_compressor_multiple_runs() {
831 let mut compressor = ProofCompressor::new();
832 let mut store = ProofFragmentStore::new();
833
834 for i in 0..5 {
836 let pred = make_predicate("parent", vec!["alice", &format!("child{}", i)]);
837 store.add(ProofFragment::fact(pred));
838 }
839
840 let initial_count = store.len();
841
842 let stats1 = compressor.compress(&mut store);
844 assert_eq!(stats1.original_count, initial_count);
845
846 let stats2 = compressor.compress(&mut store);
848 assert_eq!(stats2.original_count, stats1.compressed_count);
849 }
850}