use crate::ir::{Predicate, Rule, Term};
use crate::reasoning::{Proof, ProofRule};
use crate::{deserialize_cid, serialize_cid};
use ipfrs_core::Cid;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofFragment {
pub id: String,
pub conclusion: Predicate,
pub rule_applied: Option<RuleRef>,
pub premise_refs: Vec<ProofFragmentRef>,
pub substitution: Vec<(String, Term)>,
pub metadata: ProofMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ProofFragmentRef {
#[serde(serialize_with = "serialize_cid", deserialize_with = "deserialize_cid")]
pub cid: Cid,
pub conclusion_hint: Option<String>,
}
impl ProofFragmentRef {
pub fn new(cid: Cid) -> Self {
Self {
cid,
conclusion_hint: None,
}
}
pub fn with_hint(cid: Cid, hint: String) -> Self {
Self {
cid,
conclusion_hint: Some(hint),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleRef {
pub rule_id: String,
#[serde(
serialize_with = "serialize_cid_option",
deserialize_with = "deserialize_cid_option"
)]
pub rule_cid: Option<Cid>,
pub rule: Option<Rule>,
}
impl RuleRef {
pub fn from_rule(rule: &Rule) -> Self {
Self {
rule_id: rule.head.name.clone(),
rule_cid: None,
rule: Some(rule.clone()),
}
}
pub fn from_cid(rule_id: String, cid: Cid) -> Self {
Self {
rule_id,
rule_cid: Some(cid),
rule: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProofMetadata {
pub created_at: Option<u64>,
pub created_by: Option<String>,
pub complexity: Option<u32>,
pub depth: u32,
pub custom: HashMap<String, String>,
}
impl ProofMetadata {
pub fn new() -> Self {
Self::default()
}
pub fn with_created_at(mut self, timestamp: u64) -> Self {
self.created_at = Some(timestamp);
self
}
pub fn with_created_by(mut self, creator: String) -> Self {
self.created_by = Some(creator);
self
}
pub fn with_complexity(mut self, complexity: u32) -> Self {
self.complexity = Some(complexity);
self
}
pub fn with_depth(mut self, depth: u32) -> Self {
self.depth = depth;
self
}
}
impl ProofFragment {
pub fn fact(conclusion: Predicate) -> Self {
Self {
id: generate_fragment_id(&conclusion),
conclusion,
rule_applied: None,
premise_refs: Vec::new(),
substitution: Vec::new(),
metadata: ProofMetadata::new(),
}
}
pub fn with_rule(
conclusion: Predicate,
rule: &Rule,
premises: Vec<ProofFragmentRef>,
substitution: Vec<(String, Term)>,
) -> Self {
let depth = premises.len() as u32 + 1;
Self {
id: generate_fragment_id(&conclusion),
conclusion,
rule_applied: Some(RuleRef::from_rule(rule)),
premise_refs: premises,
substitution,
metadata: ProofMetadata::new().with_depth(depth),
}
}
#[inline]
pub fn is_fact(&self) -> bool {
self.premise_refs.is_empty() && self.rule_applied.is_none()
}
#[inline]
pub fn premise_count(&self) -> usize {
self.premise_refs.len()
}
pub fn to_proof(&self, subproofs: Vec<Proof>) -> Proof {
let rule = match &self.rule_applied {
Some(rule_ref) => {
if let Some(rule) = &rule_ref.rule {
Some(ProofRule {
head: rule.head.clone(),
body: rule.body.clone(),
is_fact: rule.body.is_empty(),
})
} else {
Some(ProofRule {
head: self.conclusion.clone(),
body: Vec::new(),
is_fact: true,
})
}
}
None => Some(ProofRule {
head: self.conclusion.clone(),
body: Vec::new(),
is_fact: true,
}),
};
Proof {
goal: self.conclusion.clone(),
rule,
subproofs,
}
}
}
fn generate_fragment_id(pred: &Predicate) -> String {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
pred.name.hash(&mut hasher);
for arg in &pred.args {
arg.hash(&mut hasher);
}
format!("pf_{:016x}", hasher.finish())
}
pub struct ProofFragmentStore {
fragments: HashMap<String, ProofFragment>,
fragments_by_cid: HashMap<Cid, String>,
by_conclusion: HashMap<String, Vec<String>>,
}
impl ProofFragmentStore {
pub fn new() -> Self {
Self {
fragments: HashMap::new(),
fragments_by_cid: HashMap::new(),
by_conclusion: HashMap::new(),
}
}
pub fn add(&mut self, fragment: ProofFragment) -> String {
let id = fragment.id.clone();
let pred_name = fragment.conclusion.name.clone();
self.by_conclusion
.entry(pred_name)
.or_default()
.push(id.clone());
self.fragments.insert(id.clone(), fragment);
id
}
pub fn add_with_cid(&mut self, fragment: ProofFragment, cid: Cid) -> String {
let id = self.add(fragment);
self.fragments_by_cid.insert(cid, id.clone());
id
}
pub fn get(&self, id: &str) -> Option<&ProofFragment> {
self.fragments.get(id)
}
pub fn get_by_cid(&self, cid: &Cid) -> Option<&ProofFragment> {
self.fragments_by_cid
.get(cid)
.and_then(|id| self.fragments.get(id))
}
pub fn find_by_conclusion(&self, predicate_name: &str) -> Vec<&ProofFragment> {
self.by_conclusion
.get(predicate_name)
.map(|ids| ids.iter().filter_map(|id| self.fragments.get(id)).collect())
.unwrap_or_default()
}
pub fn fragment_ids(&self) -> Vec<&str> {
self.fragments.keys().map(|s| s.as_str()).collect()
}
pub fn len(&self) -> usize {
self.fragments.len()
}
pub fn is_empty(&self) -> bool {
self.fragments.is_empty()
}
pub fn remove(&mut self, id: &str) -> Option<ProofFragment> {
if let Some(fragment) = self.fragments.remove(id) {
if let Some(ids) = self.by_conclusion.get_mut(&fragment.conclusion.name) {
ids.retain(|i| i != id);
}
Some(fragment)
} else {
None
}
}
pub fn clear(&mut self) {
self.fragments.clear();
self.fragments_by_cid.clear();
self.by_conclusion.clear();
}
}
impl Default for ProofFragmentStore {
fn default() -> Self {
Self::new()
}
}
pub struct ProofAssembler<'a> {
store: &'a ProofFragmentStore,
cache: HashMap<String, Proof>,
}
impl<'a> ProofAssembler<'a> {
pub fn new(store: &'a ProofFragmentStore) -> Self {
Self {
store,
cache: HashMap::new(),
}
}
pub fn assemble(&mut self, fragment_id: &str) -> Option<Proof> {
if let Some(proof) = self.cache.get(fragment_id) {
return Some(proof.clone());
}
let fragment = self.store.get(fragment_id)?;
let mut premises = Vec::new();
for premise_ref in &fragment.premise_refs {
if let Some(premise_fragment) = self.store.get_by_cid(&premise_ref.cid) {
if let Some(premise_proof) = self.assemble(&premise_fragment.id) {
premises.push(premise_proof);
} else {
return None; }
} else {
return None; }
}
let proof = fragment.to_proof(premises);
self.cache.insert(fragment_id.to_string(), proof.clone());
Some(proof)
}
#[allow(clippy::only_used_in_recursion)]
pub fn verify(&self, proof: &Proof) -> bool {
match &proof.rule {
Some(rule) if rule.is_fact => {
proof.subproofs.is_empty()
}
Some(rule) => {
if proof.subproofs.len() != rule.body.len() {
return false;
}
for subproof in &proof.subproofs {
if !self.verify(subproof) {
return false;
}
}
true
}
None => {
proof.subproofs.is_empty()
}
}
}
}
fn serialize_cid_option<S>(cid: &Option<Cid>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match cid {
Some(c) => serializer.serialize_some(&c.to_string()),
None => serializer.serialize_none(),
}
}
fn deserialize_cid_option<'de, D>(deserializer: D) -> Result<Option<Cid>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
None => Ok(None),
}
}
pub struct ProofCompressor {
shared_cache: HashMap<String, String>,
stats: CompressionStats,
}
#[derive(Debug, Clone, Default)]
pub struct CompressionStats {
pub original_count: usize,
pub compressed_count: usize,
pub shared_subproofs: usize,
pub size_reduction: usize,
}
impl CompressionStats {
pub fn compression_ratio(&self) -> f64 {
if self.original_count == 0 {
return 1.0;
}
self.compressed_count as f64 / self.original_count as f64
}
pub fn space_savings(&self) -> f64 {
if self.original_count == 0 {
return 0.0;
}
(1.0 - self.compression_ratio()) * 100.0
}
}
impl ProofCompressor {
pub fn new() -> Self {
Self {
shared_cache: HashMap::new(),
stats: CompressionStats::default(),
}
}
pub fn compress(&mut self, store: &mut ProofFragmentStore) -> CompressionStats {
self.stats = CompressionStats::default();
self.shared_cache.clear();
self.stats.original_count = store.len();
self.eliminate_common_subproofs(store);
self.remove_redundant_fragments(store);
self.stats.compressed_count = store.len();
self.stats.clone()
}
fn eliminate_common_subproofs(&mut self, store: &mut ProofFragmentStore) {
let mut conclusion_map: HashMap<String, Vec<String>> = HashMap::new();
for (id, fragment) in &store.fragments {
let conclusion_key = self.conclusion_key(&fragment.conclusion);
conclusion_map
.entry(conclusion_key)
.or_default()
.push(id.clone());
}
for (_conclusion, fragment_ids) in conclusion_map {
if fragment_ids.len() > 1 {
let canonical = fragment_ids[0].clone();
for dup_id in fragment_ids.iter().skip(1) {
self.shared_cache.insert(dup_id.clone(), canonical.clone());
self.stats.shared_subproofs += 1;
}
}
}
self.update_references(store);
}
fn update_references(&self, store: &mut ProofFragmentStore) {
let fragment_ids: Vec<String> = store.fragments.keys().cloned().collect();
for id in fragment_ids {
if let Some(fragment) = store.fragments.get_mut(&id) {
for _premise_ref in &mut fragment.premise_refs {
if let Some(canonical_id) = self.shared_cache.get(&id) {
let _ = canonical_id; }
}
}
}
}
fn remove_redundant_fragments(&mut self, store: &mut ProofFragmentStore) {
let referenced: std::collections::HashSet<String> = std::collections::HashSet::new();
#[allow(clippy::never_loop)]
for fragment in store.fragments.values() {
for _premise_ref in &fragment.premise_refs {
}
}
let to_remove: Vec<String> = self
.shared_cache
.keys()
.filter(|id| !referenced.contains(*id))
.cloned()
.collect();
for id in to_remove {
store.fragments.remove(&id);
self.stats.size_reduction += 100; }
}
fn conclusion_key(&self, conclusion: &Predicate) -> String {
format!(
"{}({})",
conclusion.name,
conclusion
.args
.iter()
.map(|t| format!("{:?}", t))
.collect::<Vec<_>>()
.join(",")
)
}
pub fn compute_delta(
&self,
base_proof: &ProofFragment,
new_proof: &ProofFragment,
) -> Vec<ProofFragment> {
let mut delta = Vec::new();
if base_proof.conclusion.name != new_proof.conclusion.name
|| base_proof.conclusion.args.len() != new_proof.conclusion.args.len()
{
delta.push(new_proof.clone());
return delta;
}
if base_proof.premise_refs.len() != new_proof.premise_refs.len() {
delta.push(new_proof.clone());
return delta;
}
delta
}
#[inline]
pub fn stats(&self) -> &CompressionStats {
&self.stats
}
}
impl Default for ProofCompressor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Constant;
fn make_predicate(name: &str, args: Vec<&str>) -> Predicate {
Predicate::new(
name.to_string(),
args.into_iter()
.map(|a| Term::Const(Constant::String(a.to_string())))
.collect(),
)
}
#[test]
fn test_proof_fragment_fact() {
let pred = make_predicate("parent", vec!["alice", "bob"]);
let fragment = ProofFragment::fact(pred.clone());
assert!(fragment.is_fact());
assert_eq!(fragment.premise_count(), 0);
assert_eq!(fragment.conclusion.name, "parent");
}
#[test]
fn test_proof_fragment_store() {
let mut store = ProofFragmentStore::new();
let pred1 = make_predicate("parent", vec!["alice", "bob"]);
let pred2 = make_predicate("parent", vec!["bob", "carol"]);
let fragment1 = ProofFragment::fact(pred1);
let fragment2 = ProofFragment::fact(pred2);
let id1 = store.add(fragment1);
let id2 = store.add(fragment2);
assert_eq!(store.len(), 2);
let found = store.find_by_conclusion("parent");
assert_eq!(found.len(), 2);
assert!(store.get(&id1).is_some());
assert!(store.get(&id2).is_some());
}
#[test]
fn test_proof_assembly() {
let mut store = ProofFragmentStore::new();
let parent_ab = make_predicate("parent", vec!["alice", "bob"]);
let parent_bc = make_predicate("parent", vec!["bob", "carol"]);
let _grandparent = make_predicate("grandparent", vec!["alice", "carol"]);
let frag_ab = ProofFragment::fact(parent_ab);
let frag_bc = ProofFragment::fact(parent_bc);
let id_ab = store.add(frag_ab);
let _id_bc = store.add(frag_bc);
let assembler = ProofAssembler::new(&store);
let fact_fragment = store.get(&id_ab).unwrap();
let fact_proof = fact_fragment.to_proof(vec![]);
assert!(assembler.verify(&fact_proof));
}
#[test]
fn test_proof_metadata() {
let metadata = ProofMetadata::new()
.with_created_at(1234567890)
.with_created_by("test".to_string())
.with_complexity(5)
.with_depth(3);
assert_eq!(metadata.created_at, Some(1234567890));
assert_eq!(metadata.created_by, Some("test".to_string()));
assert_eq!(metadata.complexity, Some(5));
assert_eq!(metadata.depth, 3);
}
#[test]
fn test_proof_compression_basic() {
let mut compressor = ProofCompressor::new();
let mut store = ProofFragmentStore::new();
let pred1 = make_predicate("parent", vec!["alice", "bob"]);
let pred2 = make_predicate("parent", vec!["bob", "carol"]);
let pred3 = make_predicate("likes", vec!["alice", "pizza"]);
let fragment1 = ProofFragment::fact(pred1);
let fragment2 = ProofFragment::fact(pred2);
let fragment3 = ProofFragment::fact(pred3);
store.add(fragment1);
store.add(fragment2);
store.add(fragment3);
let initial_count = store.len();
assert!(initial_count > 0);
let stats = compressor.compress(&mut store);
assert_eq!(stats.original_count, initial_count);
assert!(stats.compressed_count <= initial_count);
}
#[test]
fn test_compression_stats() {
let stats = CompressionStats {
original_count: 100,
compressed_count: 60,
shared_subproofs: 40,
size_reduction: 4000,
};
assert!((stats.compression_ratio() - 0.6).abs() < 0.01);
assert!((stats.space_savings() - 40.0).abs() < 0.01);
}
#[test]
fn test_compression_stats_empty() {
let stats = CompressionStats::default();
assert_eq!(stats.compression_ratio(), 1.0);
assert_eq!(stats.space_savings(), 0.0);
}
#[test]
fn test_proof_delta_same() {
let compressor = ProofCompressor::new();
let pred = make_predicate("parent", vec!["alice", "bob"]);
let fragment1 = ProofFragment::fact(pred.clone());
let fragment2 = ProofFragment::fact(pred);
let delta = compressor.compute_delta(&fragment1, &fragment2);
assert_eq!(delta.len(), 0);
}
#[test]
fn test_proof_delta_different() {
let compressor = ProofCompressor::new();
let pred1 = make_predicate("parent", vec!["alice", "bob"]);
let pred2 = make_predicate("likes", vec!["bob", "pizza"]);
let fragment1 = ProofFragment::fact(pred1);
let fragment2 = ProofFragment::fact(pred2);
let delta = compressor.compute_delta(&fragment1, &fragment2);
assert_eq!(delta.len(), 1);
}
#[test]
fn test_conclusion_key() {
let compressor = ProofCompressor::new();
let pred1 = make_predicate("parent", vec!["alice", "bob"]);
let pred2 = make_predicate("parent", vec!["alice", "bob"]);
let key1 = compressor.conclusion_key(&pred1);
let key2 = compressor.conclusion_key(&pred2);
assert_eq!(key1, key2);
}
#[test]
fn test_compressor_multiple_runs() {
let mut compressor = ProofCompressor::new();
let mut store = ProofFragmentStore::new();
for i in 0..5 {
let pred = make_predicate("parent", vec!["alice", &format!("child{}", i)]);
store.add(ProofFragment::fact(pred));
}
let initial_count = store.len();
let stats1 = compressor.compress(&mut store);
assert_eq!(stats1.original_count, initial_count);
let stats2 = compressor.compress(&mut store);
assert_eq!(stats2.original_count, stats1.compressed_count);
}
}