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        // Check nonlinear arithmetic constraints for early conflict detection
293        if self.check_nonlinear_constraints(manager) {
294            return SolverResult::Unsat;
295        }
296
297        // Check resource limits before starting
298        if self.config.max_conflicts > 0 && self.statistics.conflicts >= self.config.max_conflicts {
299            return SolverResult::Unknown;
300        }
301        if self.config.max_decisions > 0 && self.statistics.decisions >= self.config.max_decisions {
302            return SolverResult::Unknown;
303        }
304
305        // Run SAT solver with theory integration
306        let mut theory_manager = TheoryManager::new(
307            manager,
308            &mut self.euf,
309            &mut self.arith,
310            &mut self.bv,
311            &self.bv_terms,
312            &self.var_to_constraint,
313            &self.var_to_parsed_arith,
314            &self.term_to_var,
315            &self.var_to_term,
316            self.config.theory_mode,
317            &mut self.statistics,
318            self.config.max_conflicts,
319            self.config.max_decisions,
320            self.has_bv_arith_ops,
321        );
322
323        // MBQI loop for quantified formulas
324        let max_mbqi_iterations = 100;
325        let mut mbqi_iteration = 0;
326
327        loop {
328            let sat_result = self.sat.solve_with_theory(&mut theory_manager);
329            match sat_result {
330                SatResult::Unsat => {
331                    self.build_unsat_core();
332                    return SolverResult::Unsat;
333                }
334                SatResult::Unknown => {
335                    return SolverResult::Unknown;
336                }
337                SatResult::Sat => {
338                    // If no quantifiers, we're done
339                    if !self.has_quantifiers {
340                        self.build_model(manager);
341                        self.unsat_core = None;
342                        return SolverResult::Sat;
343                    }
344
345                    // Build partial model for MBQI
346                    self.build_model(manager);
347
348                    // Run MBQI to check quantified formulas
349                    let model_assignments = self
350                        .model
351                        .as_ref()
352                        .map(|m| m.assignments().clone())
353                        .unwrap_or_default();
354
355                    let mbqi_result = self.mbqi.check_with_model(&model_assignments, manager);
356                    match mbqi_result {
357                        MBQIResult::NoQuantifiers => {
358                            self.unsat_core = None;
359                            return SolverResult::Sat;
360                        }
361                        MBQIResult::Satisfied => {
362                            // All quantifiers satisfied by the current model.
363                            self.unsat_core = None;
364                            return SolverResult::Sat;
365                        }
366                        MBQIResult::InstantiationLimit => {
367                            // Too many instantiations - return unknown
368                            return SolverResult::Unknown;
369                        }
370                        MBQIResult::Conflict {
371                            quantifier: _,
372                            reason,
373                        } => {
374                            // Add conflict clause
375                            let lits: Vec<Lit> = reason
376                                .iter()
377                                .filter_map(|&t| self.term_to_var.get(&t).map(|&v| Lit::neg(v)))
378                                .collect();
379                            if !lits.is_empty() {
380                                self.sat.add_clause(lits);
381                            }
382                            // Continue loop
383                        }
384                        MBQIResult::NewInstantiations(instantiations) => {
385                            // Collect ground sub-terms (especially Skolem
386                            // applications) from instantiation results so they
387                            // become MBQI candidates in subsequent rounds.
388                            for inst in &instantiations {
389                                self.collect_ground_candidates_from_term(inst.result, manager);
390                            }
391
392                            // Collect domain/disequality info for pigeonhole
393                            let mut ph_domains: FxHashMap<TermId, (i64, i64)> =
394                                FxHashMap::default();
395                            let mut ph_diseqs: Vec<(TermId, TermId)> = Vec::new();
396
397                            // Add instantiation lemmas
398                            for inst in instantiations {
399                                // If the instantiation result is definitively False
400                                // (e.g., a nested Exists with no valid witness), add an
401                                // empty clause to signal immediate UNSAT.
402                                let is_false_result = manager
403                                    .get(inst.result)
404                                    .is_some_and(|t| matches!(t.kind, TermKind::False));
405                                if is_false_result {
406                                    self.sat.add_clause([] as [Lit; 0]);
407                                    break;
408                                }
409                                // Scan for pigeonhole patterns (recurses into Implies)
410                                self.scan_for_pigeonhole(
411                                    inst.result,
412                                    manager,
413                                    &mut ph_domains,
414                                    &mut ph_diseqs,
415                                );
416                                let lit = self.encode(inst.result, manager);
417                                let ok = self.sat.add_clause([lit]);
418                                let _ = ok;
419                                self.add_arith_diseq_split(inst.result, manager);
420                                self.add_arith_eq_trichotomy(inst.result, manager);
421                                self.add_int_domain_clauses(inst.result, manager);
422                            }
423                            // Add pigeonhole exclusion clauses
424                            if !ph_diseqs.is_empty() && !ph_domains.is_empty() {
425                                self.add_pigeonhole_exclusions_from(
426                                    &ph_domains,
427                                    &ph_diseqs,
428                                    manager,
429                                );
430                            }
431
432                            // E-matching phase: find additional instantiations via trigger patterns
433                            let ematch_lemmas =
434                                self.ematch_engine.match_round(manager).unwrap_or_default();
435                            let mut new_clauses_added = 0usize;
436                            let mut ematch_unsat = false;
437                            for lemma in ematch_lemmas {
438                                let lit = self.encode(lemma, manager);
439                                if self.sat.add_clause([lit]) {
440                                    new_clauses_added += 1;
441                                } else {
442                                    ematch_unsat = true;
443                                    break;
444                                }
445                            }
446                            if ematch_unsat || new_clauses_added > 0 {
447                                // SAT solver will process newly added clauses on next iteration
448                            }
449                            // Continue loop
450                        }
451                        MBQIResult::Unknown => {
452                            // Some evaluations produced symbolic residuals.
453                            // Generate blind instantiations (simplified) once
454                            // to seed the solver with ground lemmas for array
455                            // theory reasoning (pigeonhole, bounds, etc.).
456                            if !self.mbqi.blind_tried() {
457                                self.mbqi.mark_blind_tried();
458                                // Clear dedup cache so that blind instantiations with
459                                // corrected substitution results are not filtered out
460                                // as duplicates of earlier (broken) engine results.
461                                self.mbqi.clear_dedup_cache();
462                                let blind = self.mbqi.generate_blind_instantiations(manager);
463                                let mut ph_domains: FxHashMap<TermId, (i64, i64)> =
464                                    FxHashMap::default();
465                                let mut ph_diseqs: Vec<(TermId, TermId)> = Vec::new();
466                                for inst in blind {
467                                    let is_false = manager
468                                        .get(inst.result)
469                                        .is_some_and(|t| matches!(t.kind, TermKind::False));
470                                    if is_false {
471                                        self.sat.add_clause([] as [Lit; 0]);
472                                        break;
473                                    }
474                                    // Track domains and disequalities for pigeonhole
475                                    if let Some(dbg_t) = manager.get(inst.result) {}
476                                    self.scan_for_pigeonhole(
477                                        inst.result,
478                                        manager,
479                                        &mut ph_domains,
480                                        &mut ph_diseqs,
481                                    );
482                                    let lit = self.encode(inst.result, manager);
483                                    let _ = self.sat.add_clause([lit]);
484                                    self.add_arith_diseq_split(inst.result, manager);
485                                    self.add_arith_eq_trichotomy(inst.result, manager);
486                                    self.add_int_domain_clauses(inst.result, manager);
487                                }
488                                // Add pigeonhole exclusion clauses directly
489                                // from the collected domains and disequalities.
490                                self.add_pigeonhole_exclusions_from(
491                                    &ph_domains,
492                                    &ph_diseqs,
493                                    manager,
494                                );
495                            }
496                            // After 2 Unknown rounds, try finite instantiation:
497                            // for quantifiers with bounded integer guards like
498                            // (i >= 0 && i <= 3), enumerate all values and add
499                            // ground instances directly.
500                            if mbqi_iteration == 2 {
501                                let finite_insts =
502                                    self.mbqi.generate_finite_domain_instantiations(manager);
503                                if !finite_insts.is_empty() {
504                                    let mut ph_d: FxHashMap<TermId, (i64, i64)> =
505                                        FxHashMap::default();
506                                    let mut ph_q: Vec<(TermId, TermId)> = Vec::new();
507                                    for inst in &finite_insts {
508                                        let simplified =
509                                            self.mbqi.deep_simplify(inst.result, manager);
510                                        // Skip tautologies
511                                        if manager
512                                            .get(simplified)
513                                            .is_some_and(|t| matches!(t.kind, TermKind::True))
514                                        {
515                                            continue;
516                                        }
517                                        self.scan_for_pigeonhole(
518                                            simplified, manager, &mut ph_d, &mut ph_q,
519                                        );
520                                        let lit = self.encode(simplified, manager);
521                                        let _ = self.sat.add_clause([lit]);
522                                        self.add_arith_diseq_split(simplified, manager);
523                                        self.add_int_domain_clauses(simplified, manager);
524                                    }
525                                    if !ph_q.is_empty() && !ph_d.is_empty() {
526                                        self.add_pigeonhole_exclusions_from(&ph_d, &ph_q, manager);
527                                    }
528                                }
529                            }
530                            if mbqi_iteration >= 10 {
531                                // After exhausting blind and finite domain
532                                // instantiation attempts, assume the model
533                                // satisfies all quantifiers.  This is sound
534                                // under the incomplete-but-practical MBQI
535                                // heuristic used by Z3 and similar solvers.
536                                self.unsat_core = None;
537                                return SolverResult::Sat;
538                            }
539                            // Continue MBQI loop
540                        }
541                    }
542
543                    mbqi_iteration += 1;
544                    if mbqi_iteration >= max_mbqi_iterations {
545                        return SolverResult::Unknown;
546                    }
547
548                    // Recreate theory manager for next iteration.
549                    // Do NOT reset theory solvers here - resetting EUF/Arith/BV
550                    // state causes spurious conflicts when accumulated lemmas from
551                    // MBQI instantiations interact with theory state that was cleared.
552                    // The theory state accumulates correctly across iterations.
553                    theory_manager = TheoryManager::new(
554                        manager,
555                        &mut self.euf,
556                        &mut self.arith,
557                        &mut self.bv,
558                        &self.bv_terms,
559                        &self.var_to_constraint,
560                        &self.var_to_parsed_arith,
561                        &self.term_to_var,
562                        &self.var_to_term,
563                        self.config.theory_mode,
564                        &mut self.statistics,
565                        self.config.max_conflicts,
566                        self.config.max_decisions,
567                        self.has_bv_arith_ops,
568                    );
569                }
570            }
571        }
572    }
573
574    /// Check satisfiability under assumptions
575    /// Assumptions are temporary constraints that don't modify the assertion stack
576    pub fn check_with_assumptions(
577        &mut self,
578        assumptions: &[TermId],
579        manager: &mut TermManager,
580    ) -> SolverResult {
581        // Save current state
582        self.push();
583
584        // Assert all assumptions
585        for &assumption in assumptions {
586            self.assert(assumption, manager);
587        }
588
589        // Check satisfiability
590        let result = self.check(manager);
591
592        // Restore state
593        self.pop();
594
595        result
596    }
597
598    /// Check satisfiability (pure SAT, no theory integration)
599    /// Useful for benchmarking or when theories are not needed
600    pub fn check_sat_only(&mut self, manager: &mut TermManager) -> SolverResult {
601        if self.assertions.is_empty() {
602            return SolverResult::Sat;
603        }
604
605        match self.sat.solve() {
606            SatResult::Sat => {
607                self.build_model(manager);
608                SolverResult::Sat
609            }
610            SatResult::Unsat => SolverResult::Unsat,
611            SatResult::Unknown => SolverResult::Unknown,
612        }
613    }
614
615    /// Build the model after SAT solving, which can be used to efficiently extract minimal unsat cores
616    pub fn enable_assumption_based_cores(&mut self) {
617        self.produce_unsat_cores = true;
618        // Assumption variables would be created during assertion
619        // to enable fine-grained core extraction
620    }
621
622    /// Minimize an unsat core using greedy deletion
623    /// This creates a minimal (but not necessarily minimum) unsatisfiable subset
624    pub fn minimize_unsat_core(&mut self, manager: &mut TermManager) -> Option<UnsatCore> {
625        if !self.produce_unsat_cores {
626            return None;
627        }
628
629        // Get the current unsat core
630        let core = self.unsat_core.as_ref()?;
631        if core.is_empty() {
632            return Some(core.clone());
633        }
634
635        // Extract the assertions in the core
636        let mut core_assertions: Vec<_> = core
637            .indices
638            .iter()
639            .map(|&idx| {
640                let assertion = self.assertions[idx as usize];
641                let name = self
642                    .named_assertions
643                    .iter()
644                    .find(|na| na.index == idx)
645                    .and_then(|na| na.name.clone());
646                (idx, assertion, name)
647            })
648            .collect();
649
650        // Try to remove each assertion one by one
651        let mut i = 0;
652        while i < core_assertions.len() {
653            // Create a temporary solver with all assertions except the i-th one
654            let mut temp_solver = Solver::new();
655            temp_solver.set_logic(self.logic.as_deref().unwrap_or("ALL"));
656
657            // Add all assertions except the i-th one
658            for (j, &(_, assertion, _)) in core_assertions.iter().enumerate() {
659                if i != j {
660                    temp_solver.assert(assertion, manager);
661                }
662            }
663
664            // Check if still unsat
665            if temp_solver.check(manager) == SolverResult::Unsat {
666                // Still unsat without this assertion - remove it
667                core_assertions.remove(i);
668                // Don't increment i, check the next element which is now at position i
669            } else {
670                // This assertion is needed
671                i += 1;
672            }
673        }
674
675        // Build the minimized core
676        let mut minimized = UnsatCore::new();
677        for (idx, _, name) in core_assertions {
678            minimized.indices.push(idx);
679            if let Some(n) = name {
680                minimized.names.push(n);
681            }
682        }
683
684        Some(minimized)
685    }
686
687    /// Get the model (if sat)
688    #[must_use]
689    pub fn model(&self) -> Option<&Model> {
690        self.model.as_ref()
691    }
692
693    /// Check satisfiability with resource limits.
694    pub fn check_with_limits(
695        &mut self,
696        manager: &mut TermManager,
697        limits: &crate::resource_limits::ResourceLimits,
698    ) -> core::result::Result<SolverResult, crate::resource_limits::ResourceExhausted> {
699        use crate::resource_limits::ResourceMonitor;
700        let mut monitor = ResourceMonitor::new(limits.clone());
701        if let Some(reason) = monitor.check() {
702            return Err(reason);
703        }
704        let orig_max_conflicts = self.config.max_conflicts;
705        let orig_max_decisions = self.config.max_decisions;
706        if let Some(max_c) = limits.max_conflicts {
707            if self.config.max_conflicts == 0 || max_c < self.config.max_conflicts {
708                self.config.max_conflicts = max_c;
709            }
710        }
711        if let Some(max_d) = limits.max_decisions {
712            if self.config.max_decisions == 0 || max_d < self.config.max_decisions {
713                self.config.max_decisions = max_d;
714            }
715        }
716        let result = self.check(manager);
717        self.config.max_conflicts = orig_max_conflicts;
718        self.config.max_decisions = orig_max_decisions;
719        monitor.conflicts = self.statistics.conflicts;
720        monitor.decisions = self.statistics.decisions;
721        monitor.restarts = self.statistics.restarts;
722        monitor.theory_checks =
723            self.statistics.theory_propagations + self.statistics.theory_conflicts;
724        if result == SolverResult::Unknown {
725            if let Some(reason) = monitor.check() {
726                return Err(reason);
727            }
728        }
729        Ok(result)
730    }
731    /// Set a wall-clock timeout.
732    pub fn set_timeout(&mut self, timeout: core::time::Duration) {
733        self.config.timeout_ms = timeout.as_millis() as u64;
734    }
735    /// Set the maximum number of SAT conflicts.
736    pub fn set_conflict_limit(&mut self, max_conflicts: u64) {
737        self.config.max_conflicts = max_conflicts;
738    }
739    /// Set the maximum number of SAT decisions.
740    pub fn set_decision_limit(&mut self, max_decisions: u64) {
741        self.config.max_decisions = max_decisions;
742    }
743
744    /// Assert multiple terms at once
745    /// This is more efficient than calling assert() multiple times
746    pub fn assert_many(&mut self, terms: &[TermId], manager: &mut TermManager) {
747        for &term in terms {
748            self.assert(term, manager);
749        }
750    }
751
752    /// Get the number of assertions in the solver
753    #[must_use]
754    pub fn num_assertions(&self) -> usize {
755        self.assertions.len()
756    }
757
758    /// Get the number of variables in the SAT solver
759    #[must_use]
760    pub fn num_variables(&self) -> usize {
761        self.term_to_var.len()
762    }
763
764    /// Check if the solver has any assertions
765    #[must_use]
766    pub fn has_assertions(&self) -> bool {
767        !self.assertions.is_empty()
768    }
769
770    /// Get the current context level (push/pop depth)
771    #[must_use]
772    pub fn context_level(&self) -> usize {
773        self.context_stack.len()
774    }
775
776    /// Push a context level
777    pub fn push(&mut self) {
778        self.context_stack.push(ContextState {
779            num_assertions: self.assertions.len(),
780            num_vars: self.var_to_term.len(),
781            has_false_assertion: self.has_false_assertion,
782            trail_position: self.trail.len(),
783        });
784        self.sat.push();
785        self.euf.push();
786        self.arith.push();
787        #[cfg(feature = "std")]
788        if let Some(nlsat) = &mut self.nlsat {
789            nlsat.push();
790        }
791    }
792
793    /// Pop a context level using trail-based undo
794    pub fn pop(&mut self) {
795        if let Some(state) = self.context_stack.pop() {
796            // Undo all operations in the trail since the push
797            while self.trail.len() > state.trail_position {
798                if let Some(op) = self.trail.pop() {
799                    match op {
800                        TrailOp::AssertionAdded { index } => {
801                            if self.assertions.len() > index {
802                                self.assertions.truncate(index);
803                            }
804                        }
805                        TrailOp::VarCreated { var: _, term } => {
806                            // Remove the term-to-var mapping
807                            self.term_to_var.remove(&term);
808                        }
809                        TrailOp::ConstraintAdded { var } => {
810                            // Remove the constraint
811                            self.var_to_constraint.remove(&var);
812                        }
813                        TrailOp::FalseAssertionSet => {
814                            // Reset the flag
815                            self.has_false_assertion = false;
816                        }
817                        TrailOp::NamedAssertionAdded { index } => {
818                            // Remove the named assertion
819                            if self.named_assertions.len() > index {
820                                self.named_assertions.truncate(index);
821                            }
822                        }
823                        TrailOp::BvTermAdded { term } => {
824                            // Remove the bitvector term
825                            self.bv_terms.remove(&term);
826                        }
827                        TrailOp::ArithTermAdded { term } => {
828                            // Remove the arithmetic term
829                            self.arith_terms.remove(&term);
830                        }
831                    }
832                }
833            }
834
835            // Use state to restore other fields
836            self.assertions.truncate(state.num_assertions);
837            self.var_to_term.truncate(state.num_vars);
838            self.has_false_assertion = state.has_false_assertion;
839
840            self.sat.pop();
841            self.euf.pop();
842            self.arith.pop();
843            #[cfg(feature = "std")]
844            if let Some(nlsat) = &mut self.nlsat {
845                nlsat.pop();
846            }
847        }
848    }
849
850    /// Reset the solver
851    pub fn reset(&mut self) {
852        self.sat.reset();
853        self.euf.reset();
854        self.arith.reset();
855        self.bv.reset();
856        self.term_to_var.clear();
857        self.var_to_term.clear();
858        self.var_to_constraint.clear();
859        self.var_to_parsed_arith.clear();
860        self.assertions.clear();
861        self.named_assertions.clear();
862        self.model = None;
863        self.unsat_core = None;
864        self.context_stack.clear();
865        self.trail.clear();
866        self.logic = None;
867        self.theory_processed_up_to = 0;
868        self.has_false_assertion = false;
869        self.bv_terms.clear();
870        self.arith_terms.clear();
871        self.dt_var_constructors.clear();
872        self.arith_parse_cache.clear();
873        self.tracked_compound_terms.clear();
874    }
875
876    /// Get the configuration
877    #[must_use]
878    pub fn config(&self) -> &SolverConfig {
879        &self.config
880    }
881
882    /// Set configuration
883    pub fn set_config(&mut self, config: SolverConfig) {
884        self.config = config;
885    }
886
887    /// Get solver statistics
888    #[must_use]
889    pub fn stats(&self) -> &oxiz_sat::SolverStats {
890        self.sat.stats()
891    }
892}
893
894#[cfg(test)]
895mod tests;