1use crate::ir::Term;
12use ipfrs_core::Cid;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::fmt;
16
17fn serialize_option_cid<S>(cid: &Option<Cid>, s: S) -> Result<S::Ok, S::Error>
19where
20 S: serde::Serializer,
21{
22 match cid {
23 Some(c) => s.serialize_some(&c.to_string()),
24 None => s.serialize_none(),
25 }
26}
27
28fn deserialize_option_cid<'de, D>(d: D) -> Result<Option<Cid>, D::Error>
30where
31 D: serde::Deserializer<'de>,
32{
33 let opt: Option<String> = Option::deserialize(d)?;
34 match opt {
35 Some(s) => s.parse::<Cid>().map(Some).map_err(serde::de::Error::custom),
36 None => Ok(None),
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ProofNode {
47 pub goal: Term,
49
50 #[serde(
54 serialize_with = "serialize_option_cid",
55 deserialize_with = "deserialize_option_cid"
56 )]
57 pub rule_cid: Option<Cid>,
58
59 pub peer: Option<String>,
63
64 pub children: Vec<ProofNode>,
66
67 pub resolved: bool,
69
70 pub depth: usize,
72}
73
74impl ProofNode {
75 pub fn fact(goal: Term, depth: usize, peer: Option<String>) -> Self {
82 Self {
83 goal,
84 rule_cid: None,
85 peer,
86 children: Vec::new(),
87 resolved: true,
88 depth,
89 }
90 }
91
92 pub fn unresolved(goal: Term, depth: usize) -> Self {
94 Self {
95 goal,
96 rule_cid: None,
97 peer: None,
98 children: Vec::new(),
99 resolved: false,
100 depth,
101 }
102 }
103
104 pub fn from_rule(
113 goal: Term,
114 rule_cid: Option<Cid>,
115 children: Vec<ProofNode>,
116 depth: usize,
117 peer: Option<String>,
118 ) -> Self {
119 let resolved = children.iter().all(|c| c.resolved);
120 Self {
121 goal,
122 rule_cid,
123 peer,
124 children,
125 resolved,
126 depth,
127 }
128 }
129
130 pub fn size(&self) -> usize {
132 1 + self.children.iter().map(|c| c.size()).sum::<usize>()
133 }
134
135 pub fn max_depth(&self) -> usize {
137 if self.children.is_empty() {
138 self.depth
139 } else {
140 self.children
141 .iter()
142 .map(|c| c.max_depth())
143 .max()
144 .unwrap_or(self.depth)
145 }
146 }
147
148 pub fn contributing_peers(&self) -> Vec<String> {
150 let mut peers = Vec::new();
151 self.collect_peers(&mut peers);
152 peers.sort_unstable();
153 peers.dedup();
154 peers
155 }
156
157 fn collect_peers(&self, acc: &mut Vec<String>) {
158 if let Some(ref p) = self.peer {
159 acc.push(p.clone());
160 }
161 for child in &self.children {
162 child.collect_peers(acc);
163 }
164 }
165}
166
167impl fmt::Display for ProofNode {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 let indent = " ".repeat(self.depth);
170 let peer_label = self
171 .peer
172 .as_deref()
173 .map(|p| format!(" [peer:{}]", p))
174 .unwrap_or_default();
175 let rule_label = self
176 .rule_cid
177 .as_ref()
178 .map(|c| format!(" rule:{}", c))
179 .unwrap_or_default();
180 let status = if self.resolved { "✓" } else { "✗" };
181 writeln!(
182 f,
183 "{}{} {}{}{}",
184 indent, status, self.goal, peer_label, rule_label
185 )?;
186 for child in &self.children {
187 write!(f, "{}", child)?;
188 }
189 Ok(())
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ProofTree {
200 pub root: ProofNode,
202
203 pub query: Term,
205
206 pub bindings: HashMap<String, Term>,
208
209 pub is_complete: bool,
211}
212
213impl ProofTree {
214 pub fn new(root: ProofNode, query: Term, bindings: HashMap<String, Term>) -> Self {
216 let is_complete = root.resolved;
217 Self {
218 root,
219 query,
220 bindings,
221 is_complete,
222 }
223 }
224
225 pub fn failed(query: Term) -> Self {
227 let root = ProofNode::unresolved(query.clone(), 0);
228 Self {
229 root,
230 query,
231 bindings: HashMap::new(),
232 is_complete: false,
233 }
234 }
235
236 pub fn size(&self) -> usize {
238 self.root.size()
239 }
240
241 pub fn max_depth(&self) -> usize {
243 self.root.max_depth()
244 }
245
246 pub fn contributing_peers(&self) -> Vec<String> {
248 self.root.contributing_peers()
249 }
250
251 pub fn prune_unresolved(&mut self) {
262 prune_node(&mut self.root);
263 self.is_complete = self.root.resolved;
264 }
265
266 pub fn collapse_chains(&mut self) {
272 collapse_node(&mut self.root);
273 reindex_depths(&mut self.root, 0);
274 }
275
276 pub fn remote_contributions(&self) -> Vec<(&ProofNode, std::net::SocketAddr)> {
283 let mut out = Vec::new();
284 collect_remote_contributions(&self.root, &mut out);
285 out
286 }
287
288 pub fn merge(&mut self, other: ProofTree) {
295 merge_nodes(&mut self.root, other.root);
297 for (k, v) in other.bindings {
299 self.bindings.entry(k).or_insert(v);
300 }
301 self.is_complete = self.root.resolved;
302 }
303
304 pub fn to_stream(
311 &self,
312 session_id: impl Into<String>,
313 ) -> (
314 crate::proof_tree_streaming::ProofTreeStreamer,
315 tokio::sync::mpsc::UnboundedReceiver<crate::proof_tree_streaming::ProofTreeUpdate>,
316 ) {
317 use crate::proof_tree_streaming::{ProofTreeStreamer, ProofTreeUpdateSink};
318 use std::sync::Arc;
319
320 let (sink, rx) = ProofTreeUpdateSink::new();
321 let streamer = ProofTreeStreamer::new(session_id, Arc::new(sink));
322 streamer.stream_tree(self);
323 (streamer, rx)
324 }
325}
326
327fn prune_node(node: &mut ProofNode) {
335 for child in &mut node.children {
337 prune_node(child);
338 }
339
340 node.children.retain(|c| c.resolved);
342
343 let mut seen: HashMap<String, usize> = HashMap::new(); node.children.retain(|c| {
347 let key = format!("{}", c.goal);
348 let sz = c.size();
349 match seen.get(&key) {
350 Some(&best) if sz >= best => false,
351 _ => {
352 seen.insert(key, sz);
353 true
354 }
355 }
356 });
357
358 if !node.children.is_empty() {
360 node.resolved = node.children.iter().all(|c| c.resolved);
361 }
362 }
364
365fn collapse_node(node: &mut ProofNode) {
371 for child in &mut node.children {
373 collapse_node(child);
374 }
375
376 loop {
377 if node.children.len() == 1 && node.peer.is_none() && node.rule_cid.is_none() {
378 let child = node.children.remove(0);
380 node.children = child.children;
382 node.resolved = child.resolved;
383 node.rule_cid = child.rule_cid;
384 node.peer = child.peer;
385 } else {
388 break;
389 }
390 }
391}
392
393fn reindex_depths(node: &mut ProofNode, depth: usize) {
395 node.depth = depth;
396 for child in &mut node.children {
397 reindex_depths(child, depth + 1);
398 }
399}
400
401fn collect_remote_contributions<'a>(
403 node: &'a ProofNode,
404 out: &mut Vec<(&'a ProofNode, std::net::SocketAddr)>,
405) {
406 if let Some(ref peer_str) = node.peer {
407 if let Ok(addr) = peer_str.parse::<std::net::SocketAddr>() {
408 out.push((node, addr));
409 }
410 }
411 for child in &node.children {
412 collect_remote_contributions(child, out);
413 }
414}
415
416fn merge_nodes(a: &mut ProofNode, b: ProofNode) {
420 match (a.resolved, b.resolved) {
422 (true, false) => {
423 }
425 (false, true) => {
426 *a = b;
428 }
429 (true, true) | (false, false) => {
430 if b.size() > a.size() {
433 *a = b;
434 }
435 }
437 }
438}
439
440impl fmt::Display for ProofTree {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 let status = if self.is_complete {
443 "PROVED"
444 } else {
445 "INCOMPLETE"
446 };
447 write!(
448 f,
449 "ProofTree [{}] query={}\n{}",
450 status, self.query, self.root
451 )
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::ir::{Constant, Term};
459
460 fn atom(s: &str) -> Term {
461 Term::Const(Constant::String(s.to_string()))
462 }
463
464 #[test]
465 fn test_proof_node_fact() {
466 let goal = atom("parent_alice_bob");
467 let node = ProofNode::fact(goal.clone(), 0, None);
468 assert!(node.resolved);
469 assert!(node.peer.is_none());
470 assert!(node.rule_cid.is_none());
471 assert_eq!(node.depth, 0);
472 assert_eq!(node.size(), 1);
473 }
474
475 #[test]
476 fn test_proof_node_unresolved() {
477 let goal = atom("unknown_goal");
478 let node = ProofNode::unresolved(goal, 1);
479 assert!(!node.resolved);
480 assert_eq!(node.depth, 1);
481 }
482
483 #[test]
484 fn test_proof_node_from_rule() {
485 let goal = atom("grandparent");
486 let child1 = ProofNode::fact(atom("parent_a"), 1, None);
487 let child2 = ProofNode::fact(atom("parent_b"), 1, None);
488 let node = ProofNode::from_rule(goal, None, vec![child1, child2], 0, None);
489 assert!(node.resolved);
490 assert_eq!(node.size(), 3);
491 }
492
493 #[test]
494 fn test_proof_node_from_rule_with_unresolved_child() {
495 let goal = atom("goal");
496 let child1 = ProofNode::fact(atom("ok"), 1, None);
497 let child2 = ProofNode::unresolved(atom("fail"), 1);
498 let node = ProofNode::from_rule(goal, None, vec![child1, child2], 0, None);
499 assert!(
500 !node.resolved,
501 "parent should be unresolved if any child is"
502 );
503 }
504
505 #[test]
506 fn test_proof_tree_failed() {
507 let query = atom("impossible");
508 let tree = ProofTree::failed(query.clone());
509 assert!(!tree.is_complete);
510 assert!(tree.bindings.is_empty());
511 }
512
513 #[test]
514 fn test_proof_tree_complete() {
515 let query = atom("foo");
516 let root = ProofNode::fact(query.clone(), 0, Some("peer1".to_string()));
517 let tree = ProofTree::new(root, query, HashMap::new());
518 assert!(tree.is_complete);
519 let peers = tree.contributing_peers();
520 assert_eq!(peers, vec!["peer1".to_string()]);
521 }
522
523 #[test]
524 fn test_proof_node_max_depth() {
525 let child_inner = ProofNode::fact(atom("inner"), 3, None);
526 let child = ProofNode::from_rule(atom("middle"), None, vec![child_inner], 2, None);
527 let root = ProofNode::from_rule(atom("root"), None, vec![child], 1, None);
528 assert_eq!(root.max_depth(), 3);
529 }
530
531 #[test]
532 fn test_contributing_peers_deduplication() {
533 let child1 = ProofNode::fact(atom("a"), 1, Some("peerA".to_string()));
534 let child2 = ProofNode::fact(atom("b"), 1, Some("peerA".to_string()));
535 let child3 = ProofNode::fact(atom("c"), 1, Some("peerB".to_string()));
536 let root = ProofNode::from_rule(atom("root"), None, vec![child1, child2, child3], 0, None);
537 let mut peers = root.contributing_peers();
538 peers.sort();
539 assert_eq!(peers, vec!["peerA".to_string(), "peerB".to_string()]);
540 }
541
542 #[test]
546 fn test_proof_tree_prune_unresolved() {
547 let resolved_child = ProofNode::fact(atom("ok"), 1, None);
549 let unresolved_child = ProofNode::unresolved(atom("fail"), 1);
550 let root = ProofNode::from_rule(
551 atom("root"),
552 None,
553 vec![resolved_child, unresolved_child],
554 0,
555 None,
556 );
557
558 let mut tree = ProofTree {
559 root,
560 query: atom("root"),
561 bindings: HashMap::new(),
562 is_complete: false,
563 };
564
565 tree.prune_unresolved();
566
567 assert_eq!(
569 tree.root.children.len(),
570 1,
571 "unresolved child must be pruned"
572 );
573 assert!(
574 tree.root.children[0].resolved,
575 "remaining child must be resolved"
576 );
577 assert_eq!(
578 tree.root.children[0].goal,
579 atom("ok"),
580 "remaining child must be the resolved leaf"
581 );
582 }
583
584 #[test]
586 fn test_proof_tree_collapse_chains() {
587 let leaf = ProofNode::fact(atom("C"), 2, None);
589 let relay = ProofNode {
590 goal: atom("B"),
591 rule_cid: None,
592 peer: None,
593 children: vec![leaf],
594 resolved: true,
595 depth: 1,
596 };
597 let root = ProofNode {
598 goal: atom("A"),
599 rule_cid: None,
600 peer: None,
601 children: vec![relay],
602 resolved: true,
603 depth: 0,
604 };
605
606 let mut tree = ProofTree {
607 root,
608 query: atom("A"),
609 bindings: HashMap::new(),
610 is_complete: true,
611 };
612
613 tree.collapse_chains();
614
615 assert_eq!(
621 tree.root.children.len(),
622 0,
623 "chain should collapse so root directly holds leaf content"
624 );
625 }
626
627 #[test]
629 fn test_proof_tree_merge() {
630 let child_a = ProofNode::unresolved(atom("sub"), 1);
632 let root_a = ProofNode {
633 goal: atom("goal"),
634 rule_cid: None,
635 peer: None,
636 children: vec![child_a],
637 resolved: false,
638 depth: 0,
639 };
640 let mut tree_a = ProofTree {
641 root: root_a,
642 query: atom("goal"),
643 bindings: HashMap::new(),
644 is_complete: false,
645 };
646
647 let child_b1 = ProofNode::fact(atom("sub"), 1, None);
649 let child_b2 = ProofNode::fact(atom("extra"), 1, Some("peerX".to_string()));
650 let root_b = ProofNode {
651 goal: atom("goal"),
652 rule_cid: None,
653 peer: None,
654 children: vec![child_b1, child_b2],
655 resolved: true,
656 depth: 0,
657 };
658 let mut bindings_b = HashMap::new();
659 bindings_b.insert("X".to_string(), atom("value"));
660 let tree_b = ProofTree {
661 root: root_b,
662 query: atom("goal"),
663 bindings: bindings_b,
664 is_complete: true,
665 };
666
667 tree_a.merge(tree_b);
668
669 assert!(tree_a.is_complete, "merged tree must be complete");
671 assert_eq!(
672 tree_a.root.children.len(),
673 2,
674 "should have inherited the richer subtree"
675 );
676 assert!(
678 tree_a.bindings.contains_key("X"),
679 "bindings from other tree should be merged"
680 );
681 }
682}