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;