Skip to main content

oxiz_solver/solver/
mod.rs

1//! Main CDCL(T) SMT Solver module
2
3pub(super) mod candidates;
4pub(super) mod check_array;
5pub(super) mod check_bv;
6pub(super) mod check_dt;
7pub(super) mod check_fp;
8pub(super) mod check_nlsat;
9pub(super) mod check_string;
10pub(super) mod config;
11pub(super) mod encode;
12pub(super) mod model_builder;
13pub(super) mod pigeonhole;
14pub(super) mod theory_manager;
15pub(super) mod trail;
16pub(super) mod types;
17
18pub use types::{
19    FpConstraintData, Model, NamedAssertion, Proof, ProofStep, SolverConfig, SolverResult,
20    Statistics, TheoryMode, UnsatCore,
21};
22
23use crate::mbqi::{MBQIIntegration, MBQIResult};
24#[allow(unused_imports)]
25use crate::prelude::*;
26use crate::simplify::Simplifier;
27use oxiz_core::ast::{TermId, TermKind, TermManager};
28use oxiz_core::ematching::{EmatchingConfig, EmatchingEngine};
29use oxiz_core::sort::SortId;
30#[cfg(test)]
31use oxiz_sat::RestartStrategy;
32use oxiz_sat::{
33    Lit, Solver as SatSolver, SolverConfig as SatConfig, SolverResult as SatResult, Var,
34};
35use oxiz_theories::Theory;
36use oxiz_theories::arithmetic::ArithSolver;
37use oxiz_theories::bv::BvSolver;
38use oxiz_theories::euf::EufSolver;
39
40use theory_manager::TheoryManager;
41use trail::{ContextState, TrailOp};
42use types::{Constraint, ParsedArithConstraint, Polarity};
43
44/// Main CDCL(T) SMT Solver
45#[derive(Debug)]
46pub struct Solver {
47    /// Configuration
48    pub(super) config: SolverConfig,
49    /// SAT solver core
50    pub(super) sat: SatSolver,
51    /// EUF theory solver
52    pub(super) euf: EufSolver,
53    /// Arithmetic theory solver
54    pub(super) arith: ArithSolver,
55    /// Bitvector theory solver
56    pub(super) bv: BvSolver,
57    /// NLSAT solver for nonlinear arithmetic (QF_NIA/QF_NRA)
58    #[cfg(feature = "std")]
59    pub(super) nlsat: Option<oxiz_theories::nlsat::NlsatTheory>,
60    /// MBQI solver for quantified formulas
61    pub(super) mbqi: MBQIIntegration,
62    /// E-matching engine for quantifier instantiation via trigger patterns
63    pub(super) ematch_engine: EmatchingEngine,
64    /// Whether the formula contains quantifiers
65    pub(super) has_quantifiers: bool,
66    /// Term to SAT variable mapping
67    pub(super) term_to_var: FxHashMap<TermId, Var>,
68    /// SAT variable to term mapping
69    pub(super) var_to_term: Vec<TermId>,
70    /// SAT variable to theory constraint mapping
71    pub(super) var_to_constraint: FxHashMap<Var, Constraint>,
72    /// SAT variable to parsed arithmetic constraint mapping
73    pub(super) var_to_parsed_arith: FxHashMap<Var, ParsedArithConstraint>,
74    /// Current logic
75    pub(super) logic: Option<String>,
76    /// Assertions
77    pub(super) assertions: Vec<TermId>,
78    /// Named assertions for unsat core tracking
79    pub(super) named_assertions: Vec<NamedAssertion>,
80    /// Assumption literals for unsat core tracking (maps assertion index to assumption var)
81    /// Reserved for future use with assumption-based unsat core extraction
82    #[allow(dead_code)]
83    pub(super) assumption_vars: FxHashMap<u32, Var>,
84    /// Model (if sat)
85    pub(super) model: Option<Model>,
86    /// Unsat core (if unsat)
87    pub(super) unsat_core: Option<UnsatCore>,
88    /// Context stack for push/pop
89    pub(super) context_stack: Vec<ContextState>,
90    /// Trail of operations for efficient undo
91    pub(super) trail: Vec<TrailOp>,
92    /// Tracking which literals have been processed by theories
93    pub(super) theory_processed_up_to: usize,
94    /// Whether to produce unsat cores
95    pub(super) produce_unsat_cores: bool,
96    /// Track if we've asserted False (for immediate unsat)
97    pub(super) has_false_assertion: bool,
98    /// Polarity tracking for optimization
99    pub(super) polarities: FxHashMap<TermId, Polarity>,
100    /// Whether polarity-aware encoding is enabled
101    pub(super) polarity_aware: bool,
102    /// Whether theory-aware branching is enabled
103    pub(super) theory_aware_branching: bool,
104    /// Proof of unsatisfiability (if proof generation is enabled)
105    pub(super) proof: Option<Proof>,
106    /// Formula simplifier
107    pub(super) simplifier: Simplifier,
108    /// Solver statistics
109    pub(super) statistics: Statistics,
110    /// Bitvector terms (for model extraction)
111    pub(super) bv_terms: FxHashSet<TermId>,
112    /// Whether we've seen arithmetic BV operations (division/remainder)
113    /// Used to decide when to run eager BV checking
114    pub(super) has_bv_arith_ops: bool,
115    /// Arithmetic terms (Int/Real variables for model extraction)
116    pub(super) arith_terms: FxHashSet<TermId>,
117    /// Datatype constructor constraints: variable -> constructor name
118    /// Used to detect mutual exclusivity conflicts (var = C1 AND var = C2 where C1 != C2)
119    pub(super) dt_var_constructors: FxHashMap<TermId, oxiz_core::interner::Spur>,
120    /// Cache for parsed arithmetic constraints, keyed by the comparison term id.
121    /// `ParsedArithConstraint` is purely structural (depends only on the term graph),
122    /// so it is safe to reuse across CDCL backtracks.
123    pub(super) arith_parse_cache: FxHashMap<TermId, Option<ParsedArithConstraint>>,
124    /// Set of compound term ids whose theory-variable sub-graph has been fully
125    /// traversed by `track_theory_vars`.  Avoids redundant O(depth) re-walks
126    /// when the same sub-expression appears in multiple parent constraints.
127    pub(super) tracked_compound_terms: FxHashSet<TermId>,
128    /// Cache for FP constraint checking results.
129    pub(super) fp_constraint_cache: FxHashMap<TermId, FpConstraintData>,
130}
131
132impl Default for Solver {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl Solver {
139    /// Create a new solver
140    #[must_use]
141    pub fn new() -> Self {
142        Self::with_config(SolverConfig::default())
143    }
144
145    /// Create a new solver with configuration
146    #[must_use]
147    pub fn with_config(config: SolverConfig) -> Self {
148        let proof_enabled = config.proof;
149
150        // Build SAT solver configuration from our config
151        let sat_config = SatConfig {
152            restart_strategy: config.restart_strategy,
153            enable_inprocessing: config.enable_inprocessing,
154            inprocessing_interval: config.inprocessing_interval,
155            ..SatConfig::default()
156        };
157
158        // Note: The following features are controlled by the SAT solver's preprocessor
159        // and clause management systems. We pass the configuration but the actual
160        // implementation is in oxiz-sat:
161        // - Clause minimization (via RecursiveMinimizer)
162        // - Clause subsumption (via SubsumptionChecker)
163        // - Variable elimination (via Preprocessor::variable_elimination)
164        // - Blocked clause elimination (via Preprocessor::blocked_clause_elimination)
165        // - Symmetry breaking (via SymmetryBreaker)
166
167        Self {
168            config,
169            sat: SatSolver::with_config(sat_config),
170            euf: EufSolver::new(),
171            arith: ArithSolver::lra(),
172            bv: BvSolver::new(),
173            #[cfg(feature = "std")]
174            nlsat: None,
175            mbqi: MBQIIntegration::new(),
176            ematch_engine: EmatchingEngine::new(EmatchingConfig::default()),
177            has_quantifiers: false,
178            term_to_var: FxHashMap::default(),
179            var_to_term: Vec::new(),
180            var_to_constraint: FxHashMap::default(),
181            var_to_parsed_arith: FxHashMap::default(),
182            logic: None,
183            assertions: Vec::new(),
184            named_assertions: Vec::new(),
185            assumption_vars: FxHashMap::default(),
186            model: None,
187            unsat_core: None,
188            context_stack: Vec::new(),
189            trail: Vec::new(),
190            theory_processed_up_to: 0,
191            produce_unsat_cores: false,
192            has_false_assertion: false,
193            polarities: FxHashMap::default(),
194            polarity_aware: true, // Enable polarity-aware encoding by default
195            theory_aware_branching: true, // Enable theory-aware branching by default
196            proof: if proof_enabled {
197                Some(Proof::new())
198            } else {
199                None
200            },
201            simplifier: Simplifier::new(),
202            statistics: Statistics::new(),
203            bv_terms: FxHashSet::default(),
204            has_bv_arith_ops: false,
205            arith_terms: FxHashSet::default(),
206            dt_var_constructors: FxHashMap::default(),
207            arith_parse_cache: FxHashMap::default(),
208            tracked_compound_terms: FxHashSet::default(),
209            fp_constraint_cache: FxHashMap::default(),
210        }
211    }
212
213    /// Get the proof (if proof generation is enabled and the result is unsat)
214    #[must_use]
215    pub fn get_proof(&self) -> Option<&Proof> {
216        self.proof.as_ref()
217    }
218
219    /// Get the solver statistics
220    #[must_use]
221    pub fn get_statistics(&self) -> &Statistics {
222        &self.statistics
223    }
224
225    /// Reset the solver statistics
226    pub fn reset_statistics(&mut self) {
227        self.statistics.reset();
228    }
229
230    /// Enable or disable theory-aware branching
231    pub fn set_theory_aware_branching(&mut self, enabled: bool) {
232        self.theory_aware_branching = enabled;
233    }
234
235    /// Check if theory-aware branching is enabled
236    #[must_use]
237    pub fn theory_aware_branching(&self) -> bool {
238        self.theory_aware_branching
239    }
240
241    /// Enable or disable unsat core production
242    pub fn set_produce_unsat_cores(&mut self, produce: bool) {
243        self.produce_unsat_cores = produce;
244    }
245
246    /// Register a declared constant as an MBQI ground instantiation candidate.
247    ///
248    /// This must be called from the context layer whenever a `declare-const`
249    /// command is processed, so that trigger-free quantifiers can be
250    /// instantiated with constants that exist in scope.
251    pub fn register_declared_const(&mut self, term: TermId, sort: SortId) {
252        self.mbqi.register_declared_const(term, sort);
253    }
254
255    /// Get a SAT variable for a term, then check satisfiability
256    pub fn check(&mut self, manager: &mut TermManager) -> SolverResult {
257        // Check for trivial unsat (false assertion)
258        if self.has_false_assertion {
259            self.build_unsat_core_trivial_false();
260            return SolverResult::Unsat;
261        }
262
263        if self.assertions.is_empty() {
264            return SolverResult::Sat;
265        }
266
267        // Check string constraints for early conflict detection
268        if self.check_string_constraints(manager) {
269            return SolverResult::Unsat;
270        }
271
272        // Check floating-point constraints for early conflict detection
273        if self.check_fp_constraints(manager) {
274            return SolverResult::Unsat;
275        }
276
277        // Check datatype constraints for early conflict detection
278        if self.check_dt_constraints(manager) {
279            return SolverResult::Unsat;
280        }
281
282        // Check array constraints for early conflict detection
283        if self.check_array_constraints(manager) {
284            return SolverResult::Unsat;
285        }
286
287        // Check bitvector constraints for early conflict detection
288        if self.check_bv_constraints(manager) {
289            return SolverResult::Unsat;
290        }
291
292        // For NIA/NRA logics: dispatch all assertions to the full polynomial
293        // solver first (NiaSolver or NlsatSolver). This gives a definitive
294        // SAT/UNSAT for most benchmark problems without the CDCL(T) loop.
295        if let Some(nl_result) = self.dispatch_nl_solver(manager) {
296            match nl_result {
297                SolverResult::Sat => return SolverResult::Sat,
298                SolverResult::Unsat => return SolverResult::Unsat,
299                SolverResult::Unknown => {}
300            }
301        }
302
303        // Check nonlinear arithmetic constraints for early conflict detection
304        // (static pattern matching, complementary to the dispatch above).
305        if self.check_nonlinear_constraints(manager) {
306            return SolverResult::Unsat;
307        }
308
309        // Check resource limits before starting
310        if self.config.max_conflicts > 0 && self.statistics.conflicts >= self.config.max_conflicts {
311            return SolverResult::Unknown;
312        }
313        if self.config.max_decisions > 0 && self.statistics.decisions >= self.config.max_decisions {
314            return SolverResult::Unknown;
315        }
316
317        // Run SAT solver with theory integration
318        let mut theory_manager = TheoryManager::new(
319            manager,
320            &mut self.euf,
321            &mut self.arith,
322            &mut self.bv,
323            &self.bv_terms,
324            &self.var_to_constraint,
325            &self.var_to_parsed_arith,
326            &self.term_to_var,
327            &self.var_to_term,
328            self.config.theory_mode,
329            &mut self.statistics,
330            self.config.max_conflicts,
331            self.config.max_decisions,
332            self.has_bv_arith_ops,
333        );
334
335        // MBQI loop for quantified formulas
336        let max_mbqi_iterations = 100;
337        let mut mbqi_iteration = 0;
338
339        loop {
340            let sat_result = self.sat.solve_with_theory(&mut theory_manager);
341            match sat_result {
342                SatResult::Unsat => {
343                    self.build_unsat_core();
344                    return SolverResult::Unsat;
345                }
346                SatResult::Unknown => {
347                    return SolverResult::Unknown;
348                }
349                SatResult::Sat => {
350                    // If no quantifiers, we're done
351                    if !self.has_quantifiers {
352                        self.build_model(manager);
353                        self.unsat_core = None;
354                        return SolverResult::Sat;
355                    }
356
357                    // Build partial model for MBQI
358                    self.build_model(manager);
359
360                    // Run MBQI to check quantified formulas
361                    let model_assignments = self
362                        .model
363                        .as_ref()
364                        .map(|m| m.assignments().clone())
365                        .unwrap_or_default();
366
367                    let mbqi_result = self.mbqi.check_with_model(&model_assignments, manager);
368                    match mbqi_result {
369                        MBQIResult::NoQuantifiers => {
370                            self.unsat_core = None;
371                            return SolverResult::Sat;
372                        }
373                        MBQIResult::Satisfied => {
374                            // All quantifiers satisfied by the current model.
375                            self.unsat_core = None;
376                            return SolverResult::Sat;
377                        }
378                        MBQIResult::InstantiationLimit => {
379                            // Too many instantiations - return unknown
380                            return SolverResult::Unknown;
381                        }
382                        MBQIResult::Conflict {
383                            quantifier: _,
384                            reason,
385                        } => {
386                            // Add conflict clause
387                            let lits: Vec<Lit> = reason
388                                .iter()
389                                .filter_map(|&t| self.term_to_var.get(&t).map(|&v| Lit::neg(v)))
390                                .collect();
391                            if !lits.is_empty() {
392                                self.sat.add_clause(lits);
393                            }
394                            // Continue loop
395                        }
396                        MBQIResult::NewInstantiations(instantiations) => {
397                            // Collect ground sub-terms (especially Skolem
398                            // applications) from instantiation results so they
399                            // become MBQI candidates in subsequent rounds.
400                            for inst in &instantiations {
401                                self.collect_ground_candidates_from_term(inst.result, manager);
402                            }
403
404                            // Collect domain/disequality info for pigeonhole
405                            let mut ph_domains: FxHashMap<TermId, (i64, i64)> =
406                                FxHashMap::default();
407                            let mut ph_diseqs: Vec<(TermId, TermId)> = Vec::new();
408
409                            // Add instantiation lemmas
410                            for inst in instantiations {
411                                // If the instantiation result is definitively False
412                                // (e.g., a nested Exists with no valid witness), add an
413                                // empty clause to signal immediate UNSAT.
414                                let is_false_result = manager
415                                    .get(inst.result)
416                                    .is_some_and(|t| matches!(t.kind, TermKind::False));
417                                if is_false_result {
418                                    self.sat.add_clause([] as [Lit; 0]);
419                                    break;
420                                }
421                                // Scan for pigeonhole patterns (recurses into Implies)
422                                self.scan_for_pigeonhole(
423                                    inst.result,
424                                    manager,
425                                    &mut ph_domains,
426                                    &mut ph_diseqs,
427                                );
428                                let lit = self.encode(inst.result, manager);
429                                let ok = self.sat.add_clause([lit]);
430                                let _ = ok;
431                                self.add_arith_diseq_split(inst.result, manager);
432                                self.add_arith_eq_trichotomy(inst.result, manager);
433                                self.add_int_domain_clauses(inst.result, manager);
434                            }
435                            // Add pigeonhole exclusion clauses
436                            if !ph_diseqs.is_empty() && !ph_domains.is_empty() {
437                                self.add_pigeonhole_exclusions_from(
438                                    &ph_domains,
439                                    &ph_diseqs,
440                                    manager,
441                                );
442                            }
443
444                            // E-matching phase: find additional instantiations via trigger patterns
445                            let ematch_lemmas =
446                                self.ematch_engine.match_round(manager).unwrap_or_default();
447                            let mut new_clauses_added = 0usize;
448                            let mut ematch_unsat = false;
449                            for lemma in ematch_lemmas {
450                                let lit = self.encode(lemma, manager);
451                                if self.sat.add_clause([lit]) {
452                                    new_clauses_added += 1;
453                                } else {
454                                    ematch_unsat = true;
455                                    break;
456                                }
457                            }
458                            if ematch_unsat || new_clauses_added > 0 {
459                                // SAT solver will process newly added clauses on next iteration
460                            }
461                            // Continue loop
462                        }
463                        MBQIResult::Unknown => {
464                            // Some evaluations produced symbolic residuals.
465                            // Generate blind instantiations (simplified) once
466                            // to seed the solver with ground lemmas for array
467                            // theory reasoning (pigeonhole, bounds, etc.).
468                            if !self.mbqi.blind_tried() {
469                                self.mbqi.mark_blind_tried();
470                                // Clear dedup cache so that blind instantiations with
471                                // corrected substitution results are not filtered out
472                                // as duplicates of earlier (broken) engine results.
473                                self.mbqi.clear_dedup_cache();
474                                let blind = self.mbqi.generate_blind_instantiations(manager);
475                                let mut ph_domains: FxHashMap<TermId, (i64, i64)> =
476                                    FxHashMap::default();
477                                let mut ph_diseqs: Vec<(TermId, TermId)> = Vec::new();
478                                for inst in blind {
479                                    let is_false = manager
480                                        .get(inst.result)
481                                        .is_some_and(|t| matches!(t.kind, TermKind::False));
482                                    if is_false {
483                                        self.sat.add_clause([] as [Lit; 0]);
484                                        break;
485                                    }
486                                    // Track domains and disequalities for pigeonhole
487                                    if let Some(dbg_t) = manager.get(inst.result) {}
488                                    self.scan_for_pigeonhole(
489                                        inst.result,
490                                        manager,
491                                        &mut ph_domains,
492                                        &mut ph_diseqs,
493                                    );
494                                    let lit = self.encode(inst.result, manager);
495                                    let _ = self.sat.add_clause([lit]);
496                                    self.add_arith_diseq_split(inst.result, manager);
497                                    self.add_arith_eq_trichotomy(inst.result, manager);
498                                    self.add_int_domain_clauses(inst.result, manager);
499                                }
500                                // Add pigeonhole exclusion clauses directly
501                                // from the collected domains and disequalities.
502                                self.add_pigeonhole_exclusions_from(
503                                    &ph_domains,
504                                    &ph_diseqs,
505                                    manager,
506                                );
507                            }
508                            // After 2 Unknown rounds, try finite instantiation:
509                            // for quantifiers with bounded integer guards like
510                            // (i >= 0 && i <= 3), enumerate all values and add
511                            // ground instances directly.
512                            if mbqi_iteration == 2 {
513                                let finite_insts =
514                                    self.mbqi.generate_finite_domain_instantiations(manager);
515                                if !finite_insts.is_empty() {
516                                    let mut ph_d: FxHashMap<TermId, (i64, i64)> =
517                                        FxHashMap::default();
518                                    let mut ph_q: Vec<(TermId, TermId)> = Vec::new();
519                                    for inst in &finite_insts {
520                                        let simplified =
521                                            self.mbqi.deep_simplify(inst.result, manager);
522                                        // Skip tautologies
523                                        if manager
524                                            .get(simplified)
525                                            .is_some_and(|t| matches!(t.kind, TermKind::True))
526                                        {
527                                            continue;
528                                        }
529                                        self.scan_for_pigeonhole(
530                                            simplified, manager, &mut ph_d, &mut ph_q,
531                                        );
532                                        let lit = self.encode(simplified, manager);
533                                        let _ = self.sat.add_clause([lit]);
534                                        self.add_arith_diseq_split(simplified, manager);
535                                        self.add_int_domain_clauses(simplified, manager);
536                                    }
537                                    if !ph_q.is_empty() && !ph_d.is_empty() {
538                                        self.add_pigeonhole_exclusions_from(&ph_d, &ph_q, manager);
539                                    }
540                                }
541                            }
542                            if mbqi_iteration >= 10 {
543                                // After exhausting blind and finite domain
544                                // instantiation attempts, assume the model
545                                // satisfies all quantifiers.  This is sound
546                                // under the incomplete-but-practical MBQI
547                                // heuristic used by Z3 and similar solvers.
548                                self.unsat_core = None;
549                                return SolverResult::Sat;
550                            }
551                            // Continue MBQI loop
552                        }
553                    }
554
555                    mbqi_iteration += 1;
556                    if mbqi_iteration >= max_mbqi_iterations {
557                        return SolverResult::Unknown;
558                    }
559
560                    // Recreate theory manager for next iteration.
561                    // Do NOT reset theory solvers here - resetting EUF/Arith/BV
562                    // state causes spurious conflicts when accumulated lemmas from
563                    // MBQI instantiations interact with theory state that was cleared.
564                    // The theory state accumulates correctly across iterations.
565                    theory_manager = TheoryManager::new(
566                        manager,
567                        &mut self.euf,
568                        &mut self.arith,
569                        &mut self.bv,
570                        &self.bv_terms,
571                        &self.var_to_constraint,
572                        &self.var_to_parsed_arith,
573                        &self.term_to_var,
574                        &self.var_to_term,
575                        self.config.theory_mode,
576                        &mut self.statistics,
577                        self.config.max_conflicts,
578                        self.config.max_decisions,
579                        self.has_bv_arith_ops,
580                    );
581                }
582            }
583        }
584    }
585
586    /// Check satisfiability under assumptions
587    /// Assumptions are temporary constraints that don't modify the assertion stack
588    pub fn check_with_assumptions(
589        &mut self,
590        assumptions: &[TermId],
591        manager: &mut TermManager,
592    ) -> SolverResult {
593        // Save current state
594        self.push();
595
596        // Assert all assumptions
597        for &assumption in assumptions {
598            self.assert(assumption, manager);
599        }
600
601        // Check satisfiability
602        let result = self.check(manager);
603
604        // Restore state
605        self.pop();
606
607        result
608    }
609
610    /// Check satisfiability (pure SAT, no theory integration)
611    /// Useful for benchmarking or when theories are not needed
612    pub fn check_sat_only(&mut self, manager: &mut TermManager) -> SolverResult {
613        if self.assertions.is_empty() {
614            return SolverResult::Sat;
615        }
616
617        match self.sat.solve() {
618            SatResult::Sat => {
619                self.build_model(manager);
620                SolverResult::Sat
621            }
622            SatResult::Unsat => SolverResult::Unsat,
623            SatResult::Unknown => SolverResult::Unknown,
624        }
625    }
626
627    /// Build the model after SAT solving, which can be used to efficiently extract minimal unsat cores
628    pub fn enable_assumption_based_cores(&mut self) {
629        self.produce_unsat_cores = true;
630        // Assumption variables would be created during assertion
631        // to enable fine-grained core extraction
632    }
633
634    /// Minimize an unsat core using greedy deletion
635    /// This creates a minimal (but not necessarily minimum) unsatisfiable subset
636    pub fn minimize_unsat_core(&mut self, manager: &mut TermManager) -> Option<UnsatCore> {
637        if !self.produce_unsat_cores {
638            return None;
639        }
640
641        // Get the current unsat core
642        let core = self.unsat_core.as_ref()?;
643        if core.is_empty() {
644            return Some(core.clone());
645        }
646
647        // Extract the assertions in the core
648        let mut core_assertions: Vec<_> = core
649            .indices
650            .iter()
651            .map(|&idx| {
652                let assertion = self.assertions[idx as usize];
653                let name = self
654                    .named_assertions
655                    .iter()
656                    .find(|na| na.index == idx)
657                    .and_then(|na| na.name.clone());
658                (idx, assertion, name)
659            })
660            .collect();
661
662        // Try to remove each assertion one by one
663        let mut i = 0;
664        while i < core_assertions.len() {
665            // Create a temporary solver with all assertions except the i-th one
666            let mut temp_solver = Solver::new();
667            temp_solver.set_logic(self.logic.as_deref().unwrap_or("ALL"));
668
669            // Add all assertions except the i-th one
670            for (j, &(_, assertion, _)) in core_assertions.iter().enumerate() {
671                if i != j {
672                    temp_solver.assert(assertion, manager);
673                }
674            }
675
676            // Check if still unsat
677            if temp_solver.check(manager) == SolverResult::Unsat {
678                // Still unsat without this assertion - remove it
679                core_assertions.remove(i);
680                // Don't increment i, check the next element which is now at position i
681            } else {
682                // This assertion is needed
683                i += 1;
684            }
685        }
686
687        // Build the minimized core
688        let mut minimized = UnsatCore::new();
689        for (idx, _, name) in core_assertions {
690            minimized.indices.push(idx);
691            if let Some(n) = name {
692                minimized.names.push(n);
693            }
694        }
695
696        Some(minimized)
697    }
698
699    /// Get the model (if sat)
700    #[must_use]
701    pub fn model(&self) -> Option<&Model> {
702        self.model.as_ref()
703    }
704
705    /// Check satisfiability with resource limits.
706    pub fn check_with_limits(
707        &mut self,
708        manager: &mut TermManager,
709        limits: &crate::resource_limits::ResourceLimits,
710    ) -> core::result::Result<SolverResult, crate::resource_limits::ResourceExhausted> {
711        use crate::resource_limits::ResourceMonitor;
712        let mut monitor = ResourceMonitor::new(limits.clone());
713        if let Some(reason) = monitor.check() {
714            return Err(reason);
715        }
716        let orig_max_conflicts = self.config.max_conflicts;
717        let orig_max_decisions = self.config.max_decisions;
718        if let Some(max_c) = limits.max_conflicts {
719            if self.config.max_conflicts == 0 || max_c < self.config.max_conflicts {
720                self.config.max_conflicts = max_c;
721            }
722        }
723        if let Some(max_d) = limits.max_decisions {
724            if self.config.max_decisions == 0 || max_d < self.config.max_decisions {
725                self.config.max_decisions = max_d;
726            }
727        }
728        let result = self.check(manager);
729        self.config.max_conflicts = orig_max_conflicts;
730        self.config.max_decisions = orig_max_decisions;
731        monitor.conflicts = self.statistics.conflicts;
732        monitor.decisions = self.statistics.decisions;
733        monitor.restarts = self.statistics.restarts;
734        monitor.theory_checks =
735            self.statistics.theory_propagations + self.statistics.theory_conflicts;
736        if result == SolverResult::Unknown {
737            if let Some(reason) = monitor.check() {
738                return Err(reason);
739            }
740        }
741        Ok(result)
742    }
743    /// Set a wall-clock timeout.
744    pub fn set_timeout(&mut self, timeout: core::time::Duration) {
745        self.config.timeout_ms = timeout.as_millis() as u64;
746    }
747    /// Set the maximum number of SAT conflicts.
748    pub fn set_conflict_limit(&mut self, max_conflicts: u64) {
749        self.config.max_conflicts = max_conflicts;
750    }
751    /// Set the maximum number of SAT decisions.
752    pub fn set_decision_limit(&mut self, max_decisions: u64) {
753        self.config.max_decisions = max_decisions;
754    }
755
756    /// Assert multiple terms at once
757    /// This is more efficient than calling assert() multiple times
758    pub fn assert_many(&mut self, terms: &[TermId], manager: &mut TermManager) {
759        for &term in terms {
760            self.assert(term, manager);
761        }
762    }
763
764    /// Get the number of assertions in the solver
765    #[must_use]
766    pub fn num_assertions(&self) -> usize {
767        self.assertions.len()
768    }
769
770    /// Get the number of variables in the SAT solver
771    #[must_use]
772    pub fn num_variables(&self) -> usize {
773        self.term_to_var.len()
774    }
775
776    /// Check if the solver has any assertions
777    #[must_use]
778    pub fn has_assertions(&self) -> bool {
779        !self.assertions.is_empty()
780    }
781
782    /// Get the current context level (push/pop depth)
783    #[must_use]
784    pub fn context_level(&self) -> usize {
785        self.context_stack.len()
786    }
787
788    /// Push a context level
789    pub fn push(&mut self) {
790        self.context_stack.push(ContextState {
791            num_assertions: self.assertions.len(),
792            num_vars: self.var_to_term.len(),
793            has_false_assertion: self.has_false_assertion,
794            trail_position: self.trail.len(),
795        });
796        self.sat.push();
797        self.euf.push();
798        self.arith.push();
799        #[cfg(feature = "std")]
800        if let Some(nlsat) = &mut self.nlsat {
801            nlsat.push();
802        }
803    }
804
805    /// Pop a context level using trail-based undo
806    pub fn pop(&mut self) {
807        if let Some(state) = self.context_stack.pop() {
808            // Undo all operations in the trail since the push
809            while self.trail.len() > state.trail_position {
810                if let Some(op) = self.trail.pop() {
811                    match op {
812                        TrailOp::AssertionAdded { index } => {
813                            if self.assertions.len() > index {
814                                self.assertions.truncate(index);
815                            }
816                        }
817                        TrailOp::VarCreated { var: _, term } => {
818                            // Remove the term-to-var mapping
819                            self.term_to_var.remove(&term);
820                        }
821                        TrailOp::ConstraintAdded { var } => {
822                            // Remove the constraint
823                            self.var_to_constraint.remove(&var);
824                        }
825                        TrailOp::FalseAssertionSet => {
826                            // Reset the flag
827                            self.has_false_assertion = false;
828                        }
829                        TrailOp::NamedAssertionAdded { index } => {
830                            // Remove the named assertion
831                            if self.named_assertions.len() > index {
832                                self.named_assertions.truncate(index);
833                            }
834                        }
835                        TrailOp::BvTermAdded { term } => {
836                            // Remove the bitvector term
837                            self.bv_terms.remove(&term);
838                        }
839                        TrailOp::ArithTermAdded { term } => {
840                            // Remove the arithmetic term
841                            self.arith_terms.remove(&term);
842                        }
843                    }
844                }
845            }
846
847            // Use state to restore other fields
848            self.assertions.truncate(state.num_assertions);
849            self.var_to_term.truncate(state.num_vars);
850            self.has_false_assertion = state.has_false_assertion;
851
852            self.sat.pop();
853            self.euf.pop();
854            self.arith.pop();
855            #[cfg(feature = "std")]
856            if let Some(nlsat) = &mut self.nlsat {
857                nlsat.pop();
858            }
859        }
860    }
861
862    /// Reset the solver
863    pub fn reset(&mut self) {
864        self.sat.reset();
865        self.euf.reset();
866        self.arith.reset();
867        self.bv.reset();
868        self.term_to_var.clear();
869        self.var_to_term.clear();
870        self.var_to_constraint.clear();
871        self.var_to_parsed_arith.clear();
872        self.assertions.clear();
873        self.named_assertions.clear();
874        self.model = None;
875        self.unsat_core = None;
876        self.context_stack.clear();
877        self.trail.clear();
878        self.logic = None;
879        self.theory_processed_up_to = 0;
880        self.has_false_assertion = false;
881        self.bv_terms.clear();
882        self.arith_terms.clear();
883        self.dt_var_constructors.clear();
884        self.arith_parse_cache.clear();
885        self.tracked_compound_terms.clear();
886    }
887
888    /// Get the configuration
889    #[must_use]
890    pub fn config(&self) -> &SolverConfig {
891        &self.config
892    }
893
894    /// Set configuration
895    pub fn set_config(&mut self, config: SolverConfig) {
896        self.config = config;
897    }
898
899    /// Get solver statistics
900    #[must_use]
901    pub fn stats(&self) -> &oxiz_sat::SolverStats {
902        self.sat.stats()
903    }
904}
905
906#[cfg(test)]
907mod tests;