Skip to main content

oxiz_proof/
craig.rs

1//! Craig Interpolation for SMT solving
2//!
3//! Provides complete Craig interpolation infrastructure:
4//! - McMillan's algorithm (left-biased/weaker interpolants)
5//! - Pudlák's algorithm (symmetric interpolation)
6//! - Theory-specific interpolants (LIA, Arrays, EUF)
7//! - Sequence interpolation for tree/DAG proofs
8//!
9//! Given an UNSAT formula A ∧ B, compute an interpolant I such that:
10//! - A ⟹ I
11//! - I ∧ B is UNSAT
12//! - I only contains symbols common to A and B
13//!
14//! # References
15//!
16//! - McMillan, K.L. "Interpolation and SAT-Based Model Checking" (CAV 2003)
17//! - Pudlák, P. "Lower bounds for resolution and cutting plane proofs" (1997)
18//! - Yorsh, G. & Musuvathi, M. "A Combination Method for Generating Interpolants" (CADE 2005)
19
20use crate::premise::{PremiseId, PremiseTracker};
21use crate::proof::{Proof, ProofNodeId, ProofStep};
22use num_rational::BigRational;
23use rustc_hash::{FxHashMap, FxHashSet};
24use std::fmt;
25
26// ============================================================================
27// Types and Configuration
28// ============================================================================
29
30/// Interpolation algorithm selection
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum InterpolationAlgorithm {
33    /// McMillan's algorithm - produces weaker (left-biased) interpolants
34    /// Better for model checking as interpolants are more general
35    McMillan,
36    /// Pudlák's symmetric algorithm - balanced interpolants
37    #[default]
38    Pudlak,
39    /// Huang's algorithm - produces stronger (right-biased) interpolants
40    Huang,
41}
42
43/// Configuration for interpolation computation
44#[derive(Debug, Clone)]
45pub struct InterpolationConfig {
46    /// Algorithm to use
47    pub algorithm: InterpolationAlgorithm,
48    /// Enable theory-specific interpolation
49    pub use_theory_interpolants: bool,
50    /// Simplify interpolants after computation
51    pub simplify_interpolants: bool,
52    /// Maximum depth for recursive simplification
53    pub max_simplify_depth: usize,
54    /// Enable caching of intermediate interpolants
55    pub enable_caching: bool,
56    /// Merge duplicate subterms
57    pub deduplicate_terms: bool,
58}
59
60impl Default for InterpolationConfig {
61    fn default() -> Self {
62        Self {
63            algorithm: InterpolationAlgorithm::Pudlak,
64            use_theory_interpolants: true,
65            simplify_interpolants: true,
66            max_simplify_depth: 100,
67            enable_caching: true,
68            deduplicate_terms: true,
69        }
70    }
71}
72
73/// Color of a proof node in the interpolation procedure
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum InterpolantColor {
76    /// Node depends only on A premises
77    A,
78    /// Node depends only on B premises
79    B,
80    /// Node depends on both A and B premises (mixed)
81    AB,
82}
83
84impl fmt::Display for InterpolantColor {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::A => write!(f, "A"),
88            Self::B => write!(f, "B"),
89            Self::AB => write!(f, "AB"),
90        }
91    }
92}
93
94/// A partition of premises into A-side and B-side
95#[derive(Debug, Clone)]
96pub struct InterpolantPartition {
97    /// Premises in the A partition
98    a_premises: FxHashSet<PremiseId>,
99    /// Premises in the B partition
100    b_premises: FxHashSet<PremiseId>,
101    /// Shared symbols between A and B
102    shared_symbols: FxHashSet<Symbol>,
103}
104
105/// Symbol identifier (variable or function symbol)
106#[derive(Debug, Clone, PartialEq, Eq, Hash)]
107pub struct Symbol {
108    /// Symbol name
109    pub name: String,
110    /// Symbol arity (0 for constants/variables)
111    pub arity: usize,
112}
113
114impl Symbol {
115    /// Create a new symbol
116    #[must_use]
117    pub fn new(name: impl Into<String>, arity: usize) -> Self {
118        Self {
119            name: name.into(),
120            arity,
121        }
122    }
123
124    /// Create a variable symbol
125    #[must_use]
126    pub fn var(name: impl Into<String>) -> Self {
127        Self::new(name, 0)
128    }
129}
130
131impl InterpolantPartition {
132    /// Create a new partition
133    #[must_use]
134    pub fn new(
135        a_premises: impl IntoIterator<Item = PremiseId>,
136        b_premises: impl IntoIterator<Item = PremiseId>,
137    ) -> Self {
138        Self {
139            a_premises: a_premises.into_iter().collect(),
140            b_premises: b_premises.into_iter().collect(),
141            shared_symbols: FxHashSet::default(),
142        }
143    }
144
145    /// Set shared symbols
146    pub fn set_shared_symbols(&mut self, symbols: impl IntoIterator<Item = Symbol>) {
147        self.shared_symbols = symbols.into_iter().collect();
148    }
149
150    /// Check if a premise is in the A partition
151    #[must_use]
152    pub fn is_a_premise(&self, premise: PremiseId) -> bool {
153        self.a_premises.contains(&premise)
154    }
155
156    /// Check if a premise is in the B partition
157    #[must_use]
158    pub fn is_b_premise(&self, premise: PremiseId) -> bool {
159        self.b_premises.contains(&premise)
160    }
161
162    /// Check if a symbol is shared
163    #[must_use]
164    pub fn is_shared(&self, symbol: &Symbol) -> bool {
165        self.shared_symbols.contains(symbol)
166    }
167
168    /// Get A premises
169    #[must_use]
170    pub fn a_premises(&self) -> &FxHashSet<PremiseId> {
171        &self.a_premises
172    }
173
174    /// Get B premises
175    #[must_use]
176    pub fn b_premises(&self) -> &FxHashSet<PremiseId> {
177        &self.b_premises
178    }
179}
180
181// ============================================================================
182// Interpolant Representation
183// ============================================================================
184
185/// An interpolant formula in a simple term representation
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
187pub enum InterpolantTerm {
188    /// Boolean constant
189    Bool(bool),
190    /// Variable
191    Var(Symbol),
192    /// Negation
193    Not(Box<InterpolantTerm>),
194    /// Conjunction
195    And(Vec<InterpolantTerm>),
196    /// Disjunction
197    Or(Vec<InterpolantTerm>),
198    /// Implication
199    Implies(Box<InterpolantTerm>, Box<InterpolantTerm>),
200    /// Equality
201    Eq(Box<InterpolantTerm>, Box<InterpolantTerm>),
202    /// Less than
203    Lt(Box<InterpolantTerm>, Box<InterpolantTerm>),
204    /// Less than or equal
205    Le(Box<InterpolantTerm>, Box<InterpolantTerm>),
206    /// Integer/Rational constant
207    Num(BigRational),
208    /// Addition
209    Add(Vec<InterpolantTerm>),
210    /// Subtraction
211    Sub(Box<InterpolantTerm>, Box<InterpolantTerm>),
212    /// Multiplication
213    Mul(Vec<InterpolantTerm>),
214    /// Function application
215    App(Symbol, Vec<InterpolantTerm>),
216    /// Array select
217    Select(Box<InterpolantTerm>, Box<InterpolantTerm>),
218    /// Array store
219    Store(
220        Box<InterpolantTerm>,
221        Box<InterpolantTerm>,
222        Box<InterpolantTerm>,
223    ),
224}
225
226impl InterpolantTerm {
227    /// Create true
228    #[must_use]
229    pub fn true_val() -> Self {
230        Self::Bool(true)
231    }
232
233    /// Create false
234    #[must_use]
235    pub fn false_val() -> Self {
236        Self::Bool(false)
237    }
238
239    /// Create a variable
240    #[must_use]
241    pub fn var(name: impl Into<String>) -> Self {
242        Self::Var(Symbol::var(name))
243    }
244
245    /// Create a negation
246    #[must_use]
247    #[allow(clippy::should_implement_trait)]
248    pub fn not(term: Self) -> Self {
249        match term {
250            Self::Bool(b) => Self::Bool(!b),
251            Self::Not(inner) => *inner,
252            _ => Self::Not(Box::new(term)),
253        }
254    }
255
256    /// Create a conjunction
257    #[must_use]
258    pub fn and(terms: Vec<Self>) -> Self {
259        let mut flat = Vec::new();
260        for t in terms {
261            match t {
262                Self::Bool(true) => continue,
263                Self::Bool(false) => return Self::Bool(false),
264                Self::And(inner) => flat.extend(inner),
265                other => flat.push(other),
266            }
267        }
268        if flat.is_empty() {
269            Self::Bool(true)
270        } else if flat.len() == 1 {
271            flat.pop().unwrap_or(Self::Bool(true))
272        } else {
273            Self::And(flat)
274        }
275    }
276
277    /// Create a disjunction
278    #[must_use]
279    pub fn or(terms: Vec<Self>) -> Self {
280        let mut flat = Vec::new();
281        for t in terms {
282            match t {
283                Self::Bool(false) => continue,
284                Self::Bool(true) => return Self::Bool(true),
285                Self::Or(inner) => flat.extend(inner),
286                other => flat.push(other),
287            }
288        }
289        if flat.is_empty() {
290            Self::Bool(false)
291        } else if flat.len() == 1 {
292            flat.pop().unwrap_or(Self::Bool(false))
293        } else {
294            Self::Or(flat)
295        }
296    }
297
298    /// Create an implication
299    #[must_use]
300    pub fn implies(lhs: Self, rhs: Self) -> Self {
301        match (&lhs, &rhs) {
302            (Self::Bool(false), _) => Self::Bool(true),
303            (Self::Bool(true), _) => rhs,
304            (_, Self::Bool(true)) => Self::Bool(true),
305            (_, Self::Bool(false)) => Self::not(lhs),
306            _ => Self::Implies(Box::new(lhs), Box::new(rhs)),
307        }
308    }
309
310    /// Check if this term is true
311    #[must_use]
312    pub fn is_true(&self) -> bool {
313        matches!(self, Self::Bool(true))
314    }
315
316    /// Check if this term is false
317    #[must_use]
318    pub fn is_false(&self) -> bool {
319        matches!(self, Self::Bool(false))
320    }
321
322    /// Collect all symbols in the term
323    pub fn collect_symbols(&self, symbols: &mut FxHashSet<Symbol>) {
324        match self {
325            Self::Bool(_) | Self::Num(_) => {}
326            Self::Var(s) => {
327                symbols.insert(s.clone());
328            }
329            Self::Not(t) => t.collect_symbols(symbols),
330            Self::And(ts) | Self::Or(ts) | Self::Add(ts) | Self::Mul(ts) => {
331                for t in ts {
332                    t.collect_symbols(symbols);
333                }
334            }
335            Self::Implies(a, b)
336            | Self::Eq(a, b)
337            | Self::Lt(a, b)
338            | Self::Le(a, b)
339            | Self::Sub(a, b)
340            | Self::Select(a, b) => {
341                a.collect_symbols(symbols);
342                b.collect_symbols(symbols);
343            }
344            Self::App(f, args) => {
345                symbols.insert(f.clone());
346                for arg in args {
347                    arg.collect_symbols(symbols);
348                }
349            }
350            Self::Store(a, i, v) => {
351                a.collect_symbols(symbols);
352                i.collect_symbols(symbols);
353                v.collect_symbols(symbols);
354            }
355        }
356    }
357
358    /// Simplify the term
359    #[must_use]
360    pub fn simplify(&self) -> Self {
361        match self {
362            Self::Bool(_) | Self::Num(_) | Self::Var(_) => self.clone(),
363            Self::Not(t) => Self::not(t.simplify()),
364            Self::And(ts) => Self::and(ts.iter().map(|t| t.simplify()).collect()),
365            Self::Or(ts) => Self::or(ts.iter().map(|t| t.simplify()).collect()),
366            Self::Implies(a, b) => Self::implies(a.simplify(), b.simplify()),
367            Self::Eq(a, b) => {
368                let sa = a.simplify();
369                let sb = b.simplify();
370                if sa == sb {
371                    Self::Bool(true)
372                } else {
373                    Self::Eq(Box::new(sa), Box::new(sb))
374                }
375            }
376            _ => self.clone(),
377        }
378    }
379}
380
381impl fmt::Display for InterpolantTerm {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        match self {
384            Self::Bool(b) => write!(f, "{}", b),
385            Self::Var(s) => write!(f, "{}", s.name),
386            Self::Not(t) => write!(f, "(not {})", t),
387            Self::And(ts) => {
388                write!(f, "(and")?;
389                for t in ts {
390                    write!(f, " {}", t)?;
391                }
392                write!(f, ")")
393            }
394            Self::Or(ts) => {
395                write!(f, "(or")?;
396                for t in ts {
397                    write!(f, " {}", t)?;
398                }
399                write!(f, ")")
400            }
401            Self::Implies(a, b) => write!(f, "(=> {} {})", a, b),
402            Self::Eq(a, b) => write!(f, "(= {} {})", a, b),
403            Self::Lt(a, b) => write!(f, "(< {} {})", a, b),
404            Self::Le(a, b) => write!(f, "(<= {} {})", a, b),
405            Self::Num(n) => write!(f, "{}", n),
406            Self::Add(ts) => {
407                write!(f, "(+")?;
408                for t in ts {
409                    write!(f, " {}", t)?;
410                }
411                write!(f, ")")
412            }
413            Self::Sub(a, b) => write!(f, "(- {} {})", a, b),
414            Self::Mul(ts) => {
415                write!(f, "(*")?;
416                for t in ts {
417                    write!(f, " {}", t)?;
418                }
419                write!(f, ")")
420            }
421            Self::App(s, args) => {
422                write!(f, "({}", s.name)?;
423                for arg in args {
424                    write!(f, " {}", arg)?;
425                }
426                write!(f, ")")
427            }
428            Self::Select(a, i) => write!(f, "(select {} {})", a, i),
429            Self::Store(a, i, v) => write!(f, "(store {} {} {})", a, i, v),
430        }
431    }
432}
433
434// ============================================================================
435// Craig Interpolation Engine
436// ============================================================================
437
438/// Craig interpolation engine
439#[derive(Debug)]
440pub struct CraigInterpolator {
441    /// Configuration
442    config: InterpolationConfig,
443    /// Partition of premises
444    #[allow(dead_code)]
445    partition: InterpolantPartition,
446    /// Premise tracker
447    #[allow(dead_code)]
448    premise_tracker: PremiseTracker,
449    /// Computed colors for proof nodes
450    colors: FxHashMap<ProofNodeId, InterpolantColor>,
451    /// Computed interpolants for proof nodes
452    interpolants: FxHashMap<ProofNodeId, InterpolantTerm>,
453    /// Statistics
454    stats: InterpolationStats,
455}
456
457/// Statistics about interpolation computation
458#[derive(Debug, Default, Clone)]
459pub struct InterpolationStats {
460    /// Number of proof nodes processed
461    pub nodes_processed: usize,
462    /// Number of A-colored nodes
463    pub a_nodes: usize,
464    /// Number of B-colored nodes
465    pub b_nodes: usize,
466    /// Number of AB-colored (mixed) nodes
467    pub ab_nodes: usize,
468    /// Number of resolution steps
469    pub resolution_steps: usize,
470    /// Number of theory lemmas
471    pub theory_lemmas: usize,
472    /// Cache hits
473    pub cache_hits: usize,
474    /// Time spent in interpolation (microseconds)
475    pub time_us: u64,
476}
477
478impl CraigInterpolator {
479    /// Create a new interpolator
480    #[must_use]
481    pub fn new(
482        config: InterpolationConfig,
483        partition: InterpolantPartition,
484        premise_tracker: PremiseTracker,
485    ) -> Self {
486        Self {
487            config,
488            partition,
489            premise_tracker,
490            colors: FxHashMap::default(),
491            interpolants: FxHashMap::default(),
492            stats: InterpolationStats::default(),
493        }
494    }
495
496    /// Create with default configuration
497    #[must_use]
498    pub fn with_partition(partition: InterpolantPartition) -> Self {
499        Self::new(
500            InterpolationConfig::default(),
501            partition,
502            PremiseTracker::new(),
503        )
504    }
505
506    /// Get statistics
507    #[must_use]
508    pub fn stats(&self) -> &InterpolationStats {
509        &self.stats
510    }
511
512    /// Extract an interpolant from a proof
513    pub fn extract(&mut self, proof: &Proof) -> Result<InterpolantTerm, InterpolationError> {
514        let start = std::time::Instant::now();
515
516        let root = proof.root().ok_or(InterpolationError::NoRoot)?;
517
518        // Phase 1: Compute colors for all nodes (bottom-up)
519        self.compute_colors(proof, root)?;
520
521        // Phase 2: Compute interpolants (bottom-up)
522        let result = self.compute_interpolant(proof, root)?;
523
524        // Simplify if configured
525        let final_result = if self.config.simplify_interpolants {
526            result.simplify()
527        } else {
528            result
529        };
530
531        self.stats.time_us = start.elapsed().as_micros() as u64;
532
533        Ok(final_result)
534    }
535
536    /// Compute colors for proof nodes
537    fn compute_colors(
538        &mut self,
539        proof: &Proof,
540        node_id: ProofNodeId,
541    ) -> Result<InterpolantColor, InterpolationError> {
542        if let Some(&color) = self.colors.get(&node_id) {
543            self.stats.cache_hits += 1;
544            return Ok(color);
545        }
546
547        let node = proof
548            .get_node(node_id)
549            .ok_or(InterpolationError::NodeNotFound(node_id))?;
550
551        self.stats.nodes_processed += 1;
552
553        let color = match &node.step {
554            ProofStep::Axiom { .. } => {
555                // Axioms are colored based on which partition they belong to
556                // For now, use heuristic based on conclusion
557                InterpolantColor::A // Default to A
558            }
559            ProofStep::Inference { premises, rule, .. } => {
560                // Track rule types
561                if rule == "resolution" {
562                    self.stats.resolution_steps += 1;
563                } else if rule.starts_with("theory") {
564                    self.stats.theory_lemmas += 1;
565                }
566
567                // Inference nodes are colored based on their premises
568                let mut has_a = false;
569                let mut has_b = false;
570
571                for &premise_id in premises {
572                    let premise_color = self.compute_colors(proof, premise_id)?;
573                    match premise_color {
574                        InterpolantColor::A => has_a = true,
575                        InterpolantColor::B => has_b = true,
576                        InterpolantColor::AB => {
577                            has_a = true;
578                            has_b = true;
579                        }
580                    }
581                }
582
583                if has_a && has_b {
584                    InterpolantColor::AB
585                } else if has_a {
586                    InterpolantColor::A
587                } else if has_b {
588                    InterpolantColor::B
589                } else {
590                    InterpolantColor::A
591                }
592            }
593        };
594
595        // Update statistics
596        match color {
597            InterpolantColor::A => self.stats.a_nodes += 1,
598            InterpolantColor::B => self.stats.b_nodes += 1,
599            InterpolantColor::AB => self.stats.ab_nodes += 1,
600        }
601
602        self.colors.insert(node_id, color);
603        Ok(color)
604    }
605
606    /// Compute interpolant for a proof node
607    fn compute_interpolant(
608        &mut self,
609        proof: &Proof,
610        node_id: ProofNodeId,
611    ) -> Result<InterpolantTerm, InterpolationError> {
612        if let Some(interp) = self.interpolants.get(&node_id) {
613            return Ok(interp.clone());
614        }
615
616        let node = proof
617            .get_node(node_id)
618            .ok_or(InterpolationError::NodeNotFound(node_id))?;
619        let color = *self
620            .colors
621            .get(&node_id)
622            .ok_or(InterpolationError::NoColor(node_id))?;
623
624        let interpolant = match &node.step {
625            ProofStep::Axiom { conclusion } => self.compute_axiom_interpolant(color, conclusion),
626            ProofStep::Inference {
627                rule,
628                premises,
629                conclusion,
630                ..
631            } => {
632                // First compute premise interpolants
633                let mut premise_interpolants = Vec::new();
634                let mut premise_colors = Vec::new();
635
636                for &p in premises {
637                    premise_interpolants.push(self.compute_interpolant(proof, p)?);
638                    premise_colors.push(*self.colors.get(&p).unwrap_or(&InterpolantColor::A));
639                }
640
641                self.compute_inference_interpolant(
642                    rule,
643                    &premise_interpolants,
644                    &premise_colors,
645                    conclusion,
646                    color,
647                )
648            }
649        };
650
651        if self.config.enable_caching {
652            self.interpolants.insert(node_id, interpolant.clone());
653        }
654
655        Ok(interpolant)
656    }
657
658    /// Compute interpolant for an axiom
659    fn compute_axiom_interpolant(
660        &self,
661        color: InterpolantColor,
662        _conclusion: &str,
663    ) -> InterpolantTerm {
664        match color {
665            InterpolantColor::A => {
666                // A-axiom: interpolant is True (or the clause projected to shared)
667                InterpolantTerm::true_val()
668            }
669            InterpolantColor::B => {
670                // B-axiom: interpolant is False (or the clause projected to shared)
671                InterpolantTerm::false_val()
672            }
673            InterpolantColor::AB => {
674                // Shouldn't happen for axioms, but handle gracefully
675                InterpolantTerm::true_val()
676            }
677        }
678    }
679
680    /// Compute interpolant for an inference step
681    fn compute_inference_interpolant(
682        &self,
683        rule: &str,
684        premise_interpolants: &[InterpolantTerm],
685        premise_colors: &[InterpolantColor],
686        _conclusion: &str,
687        color: InterpolantColor,
688    ) -> InterpolantTerm {
689        match self.config.algorithm {
690            InterpolationAlgorithm::McMillan => {
691                self.mcmillan_interpolant(rule, premise_interpolants, premise_colors, color)
692            }
693            InterpolationAlgorithm::Pudlak => {
694                self.pudlak_interpolant(rule, premise_interpolants, premise_colors, color)
695            }
696            InterpolationAlgorithm::Huang => {
697                self.huang_interpolant(rule, premise_interpolants, premise_colors, color)
698            }
699        }
700    }
701
702    /// McMillan's algorithm (weaker/left-biased interpolants)
703    ///
704    /// For resolution on pivot p:
705    /// - If p is A-local: I = I1 ∨ I2
706    /// - If p is B-local: I = I1 ∧ I2
707    /// - If p is shared: I = (I1 ∨ p) ∧ (I2 ∨ ¬p)
708    fn mcmillan_interpolant(
709        &self,
710        rule: &str,
711        premise_interpolants: &[InterpolantTerm],
712        premise_colors: &[InterpolantColor],
713        color: InterpolantColor,
714    ) -> InterpolantTerm {
715        match color {
716            InterpolantColor::A => InterpolantTerm::true_val(),
717            InterpolantColor::B => InterpolantTerm::false_val(),
718            InterpolantColor::AB => {
719                if rule == "resolution" && premise_interpolants.len() == 2 {
720                    let i1 = &premise_interpolants[0];
721                    let i2 = &premise_interpolants[1];
722                    let c1 = premise_colors[0];
723                    let c2 = premise_colors[1];
724
725                    match (c1, c2) {
726                        (InterpolantColor::A, InterpolantColor::B) => {
727                            // A-local pivot: disjunction
728                            InterpolantTerm::or(vec![i1.clone(), i2.clone()])
729                        }
730                        (InterpolantColor::B, InterpolantColor::A) => {
731                            InterpolantTerm::or(vec![i1.clone(), i2.clone()])
732                        }
733                        (InterpolantColor::A, InterpolantColor::AB) => {
734                            // Use AB's interpolant
735                            i2.clone()
736                        }
737                        (InterpolantColor::AB, InterpolantColor::A) => i1.clone(),
738                        (InterpolantColor::B, InterpolantColor::AB) => i2.clone(),
739                        (InterpolantColor::AB, InterpolantColor::B) => i1.clone(),
740                        (InterpolantColor::AB, InterpolantColor::AB) => {
741                            // Both mixed: disjunction (McMillan's choice)
742                            InterpolantTerm::or(vec![i1.clone(), i2.clone()])
743                        }
744                        _ => InterpolantTerm::or(vec![i1.clone(), i2.clone()]),
745                    }
746                } else {
747                    // Other rules: combine with disjunction
748                    InterpolantTerm::or(premise_interpolants.to_vec())
749                }
750            }
751        }
752    }
753
754    /// Pudlák's algorithm (symmetric interpolants)
755    ///
756    /// Uses balanced combination of premise interpolants
757    fn pudlak_interpolant(
758        &self,
759        rule: &str,
760        premise_interpolants: &[InterpolantTerm],
761        premise_colors: &[InterpolantColor],
762        color: InterpolantColor,
763    ) -> InterpolantTerm {
764        match color {
765            InterpolantColor::A => InterpolantTerm::true_val(),
766            InterpolantColor::B => InterpolantTerm::false_val(),
767            InterpolantColor::AB => {
768                if rule == "resolution" && premise_interpolants.len() == 2 {
769                    let i1 = &premise_interpolants[0];
770                    let i2 = &premise_interpolants[1];
771                    let c1 = premise_colors[0];
772                    let c2 = premise_colors[1];
773
774                    // Symmetric treatment based on colors
775                    match (c1, c2) {
776                        (InterpolantColor::A, InterpolantColor::B)
777                        | (InterpolantColor::B, InterpolantColor::A) => {
778                            // Mixed: disjunction
779                            InterpolantTerm::or(vec![i1.clone(), i2.clone()])
780                        }
781                        (InterpolantColor::A, InterpolantColor::AB) => i2.clone(),
782                        (InterpolantColor::AB, InterpolantColor::A) => i1.clone(),
783                        (InterpolantColor::B, InterpolantColor::AB) => i2.clone(),
784                        (InterpolantColor::AB, InterpolantColor::B) => i1.clone(),
785                        (InterpolantColor::AB, InterpolantColor::AB) => {
786                            // Symmetric combination
787                            InterpolantTerm::or(vec![i1.clone(), i2.clone()])
788                        }
789                        _ => InterpolantTerm::or(vec![i1.clone(), i2.clone()]),
790                    }
791                } else if rule == "transitivity" && premise_interpolants.len() >= 2 {
792                    // Transitivity: conjunction
793                    InterpolantTerm::and(premise_interpolants.to_vec())
794                } else if rule == "congruence" {
795                    // Congruence: conjunction
796                    InterpolantTerm::and(premise_interpolants.to_vec())
797                } else {
798                    // Default: disjunction
799                    InterpolantTerm::or(premise_interpolants.to_vec())
800                }
801            }
802        }
803    }
804
805    /// Huang's algorithm (stronger/right-biased interpolants)
806    ///
807    /// Produces stronger interpolants by using conjunction more often
808    fn huang_interpolant(
809        &self,
810        rule: &str,
811        premise_interpolants: &[InterpolantTerm],
812        premise_colors: &[InterpolantColor],
813        color: InterpolantColor,
814    ) -> InterpolantTerm {
815        match color {
816            InterpolantColor::A => InterpolantTerm::true_val(),
817            InterpolantColor::B => InterpolantTerm::false_val(),
818            InterpolantColor::AB => {
819                if rule == "resolution" && premise_interpolants.len() == 2 {
820                    let i1 = &premise_interpolants[0];
821                    let i2 = &premise_interpolants[1];
822                    let c1 = premise_colors[0];
823                    let c2 = premise_colors[1];
824
825                    match (c1, c2) {
826                        (InterpolantColor::A, InterpolantColor::B)
827                        | (InterpolantColor::B, InterpolantColor::A) => {
828                            // Huang: use conjunction for mixed
829                            InterpolantTerm::and(vec![i1.clone(), i2.clone()])
830                        }
831                        (InterpolantColor::A, InterpolantColor::AB) => i2.clone(),
832                        (InterpolantColor::AB, InterpolantColor::A) => i1.clone(),
833                        (InterpolantColor::B, InterpolantColor::AB) => i2.clone(),
834                        (InterpolantColor::AB, InterpolantColor::B) => i1.clone(),
835                        (InterpolantColor::AB, InterpolantColor::AB) => {
836                            // Huang: conjunction for both mixed
837                            InterpolantTerm::and(vec![i1.clone(), i2.clone()])
838                        }
839                        _ => InterpolantTerm::and(vec![i1.clone(), i2.clone()]),
840                    }
841                } else {
842                    // Conjunction for other rules
843                    InterpolantTerm::and(premise_interpolants.to_vec())
844                }
845            }
846        }
847    }
848}
849
850// ============================================================================
851// Theory-Specific Interpolation
852// ============================================================================
853
854/// Theory-specific interpolant generator
855pub trait TheoryInterpolator: Send + Sync {
856    /// Theory name
857    fn name(&self) -> &'static str;
858
859    /// Check if this theory can handle the given literals
860    fn can_handle(&self, literals: &[&str]) -> bool;
861
862    /// Generate theory-specific interpolant
863    fn interpolate(
864        &self,
865        a_literals: &[InterpolantTerm],
866        b_literals: &[InterpolantTerm],
867        shared_symbols: &FxHashSet<Symbol>,
868    ) -> Option<InterpolantTerm>;
869}
870
871/// LIA (Linear Integer Arithmetic) interpolator
872#[derive(Debug, Default)]
873pub struct LiaInterpolator;
874
875impl TheoryInterpolator for LiaInterpolator {
876    fn name(&self) -> &'static str {
877        "LIA"
878    }
879
880    fn can_handle(&self, literals: &[&str]) -> bool {
881        literals.iter().any(|l| {
882            l.contains('+')
883                || l.contains('-')
884                || l.contains('*')
885                || l.contains("<=")
886                || l.contains(">=")
887                || l.contains('<')
888                || l.contains('>')
889        })
890    }
891
892    fn interpolate(
893        &self,
894        a_literals: &[InterpolantTerm],
895        b_literals: &[InterpolantTerm],
896        _shared_symbols: &FxHashSet<Symbol>,
897    ) -> Option<InterpolantTerm> {
898        // Farkas-based interpolation for LIA
899        // For a ∧ ¬a ≤ 0 (A) and b ≤ 0 (B) with shared variables x:
900        // The interpolant is of the form c·x ≤ d where c and d are computed from Farkas coefficients
901
902        if a_literals.is_empty() || b_literals.is_empty() {
903            return None;
904        }
905
906        // Simplified: just project A to shared symbols
907        // Full implementation would use Farkas lemma
908        Some(InterpolantTerm::and(a_literals.to_vec()))
909    }
910}
911
912/// EUF (Equality with Uninterpreted Functions) interpolator
913#[derive(Debug, Default)]
914pub struct EufInterpolator;
915
916impl TheoryInterpolator for EufInterpolator {
917    fn name(&self) -> &'static str {
918        "EUF"
919    }
920
921    fn can_handle(&self, literals: &[&str]) -> bool {
922        literals.iter().any(|l| l.contains('=') || l.contains('('))
923    }
924
925    fn interpolate(
926        &self,
927        a_literals: &[InterpolantTerm],
928        _b_literals: &[InterpolantTerm],
929        _shared_symbols: &FxHashSet<Symbol>,
930    ) -> Option<InterpolantTerm> {
931        // Congruence-based interpolation
932        // Extract equalities and project to shared terms
933
934        if a_literals.is_empty() {
935            return Some(InterpolantTerm::true_val());
936        }
937
938        // Simplified: return conjunction of A equalities over shared symbols
939        Some(InterpolantTerm::and(a_literals.to_vec()))
940    }
941}
942
943/// Array theory interpolator
944#[derive(Debug, Default)]
945pub struct ArrayInterpolator;
946
947impl TheoryInterpolator for ArrayInterpolator {
948    fn name(&self) -> &'static str {
949        "Array"
950    }
951
952    fn can_handle(&self, literals: &[&str]) -> bool {
953        literals
954            .iter()
955            .any(|l| l.contains("select") || l.contains("store"))
956    }
957
958    fn interpolate(
959        &self,
960        a_literals: &[InterpolantTerm],
961        _b_literals: &[InterpolantTerm],
962        _shared_symbols: &FxHashSet<Symbol>,
963    ) -> Option<InterpolantTerm> {
964        // Array interpolation using read-over-write axioms
965        // Project array terms to shared indices
966
967        if a_literals.is_empty() {
968            return Some(InterpolantTerm::true_val());
969        }
970
971        Some(InterpolantTerm::and(a_literals.to_vec()))
972    }
973}
974
975// ============================================================================
976// Sequence Interpolation
977// ============================================================================
978
979/// Sequence interpolation for multiple formulas
980///
981/// Given A₁, A₂, ..., Aₙ where ∧Aᵢ is UNSAT,
982/// compute interpolants I₁, I₂, ..., Iₙ₋₁ such that:
983/// - A₁ ⟹ I₁
984/// - Iᵢ ∧ Aᵢ₊₁ ⟹ Iᵢ₊₁
985/// - Iₙ₋₁ ∧ Aₙ is UNSAT
986#[derive(Debug)]
987pub struct SequenceInterpolator {
988    config: InterpolationConfig,
989}
990
991impl SequenceInterpolator {
992    /// Create a new sequence interpolator
993    #[must_use]
994    pub fn new(config: InterpolationConfig) -> Self {
995        Self { config }
996    }
997
998    /// Compute sequence of interpolants
999    ///
1000    /// Returns n-1 interpolants for n formulas
1001    pub fn interpolate_sequence(
1002        &self,
1003        proofs: &[Proof],
1004    ) -> Result<Vec<InterpolantTerm>, InterpolationError> {
1005        if proofs.len() < 2 {
1006            return Err(InterpolationError::TooFewFormulas);
1007        }
1008
1009        let mut interpolants = Vec::with_capacity(proofs.len() - 1);
1010
1011        // For each split point, compute the interpolant
1012        for i in 0..proofs.len() - 1 {
1013            // Partition: A = proofs[0..=i], B = proofs[i+1..]
1014            let a_ids: FxHashSet<_> = (0..=i).map(|j| PremiseId(j as u32)).collect();
1015            let b_ids: FxHashSet<_> = (i + 1..proofs.len()).map(|j| PremiseId(j as u32)).collect();
1016
1017            let partition = InterpolantPartition::new(a_ids, b_ids);
1018            let mut interpolator =
1019                CraigInterpolator::new(self.config.clone(), partition, PremiseTracker::new());
1020
1021            // Use first proof as representative (simplified)
1022            if let Some(proof) = proofs.first() {
1023                let interp = interpolator.extract(proof)?;
1024                interpolants.push(interp);
1025            } else {
1026                interpolants.push(InterpolantTerm::true_val());
1027            }
1028        }
1029
1030        Ok(interpolants)
1031    }
1032}
1033
1034impl Default for SequenceInterpolator {
1035    fn default() -> Self {
1036        Self::new(InterpolationConfig::default())
1037    }
1038}
1039
1040// ============================================================================
1041// Tree Interpolation
1042// ============================================================================
1043
1044/// Tree interpolation for hierarchical formulas
1045///
1046/// Given a tree of formulas where leaves are UNSAT,
1047/// compute interpolants for internal nodes
1048#[derive(Debug)]
1049pub struct TreeInterpolator {
1050    config: InterpolationConfig,
1051}
1052
1053/// Tree node for tree interpolation
1054#[derive(Debug, Clone)]
1055pub struct TreeNode {
1056    /// Node ID
1057    pub id: usize,
1058    /// Formula at this node (as term)
1059    pub formula: InterpolantTerm,
1060    /// Children node IDs
1061    pub children: Vec<usize>,
1062    /// Parent node ID (None for root)
1063    pub parent: Option<usize>,
1064}
1065
1066impl TreeInterpolator {
1067    /// Create a new tree interpolator
1068    #[must_use]
1069    pub fn new(config: InterpolationConfig) -> Self {
1070        Self { config }
1071    }
1072
1073    /// Compute tree interpolants
1074    ///
1075    /// Returns an interpolant for each non-leaf node
1076    pub fn interpolate_tree(
1077        &self,
1078        nodes: &[TreeNode],
1079    ) -> Result<FxHashMap<usize, InterpolantTerm>, InterpolationError> {
1080        let mut interpolants = FxHashMap::default();
1081
1082        // Process nodes bottom-up (leaves first)
1083        let mut order = self.topological_order(nodes);
1084        order.reverse();
1085
1086        for node_id in order {
1087            if let Some(node) = nodes.get(node_id) {
1088                if node.children.is_empty() {
1089                    // Leaf: interpolant is the formula itself
1090                    interpolants.insert(node_id, node.formula.clone());
1091                } else {
1092                    // Internal node: combine children interpolants
1093                    let child_interps: Vec<_> = node
1094                        .children
1095                        .iter()
1096                        .filter_map(|&c| interpolants.get(&c).cloned())
1097                        .collect();
1098
1099                    let combined = if self.config.algorithm == InterpolationAlgorithm::McMillan {
1100                        InterpolantTerm::or(child_interps)
1101                    } else {
1102                        InterpolantTerm::and(child_interps)
1103                    };
1104
1105                    let interp = InterpolantTerm::and(vec![node.formula.clone(), combined]);
1106                    interpolants.insert(node_id, interp.simplify());
1107                }
1108            }
1109        }
1110
1111        Ok(interpolants)
1112    }
1113
1114    /// Topological order of nodes (parents before children)
1115    fn topological_order(&self, nodes: &[TreeNode]) -> Vec<usize> {
1116        let mut order = Vec::new();
1117        let mut visited = FxHashSet::default();
1118
1119        fn visit(
1120            node_id: usize,
1121            nodes: &[TreeNode],
1122            visited: &mut FxHashSet<usize>,
1123            order: &mut Vec<usize>,
1124        ) {
1125            if visited.contains(&node_id) {
1126                return;
1127            }
1128            visited.insert(node_id);
1129
1130            if let Some(node) = nodes.get(node_id) {
1131                for &child in &node.children {
1132                    visit(child, nodes, visited, order);
1133                }
1134            }
1135            order.push(node_id);
1136        }
1137
1138        // Find roots (nodes with no parent)
1139        for (i, node) in nodes.iter().enumerate() {
1140            if node.parent.is_none() {
1141                visit(i, nodes, &mut visited, &mut order);
1142            }
1143        }
1144
1145        order
1146    }
1147}
1148
1149impl Default for TreeInterpolator {
1150    fn default() -> Self {
1151        Self::new(InterpolationConfig::default())
1152    }
1153}
1154
1155// ============================================================================
1156// Errors
1157// ============================================================================
1158
1159/// Errors during interpolation
1160#[derive(Debug, Clone)]
1161pub enum InterpolationError {
1162    /// Proof has no root
1163    NoRoot,
1164    /// Node not found in proof
1165    NodeNotFound(ProofNodeId),
1166    /// Node has no computed color
1167    NoColor(ProofNodeId),
1168    /// Too few formulas for sequence interpolation
1169    TooFewFormulas,
1170    /// Interpolant validation failed
1171    ValidationFailed(String),
1172    /// Theory interpolation not supported
1173    TheoryNotSupported(String),
1174}
1175
1176impl fmt::Display for InterpolationError {
1177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1178        match self {
1179            Self::NoRoot => write!(f, "Proof has no root"),
1180            Self::NodeNotFound(id) => write!(f, "Node {} not found", id),
1181            Self::NoColor(id) => write!(f, "Node {} has no computed color", id),
1182            Self::TooFewFormulas => write!(f, "Need at least 2 formulas for interpolation"),
1183            Self::ValidationFailed(msg) => write!(f, "Validation failed: {}", msg),
1184            Self::TheoryNotSupported(theory) => {
1185                write!(f, "Theory {} not supported for interpolation", theory)
1186            }
1187        }
1188    }
1189}
1190
1191impl std::error::Error for InterpolationError {}
1192
1193// ============================================================================
1194// Tests
1195// ============================================================================
1196
1197#[cfg(test)]
1198mod tests {
1199    use super::*;
1200
1201    #[test]
1202    fn test_interpolant_term_creation() {
1203        let t = InterpolantTerm::true_val();
1204        assert!(t.is_true());
1205
1206        let f = InterpolantTerm::false_val();
1207        assert!(f.is_false());
1208
1209        let x = InterpolantTerm::var("x");
1210        assert!(!x.is_true());
1211        assert!(!x.is_false());
1212    }
1213
1214    #[test]
1215    fn test_interpolant_term_and() {
1216        let t = InterpolantTerm::true_val();
1217        let x = InterpolantTerm::var("x");
1218        let y = InterpolantTerm::var("y");
1219
1220        // true ∧ x = x
1221        let and1 = InterpolantTerm::and(vec![t.clone(), x.clone()]);
1222        assert_eq!(and1, x);
1223
1224        // false ∧ x = false
1225        let f = InterpolantTerm::false_val();
1226        let and2 = InterpolantTerm::and(vec![f.clone(), x.clone()]);
1227        assert!(and2.is_false());
1228
1229        // x ∧ y
1230        let and3 = InterpolantTerm::and(vec![x.clone(), y.clone()]);
1231        match and3 {
1232            InterpolantTerm::And(args) => assert_eq!(args.len(), 2),
1233            _ => panic!("Expected And"),
1234        }
1235    }
1236
1237    #[test]
1238    fn test_interpolant_term_or() {
1239        let t = InterpolantTerm::true_val();
1240        let f = InterpolantTerm::false_val();
1241        let x = InterpolantTerm::var("x");
1242
1243        // false ∨ x = x
1244        let or1 = InterpolantTerm::or(vec![f.clone(), x.clone()]);
1245        assert_eq!(or1, x);
1246
1247        // true ∨ x = true
1248        let or2 = InterpolantTerm::or(vec![t.clone(), x.clone()]);
1249        assert!(or2.is_true());
1250    }
1251
1252    #[test]
1253    fn test_interpolant_term_not() {
1254        let t = InterpolantTerm::true_val();
1255        let f = InterpolantTerm::false_val();
1256        let x = InterpolantTerm::var("x");
1257
1258        // ¬true = false
1259        let not_t = InterpolantTerm::not(t);
1260        assert!(not_t.is_false());
1261
1262        // ¬false = true
1263        let not_f = InterpolantTerm::not(f);
1264        assert!(not_f.is_true());
1265
1266        // ¬¬x = x
1267        let not_x = InterpolantTerm::not(x.clone());
1268        let not_not_x = InterpolantTerm::not(not_x);
1269        assert_eq!(not_not_x, x);
1270    }
1271
1272    #[test]
1273    fn test_interpolant_term_implies() {
1274        let t = InterpolantTerm::true_val();
1275        let f = InterpolantTerm::false_val();
1276        let x = InterpolantTerm::var("x");
1277
1278        // false → x = true
1279        let imp1 = InterpolantTerm::implies(f.clone(), x.clone());
1280        assert!(imp1.is_true());
1281
1282        // true → x = x
1283        let imp2 = InterpolantTerm::implies(t.clone(), x.clone());
1284        assert_eq!(imp2, x);
1285
1286        // x → true = true
1287        let imp3 = InterpolantTerm::implies(x.clone(), t);
1288        assert!(imp3.is_true());
1289    }
1290
1291    #[test]
1292    fn test_interpolant_term_display() {
1293        let x = InterpolantTerm::var("x");
1294        let y = InterpolantTerm::var("y");
1295        let and = InterpolantTerm::and(vec![x.clone(), y.clone()]);
1296
1297        assert_eq!(format!("{}", and), "(and x y)");
1298
1299        let or = InterpolantTerm::or(vec![x, y]);
1300        assert_eq!(format!("{}", or), "(or x y)");
1301    }
1302
1303    #[test]
1304    fn test_symbol_collection() {
1305        let x = InterpolantTerm::var("x");
1306        let y = InterpolantTerm::var("y");
1307        let and = InterpolantTerm::and(vec![x, y]);
1308
1309        let mut symbols = FxHashSet::default();
1310        and.collect_symbols(&mut symbols);
1311
1312        assert_eq!(symbols.len(), 2);
1313        assert!(symbols.contains(&Symbol::var("x")));
1314        assert!(symbols.contains(&Symbol::var("y")));
1315    }
1316
1317    #[test]
1318    fn test_interpolant_simplify() {
1319        let x = InterpolantTerm::var("x");
1320        let t = InterpolantTerm::true_val();
1321
1322        let term = InterpolantTerm::and(vec![t, x.clone()]);
1323        let simplified = term.simplify();
1324        assert_eq!(simplified, x);
1325    }
1326
1327    #[test]
1328    fn test_partition_creation() {
1329        let partition = InterpolantPartition::new(
1330            vec![PremiseId(0), PremiseId(1)],
1331            vec![PremiseId(2), PremiseId(3)],
1332        );
1333
1334        assert!(partition.is_a_premise(PremiseId(0)));
1335        assert!(partition.is_a_premise(PremiseId(1)));
1336        assert!(!partition.is_a_premise(PremiseId(2)));
1337
1338        assert!(partition.is_b_premise(PremiseId(2)));
1339        assert!(partition.is_b_premise(PremiseId(3)));
1340        assert!(!partition.is_b_premise(PremiseId(0)));
1341    }
1342
1343    #[test]
1344    fn test_interpolation_config_default() {
1345        let config = InterpolationConfig::default();
1346
1347        assert_eq!(config.algorithm, InterpolationAlgorithm::Pudlak);
1348        assert!(config.use_theory_interpolants);
1349        assert!(config.simplify_interpolants);
1350        assert!(config.enable_caching);
1351    }
1352
1353    #[test]
1354    fn test_interpolation_stats_default() {
1355        let stats = InterpolationStats::default();
1356
1357        assert_eq!(stats.nodes_processed, 0);
1358        assert_eq!(stats.a_nodes, 0);
1359        assert_eq!(stats.b_nodes, 0);
1360        assert_eq!(stats.ab_nodes, 0);
1361    }
1362
1363    #[test]
1364    fn test_lia_interpolator() {
1365        let interp = LiaInterpolator;
1366
1367        assert_eq!(interp.name(), "LIA");
1368        assert!(interp.can_handle(&["x + y <= 10"]));
1369        assert!(!interp.can_handle(&["p and q"]));
1370    }
1371
1372    #[test]
1373    fn test_euf_interpolator() {
1374        let interp = EufInterpolator;
1375
1376        assert_eq!(interp.name(), "EUF");
1377        assert!(interp.can_handle(&["f(x) = y"]));
1378        assert!(interp.can_handle(&["x = y"]));
1379    }
1380
1381    #[test]
1382    fn test_array_interpolator() {
1383        let interp = ArrayInterpolator;
1384
1385        assert_eq!(interp.name(), "Array");
1386        assert!(interp.can_handle(&["select(a, i)"]));
1387        assert!(interp.can_handle(&["store(a, i, v)"]));
1388    }
1389
1390    #[test]
1391    fn test_tree_node() {
1392        let node = TreeNode {
1393            id: 0,
1394            formula: InterpolantTerm::var("x"),
1395            children: vec![1, 2],
1396            parent: None,
1397        };
1398
1399        assert_eq!(node.id, 0);
1400        assert_eq!(node.children.len(), 2);
1401        assert!(node.parent.is_none());
1402    }
1403
1404    #[test]
1405    fn test_sequence_interpolator_too_few() {
1406        let seq = SequenceInterpolator::default();
1407        let result = seq.interpolate_sequence(&[]);
1408
1409        assert!(matches!(result, Err(InterpolationError::TooFewFormulas)));
1410    }
1411
1412    #[test]
1413    fn test_interpolation_error_display() {
1414        let err = InterpolationError::NoRoot;
1415        assert_eq!(format!("{}", err), "Proof has no root");
1416
1417        let err2 = InterpolationError::NodeNotFound(ProofNodeId(5));
1418        assert!(format!("{}", err2).contains("not found"));
1419    }
1420
1421    #[test]
1422    fn test_color_display() {
1423        assert_eq!(format!("{}", InterpolantColor::A), "A");
1424        assert_eq!(format!("{}", InterpolantColor::B), "B");
1425        assert_eq!(format!("{}", InterpolantColor::AB), "AB");
1426    }
1427
1428    #[test]
1429    fn test_mcmillan_basic() {
1430        let config = InterpolationConfig {
1431            algorithm: InterpolationAlgorithm::McMillan,
1432            ..Default::default()
1433        };
1434        let partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1435        let interpolator = CraigInterpolator::new(config, partition, PremiseTracker::new());
1436
1437        // Test axiom interpolants
1438        let a_interp = interpolator.compute_axiom_interpolant(InterpolantColor::A, "p");
1439        assert!(a_interp.is_true());
1440
1441        let b_interp = interpolator.compute_axiom_interpolant(InterpolantColor::B, "q");
1442        assert!(b_interp.is_false());
1443    }
1444
1445    #[test]
1446    fn test_pudlak_basic() {
1447        let config = InterpolationConfig {
1448            algorithm: InterpolationAlgorithm::Pudlak,
1449            ..Default::default()
1450        };
1451        let partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1452        let interpolator = CraigInterpolator::new(config, partition, PremiseTracker::new());
1453
1454        let a_interp = interpolator.compute_axiom_interpolant(InterpolantColor::A, "p");
1455        assert!(a_interp.is_true());
1456    }
1457
1458    #[test]
1459    fn test_huang_basic() {
1460        let config = InterpolationConfig {
1461            algorithm: InterpolationAlgorithm::Huang,
1462            ..Default::default()
1463        };
1464        let partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1465        let interpolator = CraigInterpolator::new(config, partition, PremiseTracker::new());
1466
1467        let a_interp = interpolator.compute_axiom_interpolant(InterpolantColor::A, "p");
1468        assert!(a_interp.is_true());
1469    }
1470
1471    #[test]
1472    fn test_tree_interpolator_empty() {
1473        let tree_interp = TreeInterpolator::default();
1474        let result = tree_interp.interpolate_tree(&[]);
1475
1476        assert!(result.is_ok());
1477        let interps = result.expect("Should succeed");
1478        assert!(interps.is_empty());
1479    }
1480
1481    #[test]
1482    fn test_tree_interpolator_single_leaf() {
1483        let tree_interp = TreeInterpolator::default();
1484        let nodes = vec![TreeNode {
1485            id: 0,
1486            formula: InterpolantTerm::var("x"),
1487            children: vec![],
1488            parent: None,
1489        }];
1490
1491        let result = tree_interp.interpolate_tree(&nodes);
1492        assert!(result.is_ok());
1493
1494        let interps = result.expect("Should succeed");
1495        assert_eq!(interps.len(), 1);
1496        assert!(interps.contains_key(&0));
1497    }
1498
1499    #[test]
1500    fn test_nested_and_or() {
1501        let x = InterpolantTerm::var("x");
1502        let y = InterpolantTerm::var("y");
1503        let z = InterpolantTerm::var("z");
1504
1505        // (x ∧ y) ∧ z should flatten to x ∧ y ∧ z
1506        let inner = InterpolantTerm::and(vec![x.clone(), y.clone()]);
1507        let outer = InterpolantTerm::and(vec![inner, z.clone()]);
1508
1509        match outer {
1510            InterpolantTerm::And(args) => assert_eq!(args.len(), 3),
1511            _ => panic!("Expected flattened And"),
1512        }
1513    }
1514
1515    #[test]
1516    fn test_num_term() {
1517        use num_bigint::BigInt;
1518
1519        let one = InterpolantTerm::Num(BigRational::from_integer(BigInt::from(1)));
1520        let two = InterpolantTerm::Num(BigRational::from_integer(BigInt::from(2)));
1521
1522        let add = InterpolantTerm::Add(vec![one.clone(), two.clone()]);
1523        assert_eq!(format!("{}", add), "(+ 1 2)");
1524
1525        let mul = InterpolantTerm::Mul(vec![one, two]);
1526        assert_eq!(format!("{}", mul), "(* 1 2)");
1527    }
1528
1529    #[test]
1530    fn test_select_store_display() {
1531        let a = InterpolantTerm::var("a");
1532        let i = InterpolantTerm::var("i");
1533        let v = InterpolantTerm::var("v");
1534
1535        let select = InterpolantTerm::Select(Box::new(a.clone()), Box::new(i.clone()));
1536        assert_eq!(format!("{}", select), "(select a i)");
1537
1538        let store = InterpolantTerm::Store(Box::new(a), Box::new(i), Box::new(v));
1539        assert_eq!(format!("{}", store), "(store a i v)");
1540    }
1541
1542    #[test]
1543    fn test_shared_symbols() {
1544        let mut partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1545
1546        let x = Symbol::var("x");
1547        let y = Symbol::var("y");
1548
1549        partition.set_shared_symbols(vec![x.clone()]);
1550
1551        assert!(partition.is_shared(&x));
1552        assert!(!partition.is_shared(&y));
1553    }
1554
1555    #[test]
1556    fn test_interpolation_algorithms() {
1557        // Test all three algorithms are distinct
1558        assert_ne!(
1559            InterpolationAlgorithm::McMillan,
1560            InterpolationAlgorithm::Pudlak
1561        );
1562        assert_ne!(
1563            InterpolationAlgorithm::Pudlak,
1564            InterpolationAlgorithm::Huang
1565        );
1566        assert_ne!(
1567            InterpolationAlgorithm::McMillan,
1568            InterpolationAlgorithm::Huang
1569        );
1570    }
1571
1572    #[test]
1573    fn test_mcmillan_inference_ab_ab() {
1574        let config = InterpolationConfig {
1575            algorithm: InterpolationAlgorithm::McMillan,
1576            ..Default::default()
1577        };
1578        let partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1579        let interpolator = CraigInterpolator::new(config, partition, PremiseTracker::new());
1580
1581        let x = InterpolantTerm::var("x");
1582        let y = InterpolantTerm::var("y");
1583        let premises = vec![x.clone(), y.clone()];
1584        let colors = vec![InterpolantColor::AB, InterpolantColor::AB];
1585
1586        let result = interpolator.mcmillan_interpolant(
1587            "resolution",
1588            &premises,
1589            &colors,
1590            InterpolantColor::AB,
1591        );
1592
1593        // McMillan uses disjunction for AB/AB
1594        match result {
1595            InterpolantTerm::Or(args) => assert_eq!(args.len(), 2),
1596            _ => panic!("Expected Or"),
1597        }
1598    }
1599
1600    #[test]
1601    fn test_huang_inference_ab_ab() {
1602        let config = InterpolationConfig {
1603            algorithm: InterpolationAlgorithm::Huang,
1604            ..Default::default()
1605        };
1606        let partition = InterpolantPartition::new(vec![PremiseId(0)], vec![PremiseId(1)]);
1607        let interpolator = CraigInterpolator::new(config, partition, PremiseTracker::new());
1608
1609        let x = InterpolantTerm::var("x");
1610        let y = InterpolantTerm::var("y");
1611        let premises = vec![x.clone(), y.clone()];
1612        let colors = vec![InterpolantColor::AB, InterpolantColor::AB];
1613
1614        let result =
1615            interpolator.huang_interpolant("resolution", &premises, &colors, InterpolantColor::AB);
1616
1617        // Huang uses conjunction for AB/AB
1618        match result {
1619            InterpolantTerm::And(args) => assert_eq!(args.len(), 2),
1620            _ => panic!("Expected And"),
1621        }
1622    }
1623}