Skip to main content

oxiz_solver/solver/
types.rs

1//! Types and data structures for the SMT solver
2
3#[allow(unused_imports)]
4use crate::prelude::*;
5use num_rational::Rational64;
6use oxiz_core::ast::{RoundingMode, TermId, TermKind, TermManager};
7use oxiz_sat::{Lit, RestartStrategy, Var};
8use smallvec::SmallVec;
9
10/// Proof step for resolution-based proofs
11#[derive(Debug, Clone)]
12pub enum ProofStep {
13    /// Input clause (from the original formula)
14    Input {
15        /// Clause index
16        index: u32,
17        /// The clause (as a disjunction of literals)
18        clause: Vec<Lit>,
19    },
20    /// Resolution step
21    Resolution {
22        /// Index of this proof step
23        index: u32,
24        /// Left parent clause index
25        left: u32,
26        /// Right parent clause index
27        right: u32,
28        /// Pivot variable (the variable resolved on)
29        pivot: Var,
30        /// Resulting clause
31        clause: Vec<Lit>,
32    },
33    /// Theory lemma (from a theory solver)
34    TheoryLemma {
35        /// Index of this proof step
36        index: u32,
37        /// The theory that produced this lemma
38        theory: String,
39        /// The lemma clause
40        clause: Vec<Lit>,
41        /// Explanation terms
42        explanation: Vec<TermId>,
43    },
44}
45
46/// A proof of unsatisfiability
47#[derive(Debug, Clone)]
48pub struct Proof {
49    /// Sequence of proof steps leading to the empty clause
50    steps: Vec<ProofStep>,
51    /// Index of the final empty clause (proving unsat)
52    empty_clause_index: Option<u32>,
53}
54
55impl Proof {
56    /// Create a new empty proof
57    #[must_use]
58    pub fn new() -> Self {
59        Self {
60            steps: Vec::new(),
61            empty_clause_index: None,
62        }
63    }
64
65    /// Add a proof step
66    pub fn add_step(&mut self, step: ProofStep) {
67        self.steps.push(step);
68    }
69
70    /// Set the index of the empty clause (final step proving unsat)
71    pub fn set_empty_clause(&mut self, index: u32) {
72        self.empty_clause_index = Some(index);
73    }
74
75    /// Check if the proof is complete (has an empty clause)
76    #[must_use]
77    pub fn is_complete(&self) -> bool {
78        self.empty_clause_index.is_some()
79    }
80
81    /// Get the number of proof steps
82    #[must_use]
83    pub fn len(&self) -> usize {
84        self.steps.len()
85    }
86
87    /// Check if the proof is empty
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.steps.is_empty()
91    }
92
93    /// Iterate over all proof steps
94    pub fn steps(&self) -> impl Iterator<Item = &ProofStep> {
95        self.steps.iter()
96    }
97
98    /// Format the proof as a string (for debugging or output)
99    #[must_use]
100    pub fn format(&self) -> String {
101        let mut result = String::from("(proof\n");
102        for step in &self.steps {
103            match step {
104                ProofStep::Input { index, clause } => {
105                    result.push_str(&format!("  (input {} {:?})\n", index, clause));
106                }
107                ProofStep::Resolution {
108                    index,
109                    left,
110                    right,
111                    pivot,
112                    clause,
113                } => {
114                    result.push_str(&format!(
115                        "  (resolution {} {} {} {:?} {:?})\n",
116                        index, left, right, pivot, clause
117                    ));
118                }
119                ProofStep::TheoryLemma {
120                    index,
121                    theory,
122                    clause,
123                    ..
124                } => {
125                    result.push_str(&format!(
126                        "  (theory-lemma {} {} {:?})\n",
127                        index, theory, clause
128                    ));
129                }
130            }
131        }
132        if let Some(idx) = self.empty_clause_index {
133            result.push_str(&format!("  (empty-clause {})\n", idx));
134        }
135        result.push_str(")\n");
136        result
137    }
138}
139
140impl Default for Proof {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146/// Represents a theory constraint associated with a boolean variable
147#[derive(Debug, Clone)]
148#[allow(dead_code)]
149pub(crate) enum Constraint {
150    /// Equality constraint: lhs = rhs
151    Eq(TermId, TermId),
152    /// Disequality constraint: lhs != rhs (negation of equality)
153    Diseq(TermId, TermId),
154    /// Less-than constraint: lhs < rhs
155    Lt(TermId, TermId),
156    /// Less-than-or-equal constraint: lhs <= rhs
157    Le(TermId, TermId),
158    /// Greater-than constraint: lhs > rhs
159    Gt(TermId, TermId),
160    /// Greater-than-or-equal constraint: lhs >= rhs
161    Ge(TermId, TermId),
162    /// Boolean-valued uninterpreted function application.
163    /// When the SAT solver assigns this variable true/false, we must inform
164    /// the EUF solver so that congruence closure can detect conflicts
165    /// (e.g., `t(m) = true` and `t(co) = false` but `m = co`).
166    BoolApp(TermId),
167}
168
169/// Type of arithmetic constraint
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub(crate) enum ArithConstraintType {
172    /// Less than (<)
173    Lt,
174    /// Less than or equal (<=)
175    Le,
176    /// Greater than (>)
177    Gt,
178    /// Greater than or equal (>=)
179    Ge,
180}
181
182/// Parsed arithmetic constraint with extracted linear expression
183/// Represents: sum of (term, coefficient) <= constant OR < constant (if strict)
184#[derive(Debug, Clone)]
185pub(crate) struct ParsedArithConstraint {
186    /// Linear terms: (variable_term, coefficient)
187    pub(crate) terms: SmallVec<[(TermId, Rational64); 4]>,
188    /// Constant bound (RHS)
189    pub(crate) constant: Rational64,
190    /// Type of constraint
191    pub(crate) constraint_type: ArithConstraintType,
192    /// The original term (for conflict explanation)
193    pub(crate) reason_term: TermId,
194}
195
196/// Polarity of a term in the formula
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub(crate) enum Polarity {
199    /// Term appears only positively
200    Positive,
201    /// Term appears only negatively
202    Negative,
203    /// Term appears in both polarities
204    Both,
205}
206
207/// Result of SMT solving
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum SolverResult {
210    /// Satisfiable
211    Sat,
212    /// Unsatisfiable
213    Unsat,
214    /// Unknown (timeout, incomplete, etc.)
215    Unknown,
216}
217
218/// Theory checking mode
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum TheoryMode {
221    /// Eager theory checking (check on every assignment)
222    Eager,
223    /// Lazy theory checking (check only on complete assignments)
224    Lazy,
225}
226
227/// Solver configuration
228#[derive(Debug, Clone)]
229pub struct SolverConfig {
230    /// Timeout in milliseconds (0 = no timeout)
231    pub timeout_ms: u64,
232    /// Enable parallel solving
233    pub parallel: bool,
234    /// Number of threads for parallel solving
235    pub num_threads: usize,
236    /// Enable proof generation
237    pub proof: bool,
238    /// Enable model generation
239    pub model: bool,
240    /// Theory checking mode
241    pub theory_mode: TheoryMode,
242    /// Enable preprocessing/simplification
243    pub simplify: bool,
244    /// Maximum number of conflicts before giving up (0 = unlimited)
245    pub max_conflicts: u64,
246    /// Maximum number of decisions before giving up (0 = unlimited)
247    pub max_decisions: u64,
248    /// Restart strategy for SAT solver
249    pub restart_strategy: RestartStrategy,
250    /// Enable clause minimization (recursive minimization of learned clauses)
251    pub enable_clause_minimization: bool,
252    /// Enable learned clause subsumption
253    pub enable_clause_subsumption: bool,
254    /// Enable variable elimination during preprocessing
255    pub enable_variable_elimination: bool,
256    /// Variable elimination limit (max clauses to produce)
257    pub variable_elimination_limit: usize,
258    /// Enable blocked clause elimination during preprocessing
259    pub enable_blocked_clause_elimination: bool,
260    /// Enable symmetry breaking predicates
261    pub enable_symmetry_breaking: bool,
262    /// Enable inprocessing (periodic preprocessing during search)
263    pub enable_inprocessing: bool,
264    /// Inprocessing interval (number of conflicts between inprocessing)
265    pub inprocessing_interval: u64,
266}
267
268impl Default for SolverConfig {
269    fn default() -> Self {
270        Self::balanced()
271    }
272}
273
274impl SolverConfig {
275    /// Create a configuration optimized for speed (minimal preprocessing)
276    /// Best for easy problems or when quick results are needed
277    #[must_use]
278    pub fn fast() -> Self {
279        Self {
280            timeout_ms: 0,
281            parallel: false,
282            num_threads: 4,
283            proof: false,
284            model: true,
285            theory_mode: TheoryMode::Eager,
286            simplify: true, // Keep basic simplification
287            max_conflicts: 0,
288            max_decisions: 0,
289            restart_strategy: RestartStrategy::Geometric, // Faster than Glucose
290            enable_clause_minimization: true,             // Keep this, it's fast
291            enable_clause_subsumption: false,             // Skip for speed
292            enable_variable_elimination: false,           // Skip preprocessing
293            variable_elimination_limit: 0,
294            enable_blocked_clause_elimination: false, // Skip preprocessing
295            enable_symmetry_breaking: false,
296            enable_inprocessing: false, // No inprocessing for speed
297            inprocessing_interval: 0,
298        }
299    }
300
301    /// Create a balanced configuration (default)
302    /// Good balance between preprocessing and solving speed
303    #[must_use]
304    pub fn balanced() -> Self {
305        Self {
306            timeout_ms: 0,
307            parallel: false,
308            num_threads: 4,
309            proof: false,
310            model: true,
311            theory_mode: TheoryMode::Eager,
312            simplify: true,
313            max_conflicts: 0,
314            max_decisions: 0,
315            restart_strategy: RestartStrategy::Glucose, // Adaptive restarts
316            enable_clause_minimization: true,
317            enable_clause_subsumption: true,
318            enable_variable_elimination: true,
319            variable_elimination_limit: 1000, // Conservative limit
320            enable_blocked_clause_elimination: true,
321            enable_symmetry_breaking: false, // Still expensive
322            enable_inprocessing: true,
323            inprocessing_interval: 10000,
324        }
325    }
326
327    /// Create a configuration optimized for hard problems
328    /// Uses aggressive preprocessing and symmetry breaking
329    #[must_use]
330    pub fn thorough() -> Self {
331        Self {
332            timeout_ms: 0,
333            parallel: false,
334            num_threads: 4,
335            proof: false,
336            model: true,
337            theory_mode: TheoryMode::Eager,
338            simplify: true,
339            max_conflicts: 0,
340            max_decisions: 0,
341            restart_strategy: RestartStrategy::Glucose,
342            enable_clause_minimization: true,
343            enable_clause_subsumption: true,
344            enable_variable_elimination: true,
345            variable_elimination_limit: 5000, // More aggressive
346            enable_blocked_clause_elimination: true,
347            enable_symmetry_breaking: true, // Enable for hard problems
348            enable_inprocessing: true,
349            inprocessing_interval: 5000, // More frequent inprocessing
350        }
351    }
352
353    /// Create a minimal configuration (almost all features disabled)
354    /// Useful for debugging or when you want full control
355    #[must_use]
356    pub fn minimal() -> Self {
357        Self {
358            timeout_ms: 0,
359            parallel: false,
360            num_threads: 1,
361            proof: false,
362            model: true,
363            theory_mode: TheoryMode::Lazy, // Lazy for minimal overhead
364            simplify: false,
365            max_conflicts: 0,
366            max_decisions: 0,
367            restart_strategy: RestartStrategy::Geometric,
368            enable_clause_minimization: false,
369            enable_clause_subsumption: false,
370            enable_variable_elimination: false,
371            variable_elimination_limit: 0,
372            enable_blocked_clause_elimination: false,
373            enable_symmetry_breaking: false,
374            enable_inprocessing: false,
375            inprocessing_interval: 0,
376        }
377    }
378
379    /// Enable proof generation
380    #[must_use]
381    pub fn with_proof(mut self) -> Self {
382        self.proof = true;
383        self
384    }
385
386    /// Set timeout in milliseconds
387    #[must_use]
388    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
389        self.timeout_ms = timeout_ms;
390        self
391    }
392
393    /// Set maximum number of conflicts
394    #[must_use]
395    pub fn with_max_conflicts(mut self, max_conflicts: u64) -> Self {
396        self.max_conflicts = max_conflicts;
397        self
398    }
399
400    /// Set maximum number of decisions
401    #[must_use]
402    pub fn with_max_decisions(mut self, max_decisions: u64) -> Self {
403        self.max_decisions = max_decisions;
404        self
405    }
406
407    /// Enable parallel solving
408    #[must_use]
409    pub fn with_parallel(mut self, num_threads: usize) -> Self {
410        self.parallel = true;
411        self.num_threads = num_threads;
412        self
413    }
414
415    /// Set restart strategy
416    #[must_use]
417    pub fn with_restart_strategy(mut self, strategy: RestartStrategy) -> Self {
418        self.restart_strategy = strategy;
419        self
420    }
421
422    /// Set theory mode
423    #[must_use]
424    pub fn with_theory_mode(mut self, mode: TheoryMode) -> Self {
425        self.theory_mode = mode;
426        self
427    }
428}
429
430/// Solver statistics
431#[derive(Debug, Clone, Default)]
432pub struct Statistics {
433    /// Number of decisions made
434    pub decisions: u64,
435    /// Number of conflicts encountered
436    pub conflicts: u64,
437    /// Number of propagations performed
438    pub propagations: u64,
439    /// Number of restarts performed
440    pub restarts: u64,
441    /// Number of learned clauses
442    pub learned_clauses: u64,
443    /// Number of theory propagations
444    pub theory_propagations: u64,
445    /// Number of theory conflicts
446    pub theory_conflicts: u64,
447}
448
449impl Statistics {
450    /// Create new statistics with all counters set to zero
451    #[must_use]
452    pub fn new() -> Self {
453        Self::default()
454    }
455
456    /// Reset all statistics
457    pub fn reset(&mut self) {
458        *self = Self::default();
459    }
460}
461
462/// A model (assignment to variables)
463#[derive(Debug, Clone)]
464pub struct Model {
465    /// Variable assignments
466    assignments: FxHashMap<TermId, TermId>,
467}
468
469impl Model {
470    /// Create a new empty model
471    #[must_use]
472    pub fn new() -> Self {
473        Self {
474            assignments: FxHashMap::default(),
475        }
476    }
477
478    /// Get the value of a term in the model
479    #[must_use]
480    pub fn get(&self, term: TermId) -> Option<TermId> {
481        self.assignments.get(&term).copied()
482    }
483
484    /// Set a value in the model
485    pub fn set(&mut self, term: TermId, value: TermId) {
486        self.assignments.insert(term, value);
487    }
488
489    /// Minimize the model by removing redundant assignments
490    /// Returns a new minimized model containing only essential assignments
491    pub fn minimize(&self, essential_vars: &[TermId]) -> Model {
492        let mut minimized = Model::new();
493
494        // Only keep assignments for essential variables
495        for &var in essential_vars {
496            if let Some(&value) = self.assignments.get(&var) {
497                minimized.set(var, value);
498            }
499        }
500
501        minimized
502    }
503
504    /// Get the number of assignments in the model
505    #[must_use]
506    pub fn size(&self) -> usize {
507        self.assignments.len()
508    }
509
510    /// Get the assignments map (for MBQI integration)
511    #[must_use]
512    pub fn assignments(&self) -> &FxHashMap<TermId, TermId> {
513        &self.assignments
514    }
515
516    /// Evaluate a term in this model
517    /// Returns the simplified/evaluated term
518    pub fn eval(&self, term: TermId, manager: &mut TermManager) -> TermId {
519        // First check if we have a direct assignment
520        if let Some(val) = self.get(term) {
521            return val;
522        }
523
524        // Otherwise, recursively evaluate based on term structure
525        let Some(t) = manager.get(term).cloned() else {
526            return term;
527        };
528
529        match t.kind {
530            // Constants evaluate to themselves
531            TermKind::True
532            | TermKind::False
533            | TermKind::IntConst(_)
534            | TermKind::RealConst(_)
535            | TermKind::BitVecConst { .. } => term,
536
537            // Variables: look up in model or return the variable itself
538            TermKind::Var(_) => self.get(term).unwrap_or(term),
539
540            // Boolean operations
541            TermKind::Not(arg) => {
542                let arg_val = self.eval(arg, manager);
543                if let Some(t) = manager.get(arg_val) {
544                    match t.kind {
545                        TermKind::True => manager.mk_false(),
546                        TermKind::False => manager.mk_true(),
547                        _ => manager.mk_not(arg_val),
548                    }
549                } else {
550                    manager.mk_not(arg_val)
551                }
552            }
553
554            TermKind::And(ref args) => {
555                let mut eval_args = Vec::new();
556                for &arg in args {
557                    let val = self.eval(arg, manager);
558                    if let Some(t) = manager.get(val) {
559                        if matches!(t.kind, TermKind::False) {
560                            return manager.mk_false();
561                        }
562                        if !matches!(t.kind, TermKind::True) {
563                            eval_args.push(val);
564                        }
565                    } else {
566                        eval_args.push(val);
567                    }
568                }
569                if eval_args.is_empty() {
570                    manager.mk_true()
571                } else if eval_args.len() == 1 {
572                    eval_args[0]
573                } else {
574                    manager.mk_and(eval_args)
575                }
576            }
577
578            TermKind::Or(ref args) => {
579                let mut eval_args = Vec::new();
580                for &arg in args {
581                    let val = self.eval(arg, manager);
582                    if let Some(t) = manager.get(val) {
583                        if matches!(t.kind, TermKind::True) {
584                            return manager.mk_true();
585                        }
586                        if !matches!(t.kind, TermKind::False) {
587                            eval_args.push(val);
588                        }
589                    } else {
590                        eval_args.push(val);
591                    }
592                }
593                if eval_args.is_empty() {
594                    manager.mk_false()
595                } else if eval_args.len() == 1 {
596                    eval_args[0]
597                } else {
598                    manager.mk_or(eval_args)
599                }
600            }
601
602            TermKind::Implies(lhs, rhs) => {
603                let lhs_val = self.eval(lhs, manager);
604                let rhs_val = self.eval(rhs, manager);
605
606                if let Some(t) = manager.get(lhs_val) {
607                    if matches!(t.kind, TermKind::False) {
608                        return manager.mk_true();
609                    }
610                    if matches!(t.kind, TermKind::True) {
611                        return rhs_val;
612                    }
613                }
614
615                if let Some(t) = manager.get(rhs_val)
616                    && matches!(t.kind, TermKind::True)
617                {
618                    return manager.mk_true();
619                }
620
621                manager.mk_implies(lhs_val, rhs_val)
622            }
623
624            TermKind::Ite(cond, then_br, else_br) => {
625                let cond_val = self.eval(cond, manager);
626
627                if let Some(t) = manager.get(cond_val) {
628                    match t.kind {
629                        TermKind::True => return self.eval(then_br, manager),
630                        TermKind::False => return self.eval(else_br, manager),
631                        _ => {}
632                    }
633                }
634
635                let then_val = self.eval(then_br, manager);
636                let else_val = self.eval(else_br, manager);
637                manager.mk_ite(cond_val, then_val, else_val)
638            }
639
640            TermKind::Eq(lhs, rhs) => {
641                let lhs_val = self.eval(lhs, manager);
642                let rhs_val = self.eval(rhs, manager);
643
644                if lhs_val == rhs_val {
645                    return manager.mk_true();
646                }
647
648                // Simplify boolean equalities with constants:
649                // x = true  => x
650                // x = false => NOT x
651                // true = x  => x
652                // false = x => NOT x
653                if let Some(lhs_term) = manager.get(lhs_val)
654                    && lhs_term.sort == manager.sorts.bool_sort
655                {
656                    // Check if rhs is a boolean constant
657                    if let Some(rhs_term) = manager.get(rhs_val) {
658                        match rhs_term.kind {
659                            TermKind::True => return lhs_val,
660                            TermKind::False => return manager.mk_not(lhs_val),
661                            _ => {}
662                        }
663                    }
664                    // Check if lhs is a boolean constant
665                    match lhs_term.kind {
666                        TermKind::True => return rhs_val,
667                        TermKind::False => return manager.mk_not(rhs_val),
668                        _ => {}
669                    }
670                }
671
672                manager.mk_eq(lhs_val, rhs_val)
673            }
674
675            // Arithmetic operations - basic constant folding
676            TermKind::Neg(arg) => {
677                let arg_val = self.eval(arg, manager);
678                if let Some(t) = manager.get(arg_val) {
679                    match &t.kind {
680                        TermKind::IntConst(n) => return manager.mk_int(-n),
681                        TermKind::RealConst(r) => return manager.mk_real(-r),
682                        _ => {}
683                    }
684                }
685                manager.mk_not(arg_val)
686            }
687
688            TermKind::Add(ref args) => {
689                let eval_args: Vec<_> = args.iter().map(|&a| self.eval(a, manager)).collect();
690                manager.mk_add(eval_args)
691            }
692
693            TermKind::Sub(lhs, rhs) => {
694                let lhs_val = self.eval(lhs, manager);
695                let rhs_val = self.eval(rhs, manager);
696                manager.mk_sub(lhs_val, rhs_val)
697            }
698
699            TermKind::Mul(ref args) => {
700                let eval_args: Vec<_> = args.iter().map(|&a| self.eval(a, manager)).collect();
701                manager.mk_mul(eval_args)
702            }
703
704            // For other operations, just return the term or look it up
705            _ => self.get(term).unwrap_or(term),
706        }
707    }
708}
709
710impl Default for Model {
711    fn default() -> Self {
712        Self::new()
713    }
714}
715
716impl Model {
717    /// Pretty print the model in SMT-LIB2 format
718    #[cfg(feature = "std")]
719    pub fn pretty_print(&self, manager: &TermManager) -> String {
720        if self.assignments.is_empty() {
721            return "(model)".to_string();
722        }
723
724        let mut lines = vec!["(model".to_string()];
725        let printer = oxiz_core::smtlib::Printer::new(manager);
726
727        for (&var, &value) in &self.assignments {
728            if let Some(term) = manager.get(var) {
729                // Only print top-level variables, not internal encoding variables
730                if let TermKind::Var(name) = &term.kind {
731                    let sort_str = Self::format_sort(term.sort, manager);
732                    let value_str = printer.print_term(value);
733                    // Use Debug format for the symbol name
734                    let name_str = format!("{:?}", name);
735                    lines.push(format!(
736                        "  (define-fun {} () {} {})",
737                        name_str, sort_str, value_str
738                    ));
739                }
740            }
741        }
742        lines.push(")".to_string());
743        lines.join("\n")
744    }
745
746    /// Format a sort ID to its SMT-LIB2 representation
747    fn format_sort(sort: oxiz_core::sort::SortId, manager: &TermManager) -> String {
748        if sort == manager.sorts.bool_sort {
749            "Bool".to_string()
750        } else if sort == manager.sorts.int_sort {
751            "Int".to_string()
752        } else if sort == manager.sorts.real_sort {
753            "Real".to_string()
754        } else if let Some(s) = manager.sorts.get(sort) {
755            if let Some(w) = s.bitvec_width() {
756                format!("(_ BitVec {})", w)
757            } else {
758                "Unknown".to_string()
759            }
760        } else {
761            "Unknown".to_string()
762        }
763    }
764}
765
766/// A named assertion for unsat core tracking
767#[derive(Debug, Clone)]
768pub struct NamedAssertion {
769    /// The assertion term (kept for potential future use in minimization)
770    #[allow(dead_code)]
771    pub term: TermId,
772    /// The name (if any)
773    pub name: Option<String>,
774    /// Index of this assertion
775    pub index: u32,
776}
777
778/// An unsat core - a minimal set of assertions that are unsatisfiable
779#[derive(Debug, Clone)]
780pub struct UnsatCore {
781    /// The names of assertions in the core
782    pub names: Vec<String>,
783    /// The indices of assertions in the core
784    pub indices: Vec<u32>,
785}
786
787impl UnsatCore {
788    /// Create a new empty unsat core
789    #[must_use]
790    pub fn new() -> Self {
791        Self {
792            names: Vec::new(),
793            indices: Vec::new(),
794        }
795    }
796
797    /// Check if the core is empty
798    #[must_use]
799    pub fn is_empty(&self) -> bool {
800        self.indices.is_empty()
801    }
802
803    /// Get the number of assertions in the core
804    #[must_use]
805    pub fn len(&self) -> usize {
806        self.indices.len()
807    }
808}
809
810impl Default for UnsatCore {
811    fn default() -> Self {
812        Self::new()
813    }
814}
815
816/// Cached FP constraint data for a single assertion term.
817#[derive(Debug, Clone)]
818pub struct FpConstraintData {
819    pub additions: Vec<(TermId, TermId, TermId, TermId, RoundingMode)>,
820    pub divisions: Vec<(TermId, TermId, TermId, TermId, RoundingMode)>,
821    pub multiplications: Vec<(TermId, TermId, TermId, TermId, RoundingMode)>,
822    pub comparisons: Vec<(TermId, TermId, bool)>,
823    pub equalities: Vec<(TermId, TermId)>,
824    pub literals: FxHashMap<TermId, f64>,
825    pub rounding_add_results: FxHashMap<(TermId, TermId, RoundingMode), TermId>,
826    pub is_zero: FxHashSet<TermId>,
827    pub is_positive: FxHashSet<TermId>,
828    pub is_negative: FxHashSet<TermId>,
829    pub not_nan: FxHashSet<TermId>,
830    pub gt_comparisons: Vec<(TermId, TermId)>,
831    pub lt_comparisons: Vec<(TermId, TermId)>,
832    pub conversions: Vec<(TermId, u32, u32, TermId)>,
833    pub real_to_fp_conversions: Vec<(TermId, u32, u32, TermId)>,
834    pub subtractions: Vec<(TermId, TermId, TermId)>,
835}
836
837impl FpConstraintData {
838    #[must_use]
839    pub fn new() -> Self {
840        Self {
841            additions: Vec::new(),
842            divisions: Vec::new(),
843            multiplications: Vec::new(),
844            comparisons: Vec::new(),
845            equalities: Vec::new(),
846            literals: FxHashMap::default(),
847            rounding_add_results: FxHashMap::default(),
848            is_zero: FxHashSet::default(),
849            is_positive: FxHashSet::default(),
850            is_negative: FxHashSet::default(),
851            not_nan: FxHashSet::default(),
852            gt_comparisons: Vec::new(),
853            lt_comparisons: Vec::new(),
854            conversions: Vec::new(),
855            real_to_fp_conversions: Vec::new(),
856            subtractions: Vec::new(),
857        }
858    }
859
860    #[must_use]
861    pub fn is_empty(&self) -> bool {
862        self.additions.is_empty()
863            && self.divisions.is_empty()
864            && self.multiplications.is_empty()
865            && self.comparisons.is_empty()
866            && self.equalities.is_empty()
867    }
868
869    pub fn merge(&mut self, other: &FpConstraintData) {
870        self.additions.extend_from_slice(&other.additions);
871        self.divisions.extend_from_slice(&other.divisions);
872        self.multiplications
873            .extend_from_slice(&other.multiplications);
874        self.comparisons.extend_from_slice(&other.comparisons);
875        self.equalities.extend_from_slice(&other.equalities);
876        for (&k, &v) in &other.literals {
877            self.literals.insert(k, v);
878        }
879        for (&k, &v) in &other.rounding_add_results {
880            self.rounding_add_results.insert(k, v);
881        }
882        self.is_zero.extend(other.is_zero.iter().copied());
883        self.is_positive.extend(other.is_positive.iter().copied());
884        self.is_negative.extend(other.is_negative.iter().copied());
885        self.not_nan.extend(other.not_nan.iter().copied());
886        self.gt_comparisons.extend_from_slice(&other.gt_comparisons);
887        self.lt_comparisons.extend_from_slice(&other.lt_comparisons);
888        self.conversions.extend_from_slice(&other.conversions);
889        self.real_to_fp_conversions
890            .extend_from_slice(&other.real_to_fp_conversions);
891        self.subtractions.extend_from_slice(&other.subtractions);
892    }
893}
894
895impl Default for FpConstraintData {
896    fn default() -> Self {
897        Self::new()
898    }
899}
900
901/// Lazy model evaluation cache.
902#[derive(Debug)]
903pub struct ModelCache {
904    model: Model,
905    eval_cache: FxHashMap<TermId, TermId>,
906    cache_hits: u64,
907    cache_misses: u64,
908}
909
910impl ModelCache {
911    #[must_use]
912    pub fn new(model: Model) -> Self {
913        Self {
914            model,
915            eval_cache: FxHashMap::default(),
916            cache_hits: 0,
917            cache_misses: 0,
918        }
919    }
920
921    #[must_use]
922    pub fn model(&self) -> &Model {
923        &self.model
924    }
925
926    #[must_use]
927    pub fn get_direct(&self, term: TermId) -> Option<TermId> {
928        self.model.get(term)
929    }
930
931    pub fn eval_lazy(&mut self, term: TermId, manager: &mut TermManager) -> TermId {
932        if let Some(&cached) = self.eval_cache.get(&term) {
933            self.cache_hits += 1;
934            return cached;
935        }
936        self.cache_misses += 1;
937        let result = self.model.eval(term, manager);
938        self.eval_cache.insert(term, result);
939        result
940    }
941
942    pub fn eval_batch(
943        &mut self,
944        terms: &[TermId],
945        manager: &mut TermManager,
946    ) -> SmallVec<[TermId; 8]> {
947        terms
948            .iter()
949            .map(|&t| {
950                if let Some(&cached) = self.eval_cache.get(&t) {
951                    self.cache_hits += 1;
952                    cached
953                } else {
954                    self.cache_misses += 1;
955                    let result = self.model.eval(t, manager);
956                    self.eval_cache.insert(t, result);
957                    result
958                }
959            })
960            .collect()
961    }
962
963    pub fn invalidate(&mut self) {
964        self.eval_cache.clear();
965    }
966
967    pub fn invalidate_term(&mut self, term: TermId) {
968        self.eval_cache.remove(&term);
969    }
970
971    #[must_use]
972    pub fn cache_stats(&self) -> (u64, u64) {
973        (self.cache_hits, self.cache_misses)
974    }
975
976    #[must_use]
977    pub fn cache_size(&self) -> usize {
978        self.eval_cache.len()
979    }
980
981    #[must_use]
982    pub fn model_size(&self) -> usize {
983        self.model.size()
984    }
985
986    #[must_use]
987    pub fn is_cached(&self, term: TermId) -> bool {
988        self.eval_cache.contains_key(&term)
989    }
990
991    #[must_use]
992    pub fn into_model(self) -> Model {
993        self.model
994    }
995}