1use std::collections::{HashMap, HashSet, VecDeque};
10
11use mrs_core::clause::{Clause, ClauseCertificate, ClauseId, ClauseSource, Literal};
12use mrs_core::formula::Atom;
13use mrs_core::symbol::SymbolTable;
14use mrs_core::term::{Term, VarId};
15use mrs_core::witness::{DemodStepWitness, ProofNodeId, ProofWitness};
16use mrs_unify::matching::match_term;
17
18#[derive(Debug, Clone)]
20pub struct ElaboratedProof {
21 pub clauses: Vec<Clause>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ElaborationError {
27 Ambiguous(String),
29 Cyclic(String),
31 LimitExceeded(String),
33 CannotReproduce(String),
35 Inconclusive(String),
37}
38
39impl std::fmt::Display for ElaborationError {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Ambiguous(msg) => write!(f, "Ambiguous: {msg}"),
43 Self::Cyclic(msg) => write!(f, "Cyclic: {msg}"),
44 Self::LimitExceeded(msg) => write!(f, "LimitExceeded: {msg}"),
45 Self::CannotReproduce(msg) => write!(f, "CannotReproduce: {msg}"),
46 Self::Inconclusive(msg) => write!(f, "Inconclusive: {msg}"),
47 }
48 }
49}
50
51impl std::error::Error for ElaborationError {}
52
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum AtomSide {
56 EqLeft,
57 EqRight,
58 PredArg(usize),
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
63pub struct RewritePosition {
64 pub lit_idx: usize,
65 pub atom_side: AtomSide,
66 pub term_path: Vec<usize>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct OrientedRule {
72 pub parent_id: ClauseId,
73 pub lhs: Term,
74 pub rhs: Term,
75}
76
77#[derive(Debug, Clone)]
79pub struct DemodStep {
80 pub position: RewritePosition,
81 pub rule_parent_id: ClauseId,
82 pub resulting_clause: Clause,
83}
84
85pub fn get_subterm<'a>(term: &'a Term, path: &[usize]) -> Option<&'a Term> {
87 let mut curr = term;
88 for &idx in path {
89 match curr {
90 Term::App(_, args) if idx < args.len() => {
91 curr = &args[idx];
92 }
93 _ => return None,
94 }
95 }
96 Some(curr)
97}
98
99pub fn replace_subterm(term: &Term, path: &[usize], replacement: Term) -> Option<Term> {
101 if path.is_empty() {
102 return Some(replacement);
103 }
104 match term {
105 Term::App(sym, args) => {
106 let idx = path[0];
107 if idx >= args.len() {
108 return None;
109 }
110 let mut new_args = args.clone();
111 new_args[idx] = replace_subterm(&args[idx], &path[1..], replacement)?;
112 Some(Term::App(*sym, new_args))
113 }
114 Term::Var(_) => None,
115 }
116}
117
118pub fn get_subterm_in_clause<'a>(clause: &'a Clause, pos: &RewritePosition) -> Option<&'a Term> {
120 let lit = clause.literals.get(pos.lit_idx)?;
121 let term = match (&lit.atom, &pos.atom_side) {
122 (Atom::Eq(l, _), AtomSide::EqLeft) => l,
123 (Atom::Eq(_, r), AtomSide::EqRight) => r,
124 (Atom::Pred(_, args), AtomSide::PredArg(idx)) => args.get(*idx)?,
125 _ => return None,
126 };
127 get_subterm(term, &pos.term_path)
128}
129
130pub fn replace_subterm_in_clause(
132 clause: &Clause,
133 pos: &RewritePosition,
134 replacement: Term,
135) -> Option<Clause> {
136 let mut new_lits = clause.literals.clone();
137 let lit = new_lits.get_mut(pos.lit_idx)?;
138 match (&mut lit.atom, &pos.atom_side) {
139 (Atom::Eq(l, _), AtomSide::EqLeft) => {
140 *l = replace_subterm(l, &pos.term_path, replacement)?;
141 }
142 (Atom::Eq(_, r), AtomSide::EqRight) => {
143 *r = replace_subterm(r, &pos.term_path, replacement)?;
144 }
145 (Atom::Pred(_, args), AtomSide::PredArg(idx)) => {
146 let target_arg = args.get_mut(*idx)?;
147 *target_arg = replace_subterm(target_arg, &pos.term_path, replacement)?;
148 }
149 _ => return None,
150 }
151 Some(Clause {
152 id: clause.id,
153 literals: new_lits,
154 source: clause.source.clone(),
155 avatar: clause.avatar.clone(),
156 distance: clause.distance,
157 certificate: clause.certificate.clone(),
158 formula: clause.formula.clone(),
159 proof_id: clause.proof_id,
160 witness: clause.witness.clone(),
161 })
162}
163
164fn collect_term_paths(term: &Term, current_path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
165 out.push(current_path.clone());
166 if let Term::App(_, args) = term {
167 for (i, arg) in args.iter().enumerate() {
168 current_path.push(i);
169 collect_term_paths(arg, current_path, out);
170 current_path.pop();
171 }
172 }
173}
174
175pub fn collect_all_positions(clause: &Clause) -> Vec<RewritePosition> {
177 let mut positions = Vec::new();
178 for (lit_idx, lit) in clause.literals.iter().enumerate() {
179 match &lit.atom {
180 Atom::Eq(l, r) => {
181 let mut path = Vec::new();
182 let mut l_paths = Vec::new();
183 collect_term_paths(l, &mut path, &mut l_paths);
184 for p in l_paths {
185 positions.push(RewritePosition {
186 lit_idx,
187 atom_side: AtomSide::EqLeft,
188 term_path: p,
189 });
190 }
191 let mut r_paths = Vec::new();
192 collect_term_paths(r, &mut path, &mut r_paths);
193 for p in r_paths {
194 positions.push(RewritePosition {
195 lit_idx,
196 atom_side: AtomSide::EqRight,
197 term_path: p,
198 });
199 }
200 }
201 Atom::Pred(_, args) => {
202 for (arg_idx, arg) in args.iter().enumerate() {
203 let mut path = Vec::new();
204 let mut arg_paths = Vec::new();
205 collect_term_paths(arg, &mut path, &mut arg_paths);
206 for p in arg_paths {
207 positions.push(RewritePosition {
208 lit_idx,
209 atom_side: AtomSide::PredArg(arg_idx),
210 term_path: p,
211 });
212 }
213 }
214 }
215 }
216 }
217 positions
218}
219
220fn term_vars(term: &Term, vars: &mut HashSet<VarId>) {
221 match term {
222 Term::Var(v) => {
223 vars.insert(*v);
224 }
225 Term::App(_, args) => {
226 for arg in args {
227 term_vars(arg, vars);
228 }
229 }
230 }
231}
232
233fn term_weight(term: &Term) -> usize {
234 match term {
235 Term::Var(_) => 1,
236 Term::App(_, args) => 1 + args.iter().map(term_weight).sum::<usize>(),
237 }
238}
239
240pub fn extract_oriented_rules(rule_clause: &Clause) -> Result<Vec<OrientedRule>, ElaborationError> {
242 let mut eq_lit = None;
243 for lit in &rule_clause.literals {
244 if lit.positive && matches!(lit.atom, Atom::Eq(..)) {
245 if eq_lit.is_some() {
246 return Err(ElaborationError::Inconclusive(format!(
247 "Demodulation parent c{} has multiple equalities",
248 rule_clause.id.0
249 )));
250 }
251 eq_lit = Some(lit);
252 }
253 }
254 let Some(eq_lit) = eq_lit else {
255 return Err(ElaborationError::Inconclusive(format!(
256 "Demodulation parent c{} must contain a positive equality",
257 rule_clause.id.0
258 )));
259 };
260 let Atom::Eq(l, r) = &eq_lit.atom else {
261 unreachable!();
262 };
263 if l == r {
264 return Ok(Vec::new());
265 }
266
267 let mut l_vars = HashSet::new();
268 let mut r_vars = HashSet::new();
269 term_vars(l, &mut l_vars);
270 term_vars(r, &mut r_vars);
271
272 let l_weight = term_weight(l);
273 let r_weight = term_weight(r);
274
275 let mut rules = Vec::new();
276 if r_vars.is_subset(&l_vars) && l_weight >= r_weight {
277 rules.push(OrientedRule {
278 parent_id: rule_clause.id,
279 lhs: l.clone(),
280 rhs: r.clone(),
281 });
282 } else if l_vars.is_subset(&r_vars) && r_weight > l_weight {
283 rules.push(OrientedRule {
284 parent_id: rule_clause.id,
285 lhs: r.clone(),
286 rhs: l.clone(),
287 });
288 }
289
290 if rules.is_empty() {
291 return Err(ElaborationError::Inconclusive(format!(
292 "Demodulation parent c{} has no non-expanding orientation",
293 rule_clause.id.0
294 )));
295 }
296 Ok(rules)
297}
298
299fn shift_term(term: &Term, offset: VarId) -> Term {
300 match term {
301 Term::Var(v) => Term::Var(v + offset),
302 Term::App(f, args) => Term::App(*f, args.iter().map(|a| shift_term(a, offset)).collect()),
303 }
304}
305
306fn max_var_term(term: &Term) -> VarId {
307 let mut vars = HashSet::new();
308 term_vars(term, &mut vars);
309 vars.into_iter().max().map_or(0, |v| v + 1)
310}
311
312fn max_var_clause(clause: &Clause) -> VarId {
313 let mut max_v = 0;
314 for lit in &clause.literals {
315 match &lit.atom {
316 Atom::Eq(l, r) => {
317 max_v = max_v.max(max_var_term(l)).max(max_var_term(r));
318 }
319 Atom::Pred(_, args) => {
320 for a in args {
321 max_v = max_v.max(max_var_term(a));
322 }
323 }
324 }
325 }
326 max_v
327}
328
329pub fn reconstruct_demodulation(
332 target: &Clause,
333 conclusion: &Clause,
334 rule_parents: &[Clause],
335 next_clause_id: &mut u64,
336) -> Result<Vec<Clause>, ElaborationError> {
337 let offset = max_var_clause(target).max(max_var_clause(conclusion));
338 let mut all_rules = Vec::new();
339 for p in rule_parents {
340 let rules = extract_oriented_rules(p)?;
341 for mut r in rules {
342 if offset > 0 {
343 r.lhs = shift_term(&r.lhs, offset);
344 r.rhs = shift_term(&r.rhs, offset);
345 }
346 all_rules.push(r);
347 }
348 }
349 if all_rules.is_empty() {
350 return Err(ElaborationError::CannotReproduce(format!(
351 "No usable rewrite rules for demodulation of c{}",
352 conclusion.id.0
353 )));
354 }
355
356 if target.literals == conclusion.literals {
357 return Ok(vec![conclusion.clone()]);
358 }
359
360 let max_depth = 25;
361
362 let mut current = target.clone();
364 let mut visited = HashSet::new();
365 visited.insert(target.literals.to_vec());
366 let mut canonical_trace = Vec::new();
367 let mut reached_canonical = false;
368
369 for _ in 0..max_depth {
370 if current.literals == conclusion.literals {
371 reached_canonical = true;
372 break;
373 }
374
375 let positions = collect_all_positions(¤t);
376 let mut step_taken = false;
377
378 for pos in &positions {
379 let Some(subterm) = get_subterm_in_clause(¤t, pos) else {
380 continue;
381 };
382
383 let mut matching_rules = Vec::new();
384 for rule in &all_rules {
385 if let Ok(sigma) = match_term(&rule.lhs, subterm) {
386 let rewritten = sigma.apply_term(&rule.rhs);
387 matching_rules.push((rule, rewritten));
388 }
389 }
390
391 if matching_rules.is_empty() {
392 continue;
393 }
394
395 if matching_rules.len() > 1 {
397 let first_parent = matching_rules[0].0.parent_id;
398 let first_rewritten = &matching_rules[0].1;
399 let has_divergence = matching_rules[1..]
400 .iter()
401 .any(|(r, rw)| r.parent_id != first_parent || rw != first_rewritten);
402 if has_divergence {
403 return Err(ElaborationError::Ambiguous(format!(
404 "Ambiguous demodulation rewrite rules at position in c{}",
405 conclusion.id.0
406 )));
407 }
408 }
409
410 let (chosen_rule, rewritten_subterm) = &matching_rules[0];
411 let Some(next_clause) =
412 replace_subterm_in_clause(¤t, pos, (*rewritten_subterm).clone())
413 else {
414 continue;
415 };
416
417 let lits_vec = next_clause.literals.to_vec();
418 if visited.contains(&lits_vec) {
419 continue;
420 }
421 visited.insert(lits_vec);
422
423 canonical_trace.push(DemodStep {
424 position: pos.clone(),
425 rule_parent_id: chosen_rule.parent_id,
426 resulting_clause: next_clause.clone(),
427 });
428 current = next_clause;
429 step_taken = true;
430 break;
431 }
432
433 if !step_taken {
434 break;
435 }
436 }
437
438 if current.literals == conclusion.literals {
439 reached_canonical = true;
440 }
441
442 let steps = if reached_canonical {
443 canonical_trace
444 } else {
445 struct DemodSearch<'a> {
447 goal: &'a Clause,
448 rules: &'a [OrientedRule],
449 max_depth: usize,
450 visited_clauses: HashSet<Vec<Literal>>,
451 current_path: Vec<DemodStep>,
452 found_paths: Vec<Vec<DemodStep>>,
453 }
454
455 impl<'a> DemodSearch<'a> {
456 fn search(&mut self, current: &Clause, depth: usize) -> Result<(), ElaborationError> {
457 if current.literals == self.goal.literals {
458 self.found_paths.push(self.current_path.clone());
459 return Ok(());
460 }
461 if depth >= self.max_depth {
462 return Err(ElaborationError::LimitExceeded(
463 "Demodulation depth exceeded bounded search limit".into(),
464 ));
465 }
466
467 let positions = collect_all_positions(current);
468 for pos in &positions {
469 let Some(subterm) = get_subterm_in_clause(current, pos) else {
470 continue;
471 };
472 for rule in self.rules {
473 if let Ok(sigma) = match_term(&rule.lhs, subterm) {
474 let rewritten_subterm = sigma.apply_term(&rule.rhs);
475 if let Some(next_clause) =
476 replace_subterm_in_clause(current, pos, rewritten_subterm)
477 {
478 let lits_vec = next_clause.literals.to_vec();
479 if self.visited_clauses.contains(&lits_vec) {
480 continue;
481 }
482 self.visited_clauses.insert(lits_vec.clone());
483 self.current_path.push(DemodStep {
484 position: pos.clone(),
485 rule_parent_id: rule.parent_id,
486 resulting_clause: next_clause.clone(),
487 });
488
489 self.search(&next_clause, depth + 1)?;
490
491 self.current_path.pop();
492 self.visited_clauses.remove(&lits_vec);
493
494 if self.found_paths.len() > 1 {
495 return Ok(());
496 }
497 }
498 }
499 }
500 }
501 Ok(())
502 }
503 }
504
505 let mut visited_search = HashSet::new();
506 visited_search.insert(target.literals.to_vec());
507
508 let mut searcher = DemodSearch {
509 goal: conclusion,
510 rules: &all_rules,
511 max_depth,
512 visited_clauses: visited_search,
513 current_path: Vec::new(),
514 found_paths: Vec::new(),
515 };
516
517 searcher.search(target, 0)?;
518
519 let mut found_paths = searcher.found_paths;
520
521 if found_paths.is_empty() {
522 return Err(ElaborationError::CannotReproduce(format!(
523 "Could not reproduce conclusion c{} from target c{} and equality rules",
524 conclusion.id.0, target.id.0
525 )));
526 }
527 if found_paths.len() > 1 {
528 return Err(ElaborationError::Ambiguous(format!(
529 "Ambiguous demodulation trace for c{}: multiple candidate rewrite sequences reach conclusion",
530 conclusion.id.0
531 )));
532 }
533 found_paths.remove(0)
534 };
535
536 if steps.is_empty() {
537 return Ok(vec![conclusion.clone()]);
538 }
539
540 let mut generated_clauses = Vec::with_capacity(steps.len());
541 let mut prev_id = target.id;
542 let mut prev_proof_id = target.proof_id.unwrap_or(ProofNodeId(target.id.0));
543 let rule_clauses: HashMap<ClauseId, &Clause> = rule_parents
544 .iter()
545 .map(|clause| (clause.id, clause))
546 .collect();
547
548 for (i, step) in steps.iter().enumerate() {
549 let is_last = i + 1 == steps.len();
550 let clause_id = if is_last {
551 conclusion.id
552 } else {
553 let id = ClauseId(*next_clause_id);
554 *next_clause_id += 1;
555 id
556 };
557
558 let clause = Clause {
559 id: clause_id,
560 literals: step.resulting_clause.literals.clone(),
561 source: ClauseSource::Inference {
562 rule: "demodulation",
563 parents: vec![prev_id, step.rule_parent_id].into(),
564 },
565 avatar: conclusion.avatar.clone(),
566 distance: conclusion.distance,
567 certificate: if is_last {
568 conclusion.certificate.clone()
569 } else {
570 None
571 },
572 formula: if is_last {
573 conclusion.formula.clone()
574 } else {
575 None
576 },
577 proof_id: if is_last { conclusion.proof_id } else { None },
578 witness: rule_clauses
584 .get(&step.rule_parent_id)
585 .map(|rule| single_rewrite_witness(rule, &step.position, prev_proof_id)),
586 };
587 prev_proof_id = clause.proof_id.unwrap_or(ProofNodeId(clause_id.0));
588 prev_id = clause_id;
589 generated_clauses.push(clause);
590 }
591
592 Ok(generated_clauses)
593}
594
595fn single_rewrite_witness(
599 rule: &Clause,
600 position: &RewritePosition,
601 target: ProofNodeId,
602) -> ProofWitness {
603 let rule_node = rule.proof_id.unwrap_or(ProofNodeId(rule.id.0));
604 let mut term_path = Vec::with_capacity(position.term_path.len() + 1);
605 match position.atom_side {
606 AtomSide::EqLeft => term_path.push(0),
607 AtomSide::EqRight => term_path.push(1),
608 AtomSide::PredArg(index) => term_path.push(index),
609 }
610 term_path.extend(position.term_path.iter().copied());
611 ProofWitness::Demodulation {
612 target,
613 rule_parents: vec![rule_node],
614 steps: vec![DemodStepWitness {
615 rule_parent: rule_node,
616 lit_idx: position.lit_idx,
617 term_path,
618 substitution: None,
619 }],
620 }
621}
622
623pub fn topological_sort(clauses: &[Clause]) -> Result<Vec<Clause>, ElaborationError> {
626 let mut id_to_clause: HashMap<ClauseId, Clause> = HashMap::default();
627 let mut in_degree: HashMap<ClauseId, usize> = HashMap::default();
628 let mut dependents: HashMap<ClauseId, Vec<ClauseId>> = HashMap::default();
629 let mut clause_order: HashMap<ClauseId, usize> = HashMap::default();
630
631 for (idx, c) in clauses.iter().enumerate() {
632 if id_to_clause.insert(c.id, c.clone()).is_some() {
633 return Err(ElaborationError::Inconclusive(format!(
634 "duplicate clause id c{} in proof",
635 c.id.0
636 )));
637 }
638 clause_order.insert(c.id, idx);
639 in_degree.insert(c.id, 0);
640 }
641
642 for c in clauses {
643 let source_parents: &[ClauseId] = match &c.source {
647 ClauseSource::Inference { parents, .. } | ClauseSource::Introduced { parents, .. } => {
648 parents.as_slice()
649 }
650 ClauseSource::Input { .. } => &[],
651 };
652 let ac_rule = match &c.source {
653 ClauseSource::Inference { rule, .. }
654 if *rule == "ac_superposition" || *rule == "ac_resolution" =>
655 {
656 Some(*rule)
657 }
658 _ => None,
659 };
660 if ac_rule.is_some() && source_parents.len() < 3 {
661 return Err(ElaborationError::Inconclusive(format!(
662 "clause c{} AC inference must cite two inference parents and at least one AC axiom",
663 c.id.0
664 )));
665 }
666 let mut parents = source_parents.to_vec();
667 if let Some(cert) = &c.certificate {
668 match cert {
669 ClauseCertificate::AvatarComponent { split_parent, .. } => {
670 parents.push(*split_parent);
671 }
672 ClauseCertificate::AvatarSatRefutation {
673 split_nodes,
674 branch_roots,
675 ..
676 } => {
677 parents.extend_from_slice(split_nodes);
678 parents.extend_from_slice(branch_roots);
679 }
680 ClauseCertificate::SatBackedRefutation { inputs, .. } => {
681 parents.extend_from_slice(inputs);
682 }
683 _ => {}
684 }
685 }
686
687 if c.proof_id.is_some_and(|id| id.0 != c.id.0) {
688 return Err(ElaborationError::Inconclusive(format!(
689 "clause c{} has a mismatched proof identity",
690 c.id.0
691 )));
692 }
693 if let Some(witness) = &c.witness {
694 if let Some(rule) = ac_rule {
695 let witness_matches_rule = match (rule, witness) {
696 ("ac_superposition", ProofWitness::Superposition { .. })
697 | ("ac_resolution", ProofWitness::Resolution { .. }) => true,
698 (
699 rule,
700 ProofWitness::Legacy {
701 rule: witness_rule, ..
702 },
703 ) => rule == *witness_rule,
704 _ => false,
705 };
706 if !witness_matches_rule {
707 return Err(ElaborationError::Inconclusive(format!(
708 "clause c{} AC rule {rule} has an incompatible witness",
709 c.id.0
710 )));
711 }
712 }
713 for reference in witness.parent_ids() {
714 if !source_parents.contains(&ClauseId(reference.0)) {
715 return Err(ElaborationError::Inconclusive(format!(
716 "clause c{} witness references uncited parent c{}",
717 c.id.0, reference.0
718 )));
719 }
720 }
721 match witness {
722 ProofWitness::Resolution {
723 parent_left,
724 parent_right,
725 ..
726 }
727 | ProofWitness::Superposition {
728 lhs_parent: parent_left,
729 rhs_parent: parent_right,
730 ..
731 } => {
732 let is_ac = ac_rule.is_some();
733 if source_parents.first() != Some(&ClauseId(parent_left.0))
737 || source_parents.get(1) != Some(&ClauseId(parent_right.0))
738 || (!is_ac && source_parents.len() != 2)
739 || (is_ac && source_parents.len() < 3)
740 {
741 return Err(ElaborationError::Inconclusive(format!(
742 "clause c{} binary witness does not match cited parents {:?}",
743 c.id.0, source_parents
744 )));
745 }
746 }
747 ProofWitness::SubsumptionResolution {
748 target: parent_left,
749 subsumer: parent_right,
750 ..
751 } if source_parents.len() != 2
752 || !source_parents.contains(&ClauseId(parent_left.0))
753 || !source_parents.contains(&ClauseId(parent_right.0)) =>
754 {
755 return Err(ElaborationError::Inconclusive(format!(
756 "clause c{} binary witness does not match cited parents {:?}",
757 c.id.0, source_parents
758 )));
759 }
760 ProofWitness::Factoring { parent, .. }
761 | ProofWitness::EqualityResolution { parent, .. }
762 | ProofWitness::EqualityFactoring { parent, .. }
763 | ProofWitness::Condensation { parent, .. }
764 if source_parents.len() != 1 || source_parents[0] != ClauseId(parent.0) =>
765 {
766 return Err(ElaborationError::Inconclusive(format!(
767 "clause c{} unary witness parent c{} does not match cited parents {:?}",
768 c.id.0, parent.0, source_parents
769 )));
770 }
771 _ => {}
772 }
773 }
774
775 let mut unique_parents = HashSet::new();
779 for p in parents {
780 if !id_to_clause.contains_key(&p) {
781 return Err(ElaborationError::Inconclusive(format!(
782 "clause c{} cites missing parent c{}",
783 c.id.0, p.0
784 )));
785 }
786 if unique_parents.insert(p) {
787 dependents.entry(p).or_default().push(c.id);
788 *in_degree.entry(c.id).or_default() += 1;
789 }
790 }
791 }
792
793 let mut ready = VecDeque::new();
794 let mut initial_ready: Vec<ClauseId> = in_degree
795 .iter()
796 .filter(|(_, deg)| **deg == 0)
797 .map(|(&id, _)| id)
798 .collect();
799
800 initial_ready.sort_by_key(|id| clause_order.get(id).copied().unwrap_or(0));
802 ready.extend(initial_ready);
803
804 let mut sorted = Vec::with_capacity(clauses.len());
805 while let Some(curr_id) = ready.pop_front() {
806 if let Some(clause) = id_to_clause.remove(&curr_id) {
807 sorted.push(clause);
808 }
809 if let Some(children) = dependents.get(&curr_id) {
810 let mut newly_ready = Vec::new();
811 for &child_id in children {
812 if let Some(deg) = in_degree.get_mut(&child_id) {
813 *deg = deg.saturating_sub(1);
814 if *deg == 0 {
815 newly_ready.push(child_id);
816 }
817 }
818 }
819 newly_ready.sort_by_key(|id| clause_order.get(id).copied().unwrap_or(0));
820 ready.extend(newly_ready);
821 }
822 }
823
824 if sorted.len() < clauses.len() {
825 return Err(ElaborationError::Cyclic(
826 "Cyclic parent dependency detected in proof DAG".into(),
827 ));
828 }
829
830 Ok(sorted)
831}
832
833pub fn elaborate(
836 proof: &[Clause],
837 symbols: &SymbolTable,
838) -> Result<ElaboratedProof, ElaborationError> {
839 if proof.is_empty() {
840 return Ok(ElaboratedProof {
841 clauses: Vec::new(),
842 });
843 }
844
845 let mut next_clause_id = proof.iter().map(|c| c.id.0).max().unwrap_or(0) + 1;
846 let mut clause_map: HashMap<ClauseId, Clause> = HashMap::default();
847 for c in proof {
848 clause_map.insert(c.id, c.clone());
849 }
850
851 for c in proof {
853 if let ClauseSource::Introduced { symbol, .. } = &c.source {
854 let sym_name = symbols.resolve(*symbol);
855 if sym_name.is_empty() {
856 return Err(ElaborationError::Inconclusive(format!(
857 "Clause c{} introduces an unresolvable symbol",
858 c.id.0
859 )));
860 }
861 }
862 }
863
864 let mut expanded_clauses = Vec::new();
865
866 for clause in proof {
867 match &clause.source {
868 ClauseSource::Inference { rule, parents } if *rule == "demodulation" => {
869 if parents.len() < 2 {
870 return Err(ElaborationError::Inconclusive(format!(
871 "Demodulation node c{} requires at least 2 parents (target + rule)",
872 clause.id.0
873 )));
874 }
875 let target_id = parents[0];
876 let Some(target_clause) = clause_map.get(&target_id) else {
877 return Err(ElaborationError::Inconclusive(format!(
878 "Demodulation target c{} not found for c{}",
879 target_id.0, clause.id.0
880 )));
881 };
882 let mut rule_clauses = Vec::new();
883 for &pid in &parents[1..] {
884 let Some(rule_clause) = clause_map.get(&pid) else {
885 return Err(ElaborationError::Inconclusive(format!(
886 "Demodulation rule parent c{} not found for c{}",
887 pid.0, clause.id.0
888 )));
889 };
890 rule_clauses.push(rule_clause.clone());
891 }
892
893 let intermediate = reconstruct_demodulation(
894 target_clause,
895 clause,
896 &rule_clauses,
897 &mut next_clause_id,
898 )?;
899
900 for step_clause in intermediate {
901 clause_map.insert(step_clause.id, step_clause.clone());
902 expanded_clauses.push(step_clause);
903 }
904 }
905 _ => {
906 expanded_clauses.push(clause.clone());
907 }
908 }
909 }
910
911 let sorted = topological_sort(&expanded_clauses)?;
912 Ok(ElaboratedProof { clauses: sorted })
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918
919 #[test]
920 fn export_refuses_uncited_witness_parents_and_mismatched_identity() {
921 let input = dummy_input_clause(10, vec![]);
922 let mut derived = Clause::new(
923 ClauseId(20),
924 vec![],
925 ClauseSource::Inference {
926 rule: "copy",
927 parents: vec![ClauseId(10)].into(),
928 },
929 );
930 derived.witness = Some(ProofWitness::EqualityResolution {
931 parent: ProofNodeId(99),
932 lit_idx: 0,
933 unifier: None,
934 });
935 assert!(topological_sort(&[input.clone(), derived.clone()]).is_err());
936 derived.witness = None;
937 derived.proof_id = Some(ProofNodeId(99));
938 assert!(topological_sort(&[input.clone(), derived.clone()]).is_err());
939 derived.proof_id = Some(ProofNodeId(20));
940 assert!(topological_sort(&[input.clone(), derived.clone()]).is_ok());
941
942 let mut unary_extra = Clause::new(
944 ClauseId(21),
945 vec![],
946 ClauseSource::Inference {
947 rule: "factoring",
948 parents: vec![ClauseId(10), ClauseId(20)].into(),
949 },
950 );
951 unary_extra.witness = Some(ProofWitness::Factoring {
952 parent: ProofNodeId(10),
953 retained_lit_idx: 0,
954 removed_lit_idx: 1,
955 unifier: None,
956 });
957 assert!(topological_sort(&[input.clone(), derived, unary_extra]).is_err());
958
959 let certificate_parent = dummy_input_clause(11, vec![]);
962 let mut certificate_laundered = Clause::new(
963 ClauseId(22),
964 vec![],
965 ClauseSource::Inference {
966 rule: "resolution",
967 parents: vec![ClauseId(10)].into(),
968 },
969 );
970 certificate_laundered.certificate = Some(ClauseCertificate::AvatarComponent {
971 split_parent: ClauseId(11),
972 branch_index: 0,
973 sat_var: 1,
974 });
975 certificate_laundered.witness = Some(ProofWitness::Resolution {
976 parent_left: ProofNodeId(10),
977 parent_right: ProofNodeId(11),
978 lit_idx_left: 0,
979 lit_idx_right: 0,
980 renaming_offset: 0,
981 unifier: None,
982 });
983 assert!(topological_sort(&[input, certificate_parent, certificate_laundered]).is_err());
984 }
985
986 #[test]
987 fn topological_sort_accepts_ac_nodes_with_cited_axiom_parents() {
988 let ax1 = dummy_input_clause(1, vec![]);
989 let ax2 = dummy_input_clause(2, vec![]);
990 let mut ac_symbols = SymbolTable::new();
991 let f = ac_symbols.intern("f");
992 let x = Term::Var(0);
993 let y = Term::Var(1);
994 let z = Term::Var(2);
995 let ac1 = dummy_eq_clause(
996 3,
997 Term::App(f, vec![x.clone(), y.clone()]),
998 Term::App(f, vec![y.clone(), x.clone()]),
999 );
1000 let ac2 = dummy_eq_clause(
1001 4,
1002 Term::App(f, vec![Term::App(f, vec![x.clone(), y.clone()]), z.clone()]),
1003 Term::App(f, vec![x, Term::App(f, vec![y, z])]),
1004 );
1005
1006 let mut ac_sup = Clause::new(
1008 ClauseId(10),
1009 vec![],
1010 ClauseSource::Inference {
1011 rule: "ac_superposition",
1012 parents: vec![ClauseId(1), ClauseId(2), ClauseId(3), ClauseId(4)].into(),
1013 },
1014 );
1015 ac_sup.witness = Some(ProofWitness::Superposition {
1016 lhs_parent: ProofNodeId(1),
1017 rhs_parent: ProofNodeId(2),
1018 lhs_lit_idx: 0,
1019 rhs_lit_idx: 0,
1020 term_path: vec![],
1021 orientation_left: true,
1022 unifier: None,
1023 });
1024
1025 let mut ac_res = Clause::new(
1027 ClauseId(11),
1028 vec![],
1029 ClauseSource::Inference {
1030 rule: "ac_resolution",
1031 parents: vec![ClauseId(10), ClauseId(2), ClauseId(3)].into(),
1032 },
1033 );
1034 ac_res.witness = Some(ProofWitness::Resolution {
1035 parent_left: ProofNodeId(10),
1036 parent_right: ProofNodeId(2),
1037 lit_idx_left: 0,
1038 lit_idx_right: 0,
1039 renaming_offset: 0,
1040 unifier: None,
1041 });
1042
1043 assert!(
1044 topological_sort(&[
1045 ax1.clone(),
1046 ax2.clone(),
1047 ac1.clone(),
1048 ac2.clone(),
1049 ac_sup.clone(),
1050 ac_res.clone(),
1051 ])
1052 .is_ok()
1053 );
1054
1055 let mut ac_without_axiom = Clause::new(
1058 ClauseId(12),
1059 vec![],
1060 ClauseSource::Inference {
1061 rule: "ac_superposition",
1062 parents: vec![ClauseId(1), ClauseId(2)].into(),
1063 },
1064 );
1065 ac_without_axiom.witness = Some(ProofWitness::Superposition {
1066 lhs_parent: ProofNodeId(1),
1067 rhs_parent: ProofNodeId(2),
1068 lhs_lit_idx: 0,
1069 rhs_lit_idx: 0,
1070 term_path: vec![],
1071 orientation_left: true,
1072 unifier: None,
1073 });
1074 assert!(topological_sort(&[ax1.clone(), ax2.clone(), ac_without_axiom]).is_err());
1075
1076 let mut mismatched_ac_witness = Clause::new(
1079 ClauseId(13),
1080 vec![],
1081 ClauseSource::Inference {
1082 rule: "ac_resolution",
1083 parents: vec![ClauseId(1), ClauseId(2), ClauseId(3)].into(),
1084 },
1085 );
1086 mismatched_ac_witness.witness = Some(ProofWitness::Superposition {
1087 lhs_parent: ProofNodeId(1),
1088 rhs_parent: ProofNodeId(2),
1089 lhs_lit_idx: 0,
1090 rhs_lit_idx: 0,
1091 term_path: vec![],
1092 orientation_left: true,
1093 unifier: None,
1094 });
1095 assert!(
1096 topological_sort(&[ax1.clone(), ax2.clone(), ac1.clone(), mismatched_ac_witness,])
1097 .is_err()
1098 );
1099
1100 let mut reordered_ac_parents = Clause::new(
1103 ClauseId(14),
1104 vec![],
1105 ClauseSource::Inference {
1106 rule: "ac_resolution",
1107 parents: vec![ClauseId(2), ClauseId(1), ClauseId(3)].into(),
1108 },
1109 );
1110 reordered_ac_parents.witness = Some(ProofWitness::Resolution {
1111 parent_left: ProofNodeId(1),
1112 parent_right: ProofNodeId(2),
1113 lit_idx_left: 0,
1114 lit_idx_right: 0,
1115 renaming_offset: 0,
1116 unifier: None,
1117 });
1118 assert!(
1119 topological_sort(&[ax1.clone(), ax2.clone(), ac1.clone(), reordered_ac_parents,])
1120 .is_err()
1121 );
1122
1123 let mut bad_sup = Clause::new(
1125 ClauseId(15),
1126 vec![],
1127 ClauseSource::Inference {
1128 rule: "superposition",
1129 parents: vec![ClauseId(1), ClauseId(2), ClauseId(3)].into(),
1130 },
1131 );
1132 bad_sup.witness = Some(ProofWitness::Superposition {
1133 lhs_parent: ProofNodeId(1),
1134 rhs_parent: ProofNodeId(2),
1135 lhs_lit_idx: 0,
1136 rhs_lit_idx: 0,
1137 term_path: vec![],
1138 orientation_left: true,
1139 unifier: None,
1140 });
1141 assert!(topological_sort(&[ax1.clone(), ax2.clone(), ac1.clone(), bad_sup]).is_err());
1142
1143 let mut missing_parent_ac_sup = Clause::new(
1145 ClauseId(16),
1146 vec![],
1147 ClauseSource::Inference {
1148 rule: "ac_superposition",
1149 parents: vec![ClauseId(1), ClauseId(3), ClauseId(4)].into(),
1150 },
1151 );
1152 missing_parent_ac_sup.witness = Some(ProofWitness::Superposition {
1153 lhs_parent: ProofNodeId(1),
1154 rhs_parent: ProofNodeId(2), lhs_lit_idx: 0,
1156 rhs_lit_idx: 0,
1157 term_path: vec![],
1158 orientation_left: true,
1159 unifier: None,
1160 });
1161 assert!(topological_sort(&[ax1, ax2, ac1, ac2, missing_parent_ac_sup]).is_err());
1162 }
1163
1164 fn dummy_input_clause(id: u64, lits: Vec<Literal>) -> Clause {
1165 Clause::new(
1166 ClauseId(id),
1167 lits,
1168 ClauseSource::Input {
1169 name: format!("ax{id}"),
1170 role: "axiom".into(),
1171 },
1172 )
1173 }
1174
1175 fn dummy_eq_clause(id: u64, l: Term, r: Term) -> Clause {
1176 dummy_input_clause(id, vec![Literal::pos(Atom::eq(l, r))])
1177 }
1178
1179 #[test]
1180 fn test_one_step_demodulation() {
1181 let mut syms = SymbolTable::new();
1182 let f = syms.intern("f");
1183 let a = syms.intern("a");
1184 let b = syms.intern("b");
1185 let c = syms.intern("c");
1186
1187 let c_target = dummy_input_clause(
1189 1,
1190 vec![Literal::pos(Atom::eq(
1191 Term::app(f, vec![Term::constant(a)]),
1192 Term::constant(b),
1193 ))],
1194 );
1195 let c_rule = dummy_eq_clause(2, Term::constant(a), Term::constant(c));
1197 let c_concl = Clause::new(
1199 ClauseId(3),
1200 vec![Literal::pos(Atom::eq(
1201 Term::app(f, vec![Term::constant(c)]),
1202 Term::constant(b),
1203 ))],
1204 ClauseSource::Inference {
1205 rule: "demodulation",
1206 parents: vec![ClauseId(1), ClauseId(2)].into(),
1207 },
1208 );
1209
1210 let proof = vec![c_target, c_rule, c_concl];
1211 let elaborated = elaborate(&proof, &syms).expect("elaboration should succeed");
1212 assert_eq!(elaborated.clauses.len(), 3);
1213 let last = &elaborated.clauses[2];
1214 if let ClauseSource::Inference { rule, parents } = &last.source {
1215 assert_eq!(*rule, "demodulation");
1216 assert_eq!(parents.len(), 2);
1217 assert_eq!(parents[0], ClauseId(1));
1218 assert_eq!(parents[1], ClauseId(2));
1219 } else {
1220 panic!("expected inference");
1221 }
1222 }
1223
1224 #[test]
1225 fn test_multi_step_demodulation() {
1226 let mut syms = SymbolTable::new();
1227 let f = syms.intern("f");
1228 let a = syms.intern("a");
1229 let b = syms.intern("b");
1230 let c = syms.intern("c");
1231 let d = syms.intern("d");
1232 let e = syms.intern("e");
1233
1234 let c_target = dummy_input_clause(
1236 1,
1237 vec![Literal::pos(Atom::eq(
1238 Term::app(f, vec![Term::constant(a), Term::constant(b)]),
1239 Term::constant(c),
1240 ))],
1241 );
1242 let c_rule1 = dummy_eq_clause(2, Term::constant(a), Term::constant(d));
1244 let c_rule2 = dummy_eq_clause(3, Term::constant(b), Term::constant(e));
1246 let c_concl = Clause::new(
1248 ClauseId(4),
1249 vec![Literal::pos(Atom::eq(
1250 Term::app(f, vec![Term::constant(d), Term::constant(e)]),
1251 Term::constant(c),
1252 ))],
1253 ClauseSource::Inference {
1254 rule: "demodulation",
1255 parents: vec![ClauseId(1), ClauseId(2), ClauseId(3)].into(),
1256 },
1257 );
1258
1259 let proof = vec![c_target, c_rule1, c_rule2, c_concl];
1260 let elaborated = elaborate(&proof, &syms).expect("elaboration should succeed");
1261
1262 assert_eq!(elaborated.clauses.len(), 5);
1264
1265 let mut seen = HashSet::new();
1267 for clause in &elaborated.clauses {
1268 if let ClauseSource::Inference { parents, .. } = &clause.source {
1269 for p in parents {
1270 assert!(
1271 seen.contains(p),
1272 "parent c{} must precede child c{}",
1273 p.0,
1274 clause.id.0
1275 );
1276 }
1277 assert_eq!(
1278 parents.len(),
1279 2,
1280 "every elaborated demodulation must have exactly 2 parents"
1281 );
1282 }
1283 seen.insert(clause.id);
1284 }
1285 }
1286
1287 #[test]
1288 fn test_rewrite_position_mutation() {
1289 let mut syms = SymbolTable::new();
1290 let f = syms.intern("f");
1291 let a = syms.intern("a");
1292 let b = syms.intern("b");
1293 let c = syms.intern("c");
1294
1295 let clause = dummy_input_clause(
1296 1,
1297 vec![Literal::pos(Atom::eq(
1298 Term::app(f, vec![Term::constant(a)]),
1299 Term::constant(b),
1300 ))],
1301 );
1302
1303 let bad_pos = RewritePosition {
1305 lit_idx: 5,
1306 atom_side: AtomSide::EqLeft,
1307 term_path: vec![0],
1308 };
1309 assert!(replace_subterm_in_clause(&clause, &bad_pos, Term::constant(c)).is_none());
1310 }
1311
1312 #[test]
1313 fn test_rewrite_order_mutation() {
1314 let c1 = dummy_input_clause(1, vec![]);
1316 let c2 = Clause::new(
1317 ClauseId(2),
1318 vec![],
1319 ClauseSource::Inference {
1320 rule: "resolve",
1321 parents: vec![ClauseId(1)].into(),
1322 },
1323 );
1324
1325 let proof = vec![c2.clone(), c1.clone()];
1327 let sorted = topological_sort(&proof).expect("topological sort should reorder correctly");
1328 assert_eq!(sorted[0].id, ClauseId(1));
1329 assert_eq!(sorted[1].id, ClauseId(2));
1330 }
1331
1332 #[test]
1333 fn test_cyclic_rewrite_rules() {
1334 let mut syms = SymbolTable::new();
1335 let a = syms.intern("a");
1336 let b = syms.intern("b");
1337
1338 let c_target = dummy_eq_clause(1, Term::constant(a), Term::constant(a));
1340 let c_rule = dummy_eq_clause(2, Term::constant(a), Term::constant(b));
1342 let c_concl = Clause::new(
1344 ClauseId(3),
1345 vec![Literal::pos(Atom::eq(Term::constant(b), Term::constant(b)))],
1346 ClauseSource::Inference {
1347 rule: "demodulation",
1348 parents: vec![ClauseId(1), ClauseId(2)].into(),
1349 },
1350 );
1351
1352 let mut next_id = 10;
1353 let res = reconstruct_demodulation(&c_target, &c_concl, &[c_rule], &mut next_id);
1354 assert!(res.is_err() || res.is_ok());
1355 }
1356
1357 #[test]
1358 fn test_ambiguous_rewrite_failure() {
1359 let mut syms = SymbolTable::new();
1360 let f = syms.intern("f");
1361 let a = syms.intern("a");
1362 let b = syms.intern("b");
1363 let c = syms.intern("c");
1364
1365 let c_target = dummy_input_clause(
1367 1,
1368 vec![Literal::pos(Atom::eq(
1369 Term::app(f, vec![Term::constant(a), Term::constant(a)]),
1370 Term::constant(c),
1371 ))],
1372 );
1373 let c_rule1 = dummy_eq_clause(2, Term::constant(a), Term::constant(b));
1375 let c_rule2 = dummy_eq_clause(3, Term::constant(a), Term::constant(b));
1376
1377 let c_concl = Clause::new(
1378 ClauseId(4),
1379 vec![Literal::pos(Atom::eq(
1380 Term::app(f, vec![Term::constant(b), Term::constant(a)]),
1381 Term::constant(c),
1382 ))],
1383 ClauseSource::Inference {
1384 rule: "demodulation",
1385 parents: vec![ClauseId(1), ClauseId(2), ClauseId(3)].into(),
1386 },
1387 );
1388
1389 let mut next_id = 10;
1390 let res = reconstruct_demodulation(&c_target, &c_concl, &[c_rule1, c_rule2], &mut next_id);
1391 assert!(matches!(res, Err(ElaborationError::Ambiguous(_))));
1392 }
1393
1394 #[test]
1395 fn test_nested_definition_dependency() {
1396 let mut syms = SymbolTable::new();
1397 let d1 = syms.intern("def_d1");
1398 let d2 = syms.intern("def_d2");
1399
1400 let c_def1 = Clause::new(
1401 ClauseId(2),
1402 vec![],
1403 ClauseSource::Introduced {
1404 symbol: d1,
1405 parents: smallvec::SmallVec::new(),
1406 },
1407 );
1408 let c_def2 = Clause::new(
1409 ClauseId(3),
1410 vec![],
1411 ClauseSource::Introduced {
1412 symbol: d2,
1413 parents: vec![ClauseId(2)].into(),
1414 },
1415 );
1416
1417 let proof = vec![c_def2, c_def1];
1418 let elaborated = elaborate(&proof, &syms).expect("definitions elaborate cleanly");
1419 assert_eq!(elaborated.clauses.len(), 2);
1420 assert_eq!(elaborated.clauses[0].id, ClauseId(2));
1421 assert_eq!(elaborated.clauses[1].id, ClauseId(3));
1422 }
1423
1424 #[test]
1425 fn test_stable_topological_export() {
1426 let c1 = dummy_input_clause(1, vec![]);
1427 let c2 = dummy_input_clause(2, vec![]);
1428 let c3 = Clause::new(
1429 ClauseId(3),
1430 vec![],
1431 ClauseSource::Inference {
1432 rule: "resolve",
1433 parents: vec![ClauseId(1), ClauseId(2)].into(),
1434 },
1435 );
1436
1437 let proof1 = vec![c1.clone(), c2.clone(), c3.clone()];
1438 let proof2 = vec![c2.clone(), c1.clone(), c3.clone()];
1439
1440 let res1 = topological_sort(&proof1).unwrap();
1441 let res2 = topological_sort(&proof2).unwrap();
1442
1443 assert_eq!(res1[2].id, ClauseId(3));
1444 assert_eq!(res2[2].id, ClauseId(3));
1445 }
1446
1447 #[test]
1448 fn missing_parent_is_not_silently_dropped() {
1449 let child = Clause::new(
1450 ClauseId(2),
1451 vec![],
1452 ClauseSource::Inference {
1453 rule: "resolution",
1454 parents: vec![ClauseId(99)].into(),
1455 },
1456 );
1457 assert!(matches!(
1458 topological_sort(&[child]),
1459 Err(ElaborationError::Inconclusive(reason)) if reason.contains("missing parent")
1460 ));
1461 }
1462
1463 #[test]
1464 fn test_existing_golden_strict_certified_proofs() {
1465 let mut syms = SymbolTable::new();
1466 let f = syms.intern("f");
1467 let a = syms.intern("a");
1468 let b = syms.intern("b");
1469 let p = syms.intern("p");
1470
1471 let ax1 = dummy_eq_clause(1, Term::app(f, vec![Term::constant(a)]), Term::constant(b));
1473 let ax2 = dummy_input_clause(
1475 2,
1476 vec![Literal::pos(Atom::Pred(
1477 p,
1478 vec![Term::app(f, vec![Term::constant(a)])],
1479 ))],
1480 );
1481 let step1 = Clause::new(
1483 ClauseId(3),
1484 vec![Literal::pos(Atom::Pred(p, vec![Term::constant(b)]))],
1485 ClauseSource::Inference {
1486 rule: "demodulation",
1487 parents: vec![ClauseId(2), ClauseId(1)].into(),
1488 },
1489 );
1490 let ax3 = dummy_input_clause(
1492 4,
1493 vec![Literal::neg(Atom::Pred(p, vec![Term::constant(b)]))],
1494 );
1495 let step2 = Clause::new(
1497 ClauseId(5),
1498 vec![],
1499 ClauseSource::Inference {
1500 rule: "resolve",
1501 parents: vec![ClauseId(3), ClauseId(4)].into(),
1502 },
1503 );
1504
1505 let proof = vec![ax1, ax2, step1, ax3, step2];
1506 let elaborated = elaborate(&proof, &syms).expect("elaboration should succeed");
1507 assert_eq!(elaborated.clauses.len(), 5);
1508
1509 let tstp = crate::tstp::format_tstp(&elaborated.clauses, &syms);
1510 assert!(tstp.contains("demodulation"));
1511 assert!(tstp.contains("resolve"));
1512 assert!(tstp.contains("$false"));
1513 }
1514}