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;