1use std::collections::{HashMap, HashSet};
36
37use thiserror::Error;
38
39#[derive(Debug, Error, Clone, PartialEq, Eq)]
43pub enum VerificationError {
44 #[error("unknown rule: {0}")]
46 UnknownRule(String),
47
48 #[error("goal mismatch at node {node_id}: expected pattern {expected:?}, got {actual:?}")]
50 GoalMismatch {
51 node_id: u64,
52 expected: String,
53 actual: String,
54 },
55
56 #[error("premise mismatch at node {node_id}, premise index {index}")]
58 PremiseMismatch { node_id: u64, index: usize },
59
60 #[error("cyclic proof detected at node {0}")]
62 CyclicProof(u64),
63
64 #[error("proof is empty")]
66 EmptyProof,
67}
68
69#[derive(Debug, Clone)]
73pub struct VerificationResult {
74 pub verified: bool,
76
77 pub nodes_checked: usize,
79
80 pub nodes_memoized: usize,
82
83 pub max_depth: usize,
85
86 pub errors: Vec<VerificationError>,
88}
89
90impl VerificationResult {
91 pub fn is_valid(&self) -> bool {
93 self.verified && self.errors.is_empty()
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ProofNode {
102 pub node_id: u64,
104
105 pub rule_id: String,
107
108 pub goal: String,
110
111 pub premise_ids: Vec<u64>,
113
114 pub depth: usize,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct RuleSpec {
123 pub rule_id: String,
125
126 pub head_pattern: String,
128
129 pub arity: usize,
131}
132
133pub struct ProofVerifier {
141 pub rules: HashMap<String, RuleSpec>,
143
144 pub memo: HashMap<u64, bool>,
146
147 pub in_progress: HashSet<u64>,
149}
150
151impl ProofVerifier {
152 pub fn new() -> Self {
154 Self {
155 rules: HashMap::new(),
156 memo: HashMap::new(),
157 in_progress: HashSet::new(),
158 }
159 }
160
161 pub fn register_rule(&mut self, spec: RuleSpec) {
163 self.rules.insert(spec.rule_id.clone(), spec);
164 }
165
166 pub fn verify(&mut self, nodes: &[ProofNode]) -> VerificationResult {
174 if nodes.is_empty() {
175 return VerificationResult {
176 verified: false,
177 nodes_checked: 0,
178 nodes_memoized: 0,
179 max_depth: 0,
180 errors: vec![VerificationError::EmptyProof],
181 };
182 }
183
184 let node_map: HashMap<u64, &ProofNode> = nodes.iter().map(|n| (n.node_id, n)).collect();
186
187 let all_premise_ids: HashSet<u64> = nodes
189 .iter()
190 .flat_map(|n| n.premise_ids.iter().copied())
191 .collect();
192
193 let roots: Vec<u64> = nodes
194 .iter()
195 .map(|n| n.node_id)
196 .filter(|id| !all_premise_ids.contains(id))
197 .collect();
198
199 let root_id = if roots.len() == 1 {
200 roots[0]
201 } else {
202 nodes[0].node_id
204 };
205
206 let mut errors: Vec<VerificationError> = Vec::new();
208 let mut nodes_checked: usize = 0;
209 let mut nodes_memoized: usize = 0;
210 let mut max_depth: usize = 0;
211
212 let ok = self.verify_node(
213 root_id,
214 &node_map,
215 &mut errors,
216 &mut nodes_checked,
217 &mut nodes_memoized,
218 &mut max_depth,
219 );
220
221 self.in_progress.clear();
223
224 VerificationResult {
225 verified: ok,
226 nodes_checked,
227 nodes_memoized,
228 max_depth,
229 errors,
230 }
231 }
232
233 fn verify_node(
237 &mut self,
238 node_id: u64,
239 node_map: &HashMap<u64, &ProofNode>,
240 errors: &mut Vec<VerificationError>,
241 nodes_checked: &mut usize,
242 nodes_memoized: &mut usize,
243 max_depth: &mut usize,
244 ) -> bool {
245 if self.in_progress.contains(&node_id) {
247 errors.push(VerificationError::CyclicProof(node_id));
248 return false;
249 }
250
251 if let Some(&cached) = self.memo.get(&node_id) {
253 *nodes_memoized += 1;
254 return cached;
255 }
256
257 let node = match node_map.get(&node_id) {
259 Some(n) => *n,
260 None => {
261 errors.push(VerificationError::PremiseMismatch { node_id, index: 0 });
263 self.memo.insert(node_id, false);
264 return false;
265 }
266 };
267
268 if node.depth > *max_depth {
270 *max_depth = node.depth;
271 }
272
273 self.in_progress.insert(node_id);
275 *nodes_checked += 1;
276
277 let rule = match self.rules.get(&node.rule_id) {
279 Some(r) => r.clone(), None => {
281 errors.push(VerificationError::UnknownRule(node.rule_id.clone()));
282 self.in_progress.remove(&node_id);
283 self.memo.insert(node_id, false);
284 return false;
285 }
286 };
287
288 if !node.goal.starts_with(&rule.head_pattern) {
290 errors.push(VerificationError::GoalMismatch {
291 node_id,
292 expected: rule.head_pattern.clone(),
293 actual: node.goal.clone(),
294 });
295 self.in_progress.remove(&node_id);
296 self.memo.insert(node_id, false);
297 return false;
298 }
299
300 if node.premise_ids.len() != rule.arity {
302 let index = node.premise_ids.len().min(rule.arity);
304 errors.push(VerificationError::PremiseMismatch { node_id, index });
305 self.in_progress.remove(&node_id);
306 self.memo.insert(node_id, false);
307 return false;
308 }
309
310 let mut all_ok = true;
312 let premise_ids: Vec<u64> = node.premise_ids.clone();
313 for (i, &premise_id) in premise_ids.iter().enumerate() {
314 if !node_map.contains_key(&premise_id) {
315 errors.push(VerificationError::PremiseMismatch { node_id, index: i });
316 all_ok = false;
317 } else {
318 let premise_ok = self.verify_node(
319 premise_id,
320 node_map,
321 errors,
322 nodes_checked,
323 nodes_memoized,
324 max_depth,
325 );
326 if !premise_ok {
327 all_ok = false;
328 }
329 }
330 }
331
332 self.in_progress.remove(&node_id);
333 self.memo.insert(node_id, all_ok);
334 all_ok
335 }
336
337 pub fn clear_memo(&mut self) {
339 self.memo.clear();
340 self.in_progress.clear();
341 }
342
343 pub fn stats(&self) -> (usize, usize) {
345 (self.rules.len(), self.memo.len())
346 }
347}
348
349impl Default for ProofVerifier {
350 fn default() -> Self {
351 Self::new()
352 }
353}
354
355#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn make_verifier() -> ProofVerifier {
363 let mut v = ProofVerifier::new();
364 v.register_rule(RuleSpec {
366 rule_id: "axiom".to_string(),
367 head_pattern: "fact:".to_string(),
368 arity: 0,
369 });
370 v.register_rule(RuleSpec {
372 rule_id: "modus_ponens".to_string(),
373 head_pattern: "derived:".to_string(),
374 arity: 1,
375 });
376 v.register_rule(RuleSpec {
378 rule_id: "conjunction".to_string(),
379 head_pattern: "and:".to_string(),
380 arity: 2,
381 });
382 v
383 }
384
385 #[test]
387 fn test_empty_proof() {
388 let mut v = make_verifier();
389 let result = v.verify(&[]);
390 assert!(!result.is_valid());
391 assert_eq!(result.errors.len(), 1);
392 assert!(matches!(result.errors[0], VerificationError::EmptyProof));
393 }
394
395 #[test]
397 fn test_single_axiom() {
398 let mut v = make_verifier();
399 let nodes = vec![ProofNode {
400 node_id: 1,
401 rule_id: "axiom".to_string(),
402 goal: "fact:alice-is-human".to_string(),
403 premise_ids: vec![],
404 depth: 0,
405 }];
406 let result = v.verify(&nodes);
407 assert!(result.is_valid(), "{:?}", result.errors);
408 assert_eq!(result.nodes_checked, 1);
409 assert_eq!(result.nodes_memoized, 0);
410 assert_eq!(result.max_depth, 0);
411 }
412
413 #[test]
415 fn test_two_node_proof() {
416 let mut v = make_verifier();
417 let nodes = vec![
418 ProofNode {
419 node_id: 1,
420 rule_id: "axiom".to_string(),
421 goal: "fact:alice-is-human".to_string(),
422 premise_ids: vec![],
423 depth: 1,
424 },
425 ProofNode {
426 node_id: 2,
427 rule_id: "modus_ponens".to_string(),
428 goal: "derived:alice-is-mortal".to_string(),
429 premise_ids: vec![1],
430 depth: 0,
431 },
432 ];
433 let result = v.verify(&nodes);
434 assert!(result.is_valid(), "{:?}", result.errors);
435 assert_eq!(result.nodes_checked, 2);
436 assert_eq!(result.max_depth, 1);
437 }
438
439 #[test]
441 fn test_three_level_chain() {
442 let mut v = make_verifier();
443 let nodes = vec![
445 ProofNode {
446 node_id: 1,
447 rule_id: "axiom".to_string(),
448 goal: "fact:base".to_string(),
449 premise_ids: vec![],
450 depth: 2,
451 },
452 ProofNode {
453 node_id: 2,
454 rule_id: "modus_ponens".to_string(),
455 goal: "derived:mid".to_string(),
456 premise_ids: vec![1],
457 depth: 1,
458 },
459 ProofNode {
460 node_id: 3,
461 rule_id: "modus_ponens".to_string(),
462 goal: "derived:top".to_string(),
463 premise_ids: vec![2],
464 depth: 0,
465 },
466 ];
467 let result = v.verify(&nodes);
468 assert!(result.is_valid(), "{:?}", result.errors);
469 assert_eq!(result.nodes_checked, 3);
470 assert_eq!(result.max_depth, 2);
471 }
472
473 #[test]
475 fn test_unknown_rule() {
476 let mut v = make_verifier();
477 let nodes = vec![ProofNode {
478 node_id: 1,
479 rule_id: "no_such_rule".to_string(),
480 goal: "fact:something".to_string(),
481 premise_ids: vec![],
482 depth: 0,
483 }];
484 let result = v.verify(&nodes);
485 assert!(!result.is_valid());
486 assert!(result.errors.iter().any(|e| matches!(
487 e,
488 VerificationError::UnknownRule(id) if id == "no_such_rule"
489 )));
490 }
491
492 #[test]
494 fn test_arity_mismatch() {
495 let mut v = make_verifier();
496 let nodes = vec![ProofNode {
498 node_id: 1,
499 rule_id: "modus_ponens".to_string(),
500 goal: "derived:something".to_string(),
501 premise_ids: vec![], depth: 0,
503 }];
504 let result = v.verify(&nodes);
505 assert!(!result.is_valid());
506 assert!(result
507 .errors
508 .iter()
509 .any(|e| matches!(e, VerificationError::PremiseMismatch { node_id: 1, .. })));
510 }
511
512 #[test]
514 fn test_goal_mismatch() {
515 let mut v = make_verifier();
516 let nodes = vec![ProofNode {
517 node_id: 1,
518 rule_id: "axiom".to_string(),
519 goal: "wrong_prefix:something".to_string(), premise_ids: vec![],
521 depth: 0,
522 }];
523 let result = v.verify(&nodes);
524 assert!(!result.is_valid());
525 assert!(result
526 .errors
527 .iter()
528 .any(|e| matches!(e, VerificationError::GoalMismatch { node_id: 1, .. })));
529 }
530
531 #[test]
533 fn test_cycle_detection() {
534 let mut v = make_verifier();
535 let nodes = vec![
538 ProofNode {
539 node_id: 1,
540 rule_id: "modus_ponens".to_string(),
541 goal: "derived:a".to_string(),
542 premise_ids: vec![2],
543 depth: 0,
544 },
545 ProofNode {
546 node_id: 2,
547 rule_id: "modus_ponens".to_string(),
548 goal: "derived:b".to_string(),
549 premise_ids: vec![1],
550 depth: 1,
551 },
552 ];
553 let result = v.verify(&nodes);
554 assert!(!result.is_valid());
555 assert!(result
556 .errors
557 .iter()
558 .any(|e| matches!(e, VerificationError::CyclicProof(_))));
559 }
560
561 #[test]
563 fn test_memo_cache_hits() {
564 let mut v = make_verifier();
565 let nodes = vec![
569 ProofNode {
570 node_id: 0,
571 rule_id: "axiom".to_string(),
572 goal: "fact:shared".to_string(),
573 premise_ids: vec![],
574 depth: 2,
575 },
576 ProofNode {
577 node_id: 1,
578 rule_id: "modus_ponens".to_string(),
579 goal: "derived:left".to_string(),
580 premise_ids: vec![0],
581 depth: 1,
582 },
583 ProofNode {
584 node_id: 2,
585 rule_id: "modus_ponens".to_string(),
586 goal: "derived:right".to_string(),
587 premise_ids: vec![0],
588 depth: 1,
589 },
590 ProofNode {
591 node_id: 3,
592 rule_id: "conjunction".to_string(),
593 goal: "and:both".to_string(),
594 premise_ids: vec![1, 2],
595 depth: 0,
596 },
597 ];
598 let result = v.verify(&nodes);
599 assert!(result.is_valid(), "{:?}", result.errors);
600 assert_eq!(result.nodes_memoized, 1);
602 assert_eq!(result.nodes_checked, 4);
604 }
605
606 #[test]
608 fn test_clear_memo() {
609 let mut v = make_verifier();
610 let nodes = vec![ProofNode {
611 node_id: 42,
612 rule_id: "axiom".to_string(),
613 goal: "fact:x".to_string(),
614 premise_ids: vec![],
615 depth: 0,
616 }];
617 let _ = v.verify(&nodes);
618 assert!(!v.memo.is_empty());
619
620 v.clear_memo();
621 assert!(v.memo.is_empty());
622 assert!(v.in_progress.is_empty());
623 }
624
625 #[test]
627 fn test_stats() {
628 let mut v = make_verifier();
629 let (rules, memo) = v.stats();
630 assert_eq!(rules, 3); assert_eq!(memo, 0);
632
633 let nodes = vec![ProofNode {
634 node_id: 99,
635 rule_id: "axiom".to_string(),
636 goal: "fact:y".to_string(),
637 premise_ids: vec![],
638 depth: 0,
639 }];
640 let _ = v.verify(&nodes);
641 let (rules2, memo2) = v.stats();
642 assert_eq!(rules2, 3);
643 assert_eq!(memo2, 1);
644 }
645
646 #[test]
648 fn test_is_valid_false_on_errors() {
649 let result = VerificationResult {
650 verified: true,
651 nodes_checked: 1,
652 nodes_memoized: 0,
653 max_depth: 0,
654 errors: vec![VerificationError::EmptyProof],
655 };
656 assert!(!result.is_valid());
657 }
658
659 #[test]
661 fn test_multiple_errors_collected() {
662 let mut v = make_verifier();
663 let nodes = vec![
665 ProofNode {
666 node_id: 1,
667 rule_id: "bad_rule_a".to_string(),
668 goal: "fact:a".to_string(),
669 premise_ids: vec![],
670 depth: 1,
671 },
672 ProofNode {
673 node_id: 2,
674 rule_id: "bad_rule_b".to_string(),
675 goal: "fact:b".to_string(),
676 premise_ids: vec![],
677 depth: 1,
678 },
679 ProofNode {
680 node_id: 3,
681 rule_id: "conjunction".to_string(),
682 goal: "and:ab".to_string(),
683 premise_ids: vec![1, 2],
684 depth: 0,
685 },
686 ];
687 let result = v.verify(&nodes);
688 assert!(!result.is_valid());
689 let unknown_count = result
691 .errors
692 .iter()
693 .filter(|e| matches!(e, VerificationError::UnknownRule(_)))
694 .count();
695 assert!(
696 unknown_count >= 2,
697 "expected >=2 UnknownRule errors, got {unknown_count}"
698 );
699 }
700
701 #[test]
703 fn test_max_depth_computed() {
704 let mut v = make_verifier();
705 let nodes = vec![
706 ProofNode {
707 node_id: 1,
708 rule_id: "axiom".to_string(),
709 goal: "fact:a".to_string(),
710 premise_ids: vec![],
711 depth: 5,
712 },
713 ProofNode {
714 node_id: 2,
715 rule_id: "modus_ponens".to_string(),
716 goal: "derived:b".to_string(),
717 premise_ids: vec![1],
718 depth: 2,
719 },
720 ];
721 let result = v.verify(&nodes);
722 assert!(result.is_valid(), "{:?}", result.errors);
723 assert_eq!(result.max_depth, 5);
724 }
725
726 #[test]
728 fn test_register_rule_replaces() {
729 let mut v = ProofVerifier::new();
730 v.register_rule(RuleSpec {
731 rule_id: "my_rule".to_string(),
732 head_pattern: "old:".to_string(),
733 arity: 0,
734 });
735 v.register_rule(RuleSpec {
737 rule_id: "my_rule".to_string(),
738 head_pattern: "new:".to_string(),
739 arity: 0,
740 });
741 assert_eq!(v.rules.len(), 1);
742 assert_eq!(v.rules["my_rule"].head_pattern, "new:");
743
744 let nodes = vec![ProofNode {
746 node_id: 1,
747 rule_id: "my_rule".to_string(),
748 goal: "new:something".to_string(),
749 premise_ids: vec![],
750 depth: 0,
751 }];
752 let result = v.verify(&nodes);
753 assert!(result.is_valid(), "{:?}", result.errors);
754 }
755
756 #[test]
758 fn test_large_flat_proof() {
759 let mut v = ProofVerifier::new();
760 v.register_rule(RuleSpec {
764 rule_id: "axiom".to_string(),
765 head_pattern: "fact:".to_string(),
766 arity: 0,
767 });
768
769 let nodes: Vec<ProofNode> = (0u64..1000)
771 .map(|i| ProofNode {
772 node_id: i,
773 rule_id: "axiom".to_string(),
774 goal: format!("fact:item-{i}"),
775 premise_ids: vec![],
776 depth: 0,
777 })
778 .collect();
779
780 let result = v.verify(&nodes);
784 assert!(result.is_valid(), "{:?}", result.errors);
786 assert_eq!(result.nodes_checked, 1);
787 assert_eq!(result.nodes_memoized, 0);
788 }
789}