oxiz_solver/solver/encode.rs
1//! Term encoding (Tseitin transformation) for the SMT solver
2
3#[allow(unused_imports)]
4use crate::prelude::*;
5use num_rational::Rational64;
6use num_traits::{One, ToPrimitive, Zero};
7use oxiz_core::ast::{TermId, TermKind, TermManager};
8use oxiz_sat::{Lit, Var};
9use smallvec::SmallVec;
10
11use super::Solver;
12use super::trail::TrailOp;
13use super::types::{
14 ArithConstraintType, Constraint, NamedAssertion, ParsedArithConstraint, Polarity, UnsatCore,
15};
16
17impl Solver {
18 pub(super) fn get_or_create_var(&mut self, term: TermId) -> Var {
19 if let Some(&var) = self.term_to_var.get(&term) {
20 return var;
21 }
22
23 let var = self.sat.new_var();
24 self.term_to_var.insert(term, var);
25 self.trail.push(TrailOp::VarCreated { var, term });
26
27 while self.var_to_term.len() <= var.index() {
28 self.var_to_term.push(TermId::new(0));
29 }
30 self.var_to_term[var.index()] = term;
31 var
32 }
33
34 /// Track theory variables in a term for model extraction.
35 /// Recursively scans a term to find Int/Real/BV variables and registers them.
36 ///
37 /// Compound terms that have already been fully traversed are recorded in
38 /// `tracked_compound_terms` to avoid redundant O(depth) re-walks when the
39 /// same sub-expression appears in multiple parent constraints.
40 pub(super) fn track_theory_vars(&mut self, term_id: TermId, manager: &TermManager) {
41 let Some(term) = manager.get(term_id) else {
42 return;
43 };
44
45 match &term.kind {
46 TermKind::Var(_) => {
47 // Found a variable - check its sort and track appropriately
48 let is_int = term.sort == manager.sorts.int_sort;
49 let is_real = term.sort == manager.sorts.real_sort;
50
51 if is_int || is_real {
52 if !self.arith_terms.contains(&term_id) {
53 self.arith_terms.insert(term_id);
54 self.trail.push(TrailOp::ArithTermAdded { term: term_id });
55 self.arith.intern(term_id);
56 }
57 } else if let Some(sort) = manager.sorts.get(term.sort)
58 && sort.is_bitvec()
59 && !self.bv_terms.contains(&term_id)
60 {
61 self.bv_terms.insert(term_id);
62 self.trail.push(TrailOp::BvTermAdded { term: term_id });
63 if let Some(width) = sort.bitvec_width() {
64 self.bv.new_bv(term_id, width);
65 }
66 // Also intern in ArithSolver for BV comparison constraints
67 // (BV comparisons are handled as bounded integer arithmetic)
68 self.arith.intern(term_id);
69 }
70 }
71 // Recursively scan compound terms.
72 // Guard: if this compound node was already fully traversed, skip it.
73 TermKind::Add(args)
74 | TermKind::Mul(args)
75 | TermKind::And(args)
76 | TermKind::Or(args) => {
77 if self.tracked_compound_terms.contains(&term_id) {
78 return;
79 }
80 self.tracked_compound_terms.insert(term_id);
81 // Collect args to avoid re-borrowing `self` during iteration
82 let args_vec: SmallVec<[TermId; 8]> = args.iter().copied().collect();
83 for arg in args_vec {
84 self.track_theory_vars(arg, manager);
85 }
86 }
87 TermKind::Sub(lhs, rhs)
88 | TermKind::Eq(lhs, rhs)
89 | TermKind::Lt(lhs, rhs)
90 | TermKind::Le(lhs, rhs)
91 | TermKind::Gt(lhs, rhs)
92 | TermKind::Ge(lhs, rhs)
93 | TermKind::BvAdd(lhs, rhs)
94 | TermKind::BvSub(lhs, rhs)
95 | TermKind::BvMul(lhs, rhs)
96 | TermKind::BvAnd(lhs, rhs)
97 | TermKind::BvOr(lhs, rhs)
98 | TermKind::BvXor(lhs, rhs)
99 | TermKind::BvUlt(lhs, rhs)
100 | TermKind::BvUle(lhs, rhs)
101 | TermKind::BvSlt(lhs, rhs)
102 | TermKind::BvSle(lhs, rhs)
103 // Shifts and concatenation: recurse so leaf operands are tracked
104 // (and thus get model values for counterexamples).
105 | TermKind::BvShl(lhs, rhs)
106 | TermKind::BvLshr(lhs, rhs)
107 | TermKind::BvAshr(lhs, rhs)
108 | TermKind::BvConcat(lhs, rhs) => {
109 if self.tracked_compound_terms.contains(&term_id) {
110 return;
111 }
112 self.tracked_compound_terms.insert(term_id);
113 let (l, r) = (*lhs, *rhs);
114 self.track_theory_vars(l, manager);
115 self.track_theory_vars(r, manager);
116 }
117 // Bit extraction: recurse into the single source operand.
118 TermKind::BvExtract { arg, .. } => {
119 if self.tracked_compound_terms.contains(&term_id) {
120 return;
121 }
122 self.tracked_compound_terms.insert(term_id);
123 let a = *arg;
124 self.track_theory_vars(a, manager);
125 }
126 // BV arithmetic operations (division/remainder)
127 // These need the has_bv_arith_ops flag for conflict detection
128 TermKind::BvUdiv(lhs, rhs)
129 | TermKind::BvSdiv(lhs, rhs)
130 | TermKind::BvUrem(lhs, rhs)
131 | TermKind::BvSrem(lhs, rhs) => {
132 if self.tracked_compound_terms.contains(&term_id) {
133 return;
134 }
135 self.tracked_compound_terms.insert(term_id);
136 self.has_bv_arith_ops = true;
137 let (l, r) = (*lhs, *rhs);
138 self.track_theory_vars(l, manager);
139 self.track_theory_vars(r, manager);
140 }
141 TermKind::Neg(arg) | TermKind::Not(arg) | TermKind::BvNot(arg) => {
142 if self.tracked_compound_terms.contains(&term_id) {
143 return;
144 }
145 self.tracked_compound_terms.insert(term_id);
146 let a = *arg;
147 self.track_theory_vars(a, manager);
148 }
149 TermKind::Ite(cond, then_br, else_br) => {
150 if self.tracked_compound_terms.contains(&term_id) {
151 return;
152 }
153 self.tracked_compound_terms.insert(term_id);
154 let (c, t, e) = (*cond, *then_br, *else_br);
155 self.track_theory_vars(c, manager);
156 self.track_theory_vars(t, manager);
157 self.track_theory_vars(e, manager);
158 }
159 // Uninterpreted function application: if the sort is numeric (Int or
160 // Real), treat the whole application as an opaque arithmetic variable.
161 // This supports the UFLIA / UFLRA combination: `f(k)` appearing in
162 // `(> (f k) 10)` must be tracked so that its model value is extracted
163 // and available to the MBQI counterexample generator.
164 //
165 // We do NOT recurse into the arguments here -- argument terms are
166 // arithmetic values passed to an opaque symbol, not arithmetic
167 // variables in their own right within this constraint. (They will be
168 // tracked separately when they appear in other constraints.)
169 //
170 // RESTRICTION: skip Apply terms that have an argument which is itself
171 // an Apply term that is already in `arith_terms` (i.e. already has a
172 // numeric model value in the arithmetic solver). When `f(g(a))` is
173 // added to arith AND `g(a)` also has an arith model value `v`, the
174 // arith solver treats `f(g(a))` as independent from `f(v)`, leading
175 // to theory combination conflicts with EUF (which knows via congruence
176 // that `f(g(a)) = f(v)`).
177 //
178 // In contrast, terms like `f(sk(x))` where `sk(x)` is a fresh Skolem
179 // constant (NOT in arith_terms) are safe to add because there are no
180 // contradictory EUF congruence facts to violate.
181 TermKind::Apply { args, .. } => {
182 let is_int = term.sort == manager.sorts.int_sort;
183 let is_real = term.sort == manager.sorts.real_sort;
184 if is_int || is_real {
185 // Check: is any argument a *non-Skolem* Apply term that is
186 // already in arith? When f(g(a)) is added to arith AND g(a)
187 // has an arith model value v, EUF applies congruence to derive
188 // f(g(a)) = f(v), conflicting with arith's independent
189 // assignment to f(g(a)). Skolem-generated Apply terms (whose
190 // function names start with "sk!") are fresh constants created
191 // during quantifier Skolemization; EUF has no equality facts
192 // about them so no congruence conflict can arise.
193 let has_conflicting_apply_arg = args.iter().any(|&arg| {
194 manager.get(arg).is_some_and(|a| {
195 if let TermKind::Apply {
196 func,
197 args: inner_args,
198 ..
199 } = &a.kind
200 {
201 if inner_args.is_empty() {
202 return false;
203 }
204 let fname = manager.resolve_str(*func);
205 let is_skolem = fname.starts_with("sk!");
206 !is_skolem && self.arith_terms.contains(&arg)
207 } else {
208 false
209 }
210 })
211 });
212 if !has_conflicting_apply_arg && !self.arith_terms.contains(&term_id) {
213 self.arith_terms.insert(term_id);
214 self.trail.push(TrailOp::ArithTermAdded { term: term_id });
215 self.arith.intern(term_id);
216 }
217 }
218 }
219
220 // Array select with numeric sort: `(select a i) : Int/Real` is an
221 // opaque arithmetic variable -- the array theory handles equality
222 // propagation for equal indices, while arithmetic sees the result as
223 // an unconstrained integer/real. We register it here so that
224 // constraints like `(> (select a 0) 7)` are tracked by the arithmetic
225 // solver and model values are extracted correctly.
226 TermKind::Select(_, _) => {
227 let is_int = term.sort == manager.sorts.int_sort;
228 let is_real = term.sort == manager.sorts.real_sort;
229 if (is_int || is_real) && !self.arith_terms.contains(&term_id) {
230 self.arith_terms.insert(term_id);
231 self.trail.push(TrailOp::ArithTermAdded { term: term_id });
232 self.arith.intern(term_id);
233 }
234 }
235
236 // Constants and other leaf terms - nothing to track
237 _ => {}
238 }
239 }
240
241 /// Parse an arithmetic comparison and extract linear expression.
242 /// Returns: (terms with coefficients, constant, constraint_type).
243 ///
244 /// Results are cached by `reason` (the comparison term id).
245 /// `ParsedArithConstraint` is purely structural — it depends only on the
246 /// term graph — so the cache is safe to retain across CDCL backtracks.
247 pub(super) fn parse_arith_comparison(
248 &mut self,
249 lhs: TermId,
250 rhs: TermId,
251 constraint_type: ArithConstraintType,
252 reason: TermId,
253 manager: &TermManager,
254 ) -> Option<ParsedArithConstraint> {
255 // Fast path: return cached result if available.
256 if let Some(cached) = self.arith_parse_cache.get(&reason) {
257 return cached.clone();
258 }
259
260 let mut terms: SmallVec<[(TermId, Rational64); 4]> = SmallVec::new();
261 let mut constant = Rational64::zero();
262
263 // Parse LHS (add positive coefficients)
264 let lhs_ok =
265 self.extract_linear_terms(lhs, Rational64::one(), &mut terms, &mut constant, manager);
266 if lhs_ok.is_none() {
267 self.arith_parse_cache.insert(reason, None);
268 return None;
269 }
270
271 // Parse RHS (subtract, so coefficients are negated)
272 // For lhs OP rhs, we want lhs - rhs OP 0
273 let rhs_ok =
274 self.extract_linear_terms(rhs, -Rational64::one(), &mut terms, &mut constant, manager);
275 if rhs_ok.is_none() {
276 self.arith_parse_cache.insert(reason, None);
277 return None;
278 }
279
280 // Combine like terms
281 let mut combined: FxHashMap<TermId, Rational64> = FxHashMap::default();
282 for (term, coef) in terms {
283 *combined.entry(term).or_insert(Rational64::zero()) += coef;
284 }
285
286 // Remove zero coefficients
287 let final_terms: SmallVec<[(TermId, Rational64); 4]> =
288 combined.into_iter().filter(|(_, c)| !c.is_zero()).collect();
289
290 let result = ParsedArithConstraint {
291 terms: final_terms,
292 constant: -constant, // Move constant to RHS
293 constraint_type,
294 reason_term: reason,
295 };
296
297 self.arith_parse_cache.insert(reason, Some(result.clone()));
298 Some(result)
299 }
300
301 /// Extract linear terms recursively from an arithmetic expression
302 /// Returns None if the term is not linear
303 #[allow(clippy::only_used_in_recursion)]
304 pub(super) fn extract_linear_terms(
305 &self,
306 term_id: TermId,
307 scale: Rational64,
308 terms: &mut SmallVec<[(TermId, Rational64); 4]>,
309 constant: &mut Rational64,
310 manager: &TermManager,
311 ) -> Option<()> {
312 let term = manager.get(term_id)?;
313
314 match &term.kind {
315 // Integer constant
316 TermKind::IntConst(n) => {
317 if let Some(val) = n.to_i64() {
318 *constant += scale * Rational64::from_integer(val);
319 Some(())
320 } else {
321 // BigInt too large, skip for now
322 None
323 }
324 }
325
326 // Rational constant
327 TermKind::RealConst(r) => {
328 *constant += scale * *r;
329 Some(())
330 }
331
332 // Bitvector constant - treat as integer
333 TermKind::BitVecConst { value, .. } => {
334 if let Some(val) = value.to_i64() {
335 *constant += scale * Rational64::from_integer(val);
336 Some(())
337 } else {
338 // BigInt too large, skip for now
339 None
340 }
341 }
342
343 // Variable (or bitvector variable - treat as integer variable)
344 TermKind::Var(_) => {
345 terms.push((term_id, scale));
346 Some(())
347 }
348
349 // Uninterpreted function application whose sort is numeric -- treat
350 // as an opaque arithmetic variable. This is the UFLIA / UFLRA case:
351 // e.g. `f(k)` in `(> (f k) 10)` where `f : Int -> Int`. By
352 // representing `f(k)` as an arithmetic variable we ensure that
353 // (a) the arithmetic solver tracks it and assigns it a model value,
354 // (b) the constraint `f(k) > 10` is handled consistently with any
355 // later instantiation that produces `f(k) <= 10`.
356 //
357 // Restriction: only treat flat Apply terms (all args atomic) as
358 // arithmetic variables. Nested applications like `f(f(k))` are
359 // handled by the EUF solver; including them in arith would require
360 // full Nelson-Oppen equality propagation to avoid spurious UNSAT.
361 TermKind::Apply { args, .. } => {
362 let sort = term.sort;
363 let is_numeric = sort == manager.sorts.int_sort || sort == manager.sorts.real_sort;
364 if is_numeric {
365 // Skip if any argument is a *non-Skolem* Apply term that is
366 // already in arith_terms. This mirrors the restriction in
367 // `track_theory_vars` and avoids EUF/arith congruence conflicts.
368 // Skolem Apply terms (prefix "sk!") are safe because EUF has no
369 // equality facts about fresh Skolem symbols.
370 let has_conflicting_apply_arg = args.iter().any(|&arg| {
371 manager.get(arg).is_some_and(|a| {
372 if let TermKind::Apply {
373 func,
374 args: inner_args,
375 ..
376 } = &a.kind
377 {
378 if inner_args.is_empty() {
379 return false;
380 }
381 let fname = manager.resolve_str(*func);
382 let is_skolem = fname.starts_with("sk!");
383 !is_skolem && self.arith_terms.contains(&arg)
384 } else {
385 false
386 }
387 })
388 });
389 if !has_conflicting_apply_arg {
390 terms.push((term_id, scale));
391 Some(())
392 } else {
393 // Non-Skolem nested Apply in arith -- cannot safely represent.
394 None
395 }
396 } else {
397 // Non-numeric Apply (e.g. uninterpreted predicate) -- not linear.
398 None
399 }
400 }
401
402 // Array select with numeric sort: treat `(select a i) : Int/Real` as
403 // an opaque arithmetic atom with the given scale coefficient. This
404 // allows expressions such as `(+ (select a 0) (select a 1))` to be
405 // parsed as linear arithmetic sums.
406 TermKind::Select(_, _) => {
407 let sort = term.sort;
408 let is_numeric = sort == manager.sorts.int_sort || sort == manager.sorts.real_sort;
409 if is_numeric {
410 terms.push((term_id, scale));
411 Some(())
412 } else {
413 // Select of non-numeric sort (e.g. Bool array) -- not linear.
414 None
415 }
416 }
417
418 // Addition
419 TermKind::Add(args) => {
420 for &arg in args {
421 self.extract_linear_terms(arg, scale, terms, constant, manager)?;
422 }
423 Some(())
424 }
425
426 // Subtraction
427 TermKind::Sub(lhs, rhs) => {
428 self.extract_linear_terms(*lhs, scale, terms, constant, manager)?;
429 self.extract_linear_terms(*rhs, -scale, terms, constant, manager)?;
430 Some(())
431 }
432
433 // Negation
434 TermKind::Neg(arg) => self.extract_linear_terms(*arg, -scale, terms, constant, manager),
435
436 // Multiplication of linear terms. A product is linear iff at most one
437 // factor is non-constant. Each factor may itself be a nested
438 // expression that reduces to a pure constant (e.g. `(- 3.0)`,
439 // `(+ 1 2)`) or to a single scaled variable (e.g. `(- x)`).
440 TermKind::Mul(args) => {
441 let mut const_product = Rational64::one();
442 // The single non-constant factor, if any, represented as a sum of
443 // (variable, coefficient) pairs. The factor must be linear-as-a-whole
444 // (exactly one variable term, no additive constant) for the product
445 // to remain linear.
446 let mut var_factor: Option<(TermId, Rational64)> = None;
447
448 for &arg in args {
449 let mut sub_terms: SmallVec<[(TermId, Rational64); 4]> = SmallVec::new();
450 let mut sub_constant = Rational64::zero();
451 self.extract_linear_terms(
452 arg,
453 Rational64::one(),
454 &mut sub_terms,
455 &mut sub_constant,
456 manager,
457 )?;
458
459 if sub_terms.is_empty() {
460 // Pure constant factor — absorb into product.
461 const_product *= sub_constant;
462 } else if sub_terms.len() == 1 && sub_constant.is_zero() {
463 // Exactly one scaled variable with no additive constant,
464 // e.g. `x`, `(- x)`, `(* 2 x)`. Record as the variable
465 // factor; if we already have one, the product is nonlinear.
466 if var_factor.is_some() {
467 return None;
468 }
469 var_factor = Some(sub_terms[0]);
470 } else {
471 // Either multi-variable (e.g. `(+ x y)`), or a linear
472 // expression with a constant offset (e.g. `(+ 1 x)`).
473 // Multiplying such a factor by another variable yields a
474 // nonlinear product.
475 return None;
476 }
477 }
478
479 let new_scale = scale * const_product;
480 match var_factor {
481 Some((v, coef)) => {
482 terms.push((v, new_scale * coef));
483 Some(())
484 }
485 None => {
486 *constant += new_scale;
487 Some(())
488 }
489 }
490 }
491
492 // Not linear
493 _ => None,
494 }
495 }
496
497 /// Assert a term
498 pub fn assert(&mut self, term: TermId, manager: &mut TermManager) {
499 let index = self.assertions.len();
500 self.assertions.push(term);
501 self.trail.push(TrailOp::AssertionAdded { index });
502 self.invalidate_fp_cache();
503
504 // Check if this is a boolean constant first
505 if let Some(t) = manager.get(term) {
506 match t.kind {
507 TermKind::False => {
508 // Mark that we have a false assertion
509 if !self.has_false_assertion {
510 self.has_false_assertion = true;
511 self.trail.push(TrailOp::FalseAssertionSet);
512 }
513 if self.produce_unsat_cores {
514 let na_index = self.named_assertions.len();
515 self.named_assertions.push(NamedAssertion {
516 term,
517 name: None,
518 index: index as u32,
519 });
520 self.trail
521 .push(TrailOp::NamedAssertionAdded { index: na_index });
522 }
523 return;
524 }
525 TermKind::True => {
526 // True is always satisfied, no need to encode
527 if self.produce_unsat_cores {
528 let na_index = self.named_assertions.len();
529 self.named_assertions.push(NamedAssertion {
530 term,
531 name: None,
532 index: index as u32,
533 });
534 self.trail
535 .push(TrailOp::NamedAssertionAdded { index: na_index });
536 }
537 return;
538 }
539 _ => {}
540 }
541 }
542
543 // Apply simplification if enabled
544 let term_to_encode = if self.config.simplify {
545 self.simplifier.simplify(term, manager)
546 } else {
547 term
548 };
549
550 // Check again if simplification produced a constant
551 if let Some(t) = manager.get(term_to_encode) {
552 match t.kind {
553 TermKind::False => {
554 if !self.has_false_assertion {
555 self.has_false_assertion = true;
556 self.trail.push(TrailOp::FalseAssertionSet);
557 }
558 return;
559 }
560 TermKind::True => {
561 // Simplified to true, no need to encode
562 return;
563 }
564 _ => {}
565 }
566 }
567
568 // Check for datatype constructor mutual exclusivity
569 // If we see (= var Constructor), track it and check for conflicts
570 if let Some(t) = manager.get(term_to_encode).cloned() {
571 if let TermKind::Eq(lhs, rhs) = &t.kind {
572 if let Some((var_term, constructor)) =
573 self.extract_dt_var_constructor(*lhs, *rhs, manager)
574 {
575 if let Some(&existing_con) = self.dt_var_constructors.get(&var_term) {
576 if existing_con != constructor {
577 // Variable constrained to two different constructors - UNSAT
578 if !self.has_false_assertion {
579 self.has_false_assertion = true;
580 self.trail.push(TrailOp::FalseAssertionSet);
581 }
582 return;
583 }
584 } else {
585 self.dt_var_constructors.insert(var_term, constructor);
586 }
587 }
588 }
589 }
590
591 // Collect polarity information if polarity-aware encoding is enabled
592 if self.polarity_aware {
593 self.collect_polarities(term_to_encode, Polarity::Positive, manager);
594 }
595
596 // Encode the assertion immediately
597 let lit = self.encode(term_to_encode, manager);
598 self.sat.add_clause([lit]);
599
600 // For Not(Eq(a,b)) assertions on arithmetic terms, eagerly add the
601 // arithmetic disequality split (a<b OR a>b) so that ArithSolver assigns
602 // distinct values from the very first SAT solve iteration. Without this,
603 // the ArithSolver may not enforce disequalities correctly.
604 self.add_arith_diseq_split(term_to_encode, manager);
605
606 if self.produce_unsat_cores {
607 let na_index = self.named_assertions.len();
608 self.named_assertions.push(NamedAssertion {
609 term,
610 name: None,
611 index: index as u32,
612 });
613 self.trail
614 .push(TrailOp::NamedAssertionAdded { index: na_index });
615 }
616 }
617
618 /// Assert a named term (for unsat core tracking)
619 pub fn assert_named(&mut self, term: TermId, name: &str, manager: &mut TermManager) {
620 let index = self.assertions.len();
621 self.assertions.push(term);
622 self.trail.push(TrailOp::AssertionAdded { index });
623 self.invalidate_fp_cache();
624
625 // Check if this is a boolean constant first
626 if let Some(t) = manager.get(term) {
627 match t.kind {
628 TermKind::False => {
629 // Mark that we have a false assertion
630 if !self.has_false_assertion {
631 self.has_false_assertion = true;
632 self.trail.push(TrailOp::FalseAssertionSet);
633 }
634 if self.produce_unsat_cores {
635 let na_index = self.named_assertions.len();
636 self.named_assertions.push(NamedAssertion {
637 term,
638 name: Some(name.to_string()),
639 index: index as u32,
640 });
641 self.trail
642 .push(TrailOp::NamedAssertionAdded { index: na_index });
643 }
644 return;
645 }
646 TermKind::True => {
647 // True is always satisfied, no need to encode
648 if self.produce_unsat_cores {
649 let na_index = self.named_assertions.len();
650 self.named_assertions.push(NamedAssertion {
651 term,
652 name: Some(name.to_string()),
653 index: index as u32,
654 });
655 self.trail
656 .push(TrailOp::NamedAssertionAdded { index: na_index });
657 }
658 return;
659 }
660 _ => {}
661 }
662 }
663
664 // Collect polarity information if polarity-aware encoding is enabled
665 if self.polarity_aware {
666 self.collect_polarities(term, Polarity::Positive, manager);
667 }
668
669 // Encode the assertion immediately
670 let lit = self.encode(term, manager);
671 self.sat.add_clause([lit]);
672
673 // Eagerly add arith diseq split for Not(Eq(a,b)) assertions
674 self.add_arith_diseq_split(term, manager);
675
676 if self.produce_unsat_cores {
677 let na_index = self.named_assertions.len();
678 self.named_assertions.push(NamedAssertion {
679 term,
680 name: Some(name.to_string()),
681 index: index as u32,
682 });
683 self.trail
684 .push(TrailOp::NamedAssertionAdded { index: na_index });
685 }
686 }
687
688 /// Get the unsat core (after check() returned Unsat)
689 #[must_use]
690 pub fn get_unsat_core(&self) -> Option<&UnsatCore> {
691 self.unsat_core.as_ref()
692 }
693
694 /// Encode a term into SAT clauses using Tseitin transformation
695 pub(super) fn encode(&mut self, term: TermId, manager: &mut TermManager) -> Lit {
696 // Clone the term data to avoid borrowing issues
697 let Some(t) = manager.get(term).cloned() else {
698 let var = self.get_or_create_var(term);
699 return Lit::pos(var);
700 };
701
702 match &t.kind {
703 TermKind::True => {
704 let var = self.get_or_create_var(manager.mk_true());
705 self.sat.add_clause([Lit::pos(var)]);
706 Lit::pos(var)
707 }
708 TermKind::False => {
709 let var = self.get_or_create_var(manager.mk_false());
710 self.sat.add_clause([Lit::neg(var)]);
711 Lit::neg(var)
712 }
713 TermKind::Var(_) => {
714 let var = self.get_or_create_var(term);
715 // Track theory terms for model extraction
716 let is_int = t.sort == manager.sorts.int_sort;
717 let is_real = t.sort == manager.sorts.real_sort;
718
719 if is_int || is_real {
720 // Track arithmetic terms
721 if !self.arith_terms.contains(&term) {
722 self.arith_terms.insert(term);
723 self.trail.push(TrailOp::ArithTermAdded { term });
724 // Register with arithmetic solver
725 self.arith.intern(term);
726 }
727 } else if let Some(sort) = manager.sorts.get(t.sort)
728 && sort.is_bitvec()
729 && !self.bv_terms.contains(&term)
730 {
731 self.bv_terms.insert(term);
732 self.trail.push(TrailOp::BvTermAdded { term });
733 // Register with BV solver if not already registered
734 if let Some(width) = sort.bitvec_width() {
735 self.bv.new_bv(term, width);
736 }
737 }
738 Lit::pos(var)
739 }
740 TermKind::Not(arg) => {
741 let arg_lit = self.encode(*arg, manager);
742 arg_lit.negate()
743 }
744 TermKind::And(args) => {
745 let result_var = self.get_or_create_var(term);
746 let result = Lit::pos(result_var);
747
748 let mut arg_lits: Vec<Lit> = Vec::new();
749 for &arg in args {
750 arg_lits.push(self.encode(arg, manager));
751 }
752
753 // Get polarity for optimization
754 let polarity = if self.polarity_aware {
755 self.polarities
756 .get(&term)
757 .copied()
758 .unwrap_or(Polarity::Both)
759 } else {
760 Polarity::Both
761 };
762
763 // result => all args (needed when result is positive)
764 // ~result or arg1, ~result or arg2, ...
765 if polarity != Polarity::Negative {
766 for &arg in &arg_lits {
767 self.sat.add_clause([result.negate(), arg]);
768 }
769 }
770
771 // all args => result (needed when result is negative)
772 // ~arg1 or ~arg2 or ... or result
773 if polarity != Polarity::Positive {
774 let mut clause: Vec<Lit> = arg_lits.iter().map(|l| l.negate()).collect();
775 clause.push(result);
776 self.sat.add_clause(clause);
777 }
778
779 result
780 }
781 TermKind::Or(args) => {
782 let result_var = self.get_or_create_var(term);
783 let result = Lit::pos(result_var);
784
785 let mut arg_lits: Vec<Lit> = Vec::new();
786 for &arg in args {
787 arg_lits.push(self.encode(arg, manager));
788 }
789
790 // Get polarity for optimization
791 let polarity = if self.polarity_aware {
792 self.polarities
793 .get(&term)
794 .copied()
795 .unwrap_or(Polarity::Both)
796 } else {
797 Polarity::Both
798 };
799
800 // result => some arg (needed when result is positive)
801 // ~result or arg1 or arg2 or ...
802 if polarity != Polarity::Negative {
803 let mut clause: Vec<Lit> = vec![result.negate()];
804 clause.extend(arg_lits.iter().copied());
805 self.sat.add_clause(clause);
806 }
807
808 // some arg => result (needed when result is negative)
809 // ~arg1 or result, ~arg2 or result, ...
810 if polarity != Polarity::Positive {
811 for &arg in &arg_lits {
812 self.sat.add_clause([arg.negate(), result]);
813 }
814 }
815
816 result
817 }
818 TermKind::Xor(lhs, rhs) => {
819 let lhs_lit = self.encode(*lhs, manager);
820 let rhs_lit = self.encode(*rhs, manager);
821
822 let result_var = self.get_or_create_var(term);
823 let result = Lit::pos(result_var);
824
825 // result <=> (lhs xor rhs)
826 // result <=> (lhs and ~rhs) or (~lhs and rhs)
827
828 // result => (lhs or rhs)
829 self.sat.add_clause([result.negate(), lhs_lit, rhs_lit]);
830 // result => (~lhs or ~rhs)
831 self.sat
832 .add_clause([result.negate(), lhs_lit.negate(), rhs_lit.negate()]);
833
834 // (lhs and ~rhs) => result
835 self.sat.add_clause([lhs_lit.negate(), rhs_lit, result]);
836 // (~lhs and rhs) => result
837 self.sat.add_clause([lhs_lit, rhs_lit.negate(), result]);
838
839 result
840 }
841 TermKind::Implies(lhs, rhs) => {
842 let lhs_lit = self.encode(*lhs, manager);
843 let rhs_lit = self.encode(*rhs, manager);
844
845 let result_var = self.get_or_create_var(term);
846 let result = Lit::pos(result_var);
847
848 // result <=> (~lhs or rhs)
849 // result => ~lhs or rhs
850 self.sat
851 .add_clause([result.negate(), lhs_lit.negate(), rhs_lit]);
852
853 // (~lhs or rhs) => result
854 // lhs or result, ~rhs or result
855 self.sat.add_clause([lhs_lit, result]);
856 self.sat.add_clause([rhs_lit.negate(), result]);
857
858 result
859 }
860 TermKind::Ite(cond, then_br, else_br) => {
861 let cond_lit = self.encode(*cond, manager);
862 let then_lit = self.encode(*then_br, manager);
863 let else_lit = self.encode(*else_br, manager);
864
865 let result_var = self.get_or_create_var(term);
866 let result = Lit::pos(result_var);
867
868 // result <=> (cond ? then : else)
869 // cond and result => then
870 self.sat
871 .add_clause([cond_lit.negate(), result.negate(), then_lit]);
872 // cond and then => result
873 self.sat
874 .add_clause([cond_lit.negate(), then_lit.negate(), result]);
875
876 // ~cond and result => else
877 self.sat.add_clause([cond_lit, result.negate(), else_lit]);
878 // ~cond and else => result
879 self.sat.add_clause([cond_lit, else_lit.negate(), result]);
880
881 result
882 }
883 TermKind::Eq(lhs, rhs) => {
884 // Check if this is a boolean equality or theory equality
885 let lhs_term = manager.get(*lhs);
886 let is_bool_eq = lhs_term.is_some_and(|t| t.sort == manager.sorts.bool_sort);
887
888 if is_bool_eq {
889 // Boolean equality: encode as iff
890 let lhs_lit = self.encode(*lhs, manager);
891 let rhs_lit = self.encode(*rhs, manager);
892
893 let result_var = self.get_or_create_var(term);
894 let result = Lit::pos(result_var);
895
896 // result <=> (lhs <=> rhs)
897 // result => (lhs => rhs) and (rhs => lhs)
898 self.sat
899 .add_clause([result.negate(), lhs_lit.negate(), rhs_lit]);
900 self.sat
901 .add_clause([result.negate(), rhs_lit.negate(), lhs_lit]);
902
903 // (lhs <=> rhs) => result
904 self.sat.add_clause([lhs_lit, rhs_lit, result]);
905 self.sat
906 .add_clause([lhs_lit.negate(), rhs_lit.negate(), result]);
907
908 result
909 } else {
910 // Theory equality: create a fresh boolean variable
911 // Store the constraint for theory propagation
912 let var = self.get_or_create_var(term);
913 self.var_to_constraint
914 .insert(var, Constraint::Eq(*lhs, *rhs));
915 self.trail.push(TrailOp::ConstraintAdded { var });
916
917 // Track theory variables for model extraction
918 self.track_theory_vars(*lhs, manager);
919 self.track_theory_vars(*rhs, manager);
920
921 // Pre-parse arithmetic equality for ArithSolver
922 // Only for Int/Real sorts, not BitVec
923 let is_arith = lhs_term.is_some_and(|t| {
924 t.sort == manager.sorts.int_sort || t.sort == manager.sorts.real_sort
925 });
926 if is_arith {
927 // We use Le type as placeholder since equality will be asserted
928 // as both Le and Ge
929 if let Some(parsed) = self.parse_arith_comparison(
930 *lhs,
931 *rhs,
932 ArithConstraintType::Le,
933 term,
934 manager,
935 ) {
936 self.var_to_parsed_arith.insert(var, parsed);
937 }
938 }
939
940 Lit::pos(var)
941 }
942 }
943 TermKind::Distinct(args) => {
944 // Encode distinct as pairwise disequalities
945 // distinct(a,b,c) <=> (a!=b) and (a!=c) and (b!=c)
946 if args.len() <= 1 {
947 // trivially true
948 let var = self.get_or_create_var(manager.mk_true());
949 return Lit::pos(var);
950 }
951
952 let result_var = self.get_or_create_var(term);
953 let result = Lit::pos(result_var);
954
955 let mut diseq_lits = Vec::new();
956 for i in 0..args.len() {
957 for j in (i + 1)..args.len() {
958 let eq = manager.mk_eq(args[i], args[j]);
959 let eq_lit = self.encode(eq, manager);
960 diseq_lits.push(eq_lit.negate());
961 }
962 }
963
964 // result => all disequalities
965 for &diseq in &diseq_lits {
966 self.sat.add_clause([result.negate(), diseq]);
967 }
968
969 // all disequalities => result
970 let mut clause: Vec<Lit> = diseq_lits.iter().map(|l| l.negate()).collect();
971 clause.push(result);
972 self.sat.add_clause(clause);
973
974 result
975 }
976 TermKind::Let { bindings, body } => {
977 // For encoding, we can substitute the bindings into the body
978 // This is a simplification - a more sophisticated approach would
979 // memoize the bindings
980 let substituted = *body;
981 for (name, value) in bindings.iter().rev() {
982 // In a full implementation, we'd perform proper substitution
983 // For now, just encode the body directly
984 let _ = (name, value);
985 }
986 self.encode(substituted, manager)
987 }
988 // Theory atoms (arithmetic, bitvec, arrays, UF)
989 // These get fresh boolean variables - the theory solver handles the semantics
990 TermKind::IntConst(_) | TermKind::RealConst(_) | TermKind::BitVecConst { .. } => {
991 // Constants are theory terms, not boolean formulas
992 // Should not appear at top level in boolean context
993 let var = self.get_or_create_var(term);
994 Lit::pos(var)
995 }
996 TermKind::Neg(_)
997 | TermKind::Add(_)
998 | TermKind::Sub(_, _)
999 | TermKind::Mul(_)
1000 | TermKind::Div(_, _)
1001 | TermKind::Mod(_, _) => {
1002 // Arithmetic terms - should not appear at boolean top level
1003 let var = self.get_or_create_var(term);
1004 Lit::pos(var)
1005 }
1006 TermKind::Lt(lhs, rhs) => {
1007 // Arithmetic predicate: lhs < rhs
1008 let var = self.get_or_create_var(term);
1009 self.var_to_constraint
1010 .insert(var, Constraint::Lt(*lhs, *rhs));
1011 self.trail.push(TrailOp::ConstraintAdded { var });
1012 // Parse and store linear constraint for ArithSolver
1013 if let Some(parsed) =
1014 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, term, manager)
1015 {
1016 self.var_to_parsed_arith.insert(var, parsed);
1017 }
1018 // Track theory variables for model extraction
1019 self.track_theory_vars(*lhs, manager);
1020 self.track_theory_vars(*rhs, manager);
1021 Lit::pos(var)
1022 }
1023 TermKind::Le(lhs, rhs) => {
1024 // Arithmetic predicate: lhs <= rhs
1025 let var = self.get_or_create_var(term);
1026 self.var_to_constraint
1027 .insert(var, Constraint::Le(*lhs, *rhs));
1028 self.trail.push(TrailOp::ConstraintAdded { var });
1029 // Parse and store linear constraint for ArithSolver
1030 if let Some(parsed) =
1031 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, term, manager)
1032 {
1033 self.var_to_parsed_arith.insert(var, parsed);
1034 }
1035 // Track theory variables for model extraction
1036 self.track_theory_vars(*lhs, manager);
1037 self.track_theory_vars(*rhs, manager);
1038 Lit::pos(var)
1039 }
1040 TermKind::Gt(lhs, rhs) => {
1041 // Arithmetic predicate: lhs > rhs
1042 let var = self.get_or_create_var(term);
1043 self.var_to_constraint
1044 .insert(var, Constraint::Gt(*lhs, *rhs));
1045 self.trail.push(TrailOp::ConstraintAdded { var });
1046 // Parse and store linear constraint for ArithSolver
1047 if let Some(parsed) =
1048 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Gt, term, manager)
1049 {
1050 self.var_to_parsed_arith.insert(var, parsed);
1051 }
1052 // Track theory variables for model extraction
1053 self.track_theory_vars(*lhs, manager);
1054 self.track_theory_vars(*rhs, manager);
1055 Lit::pos(var)
1056 }
1057 TermKind::Ge(lhs, rhs) => {
1058 // Arithmetic predicate: lhs >= rhs
1059 let var = self.get_or_create_var(term);
1060 self.var_to_constraint
1061 .insert(var, Constraint::Ge(*lhs, *rhs));
1062 self.trail.push(TrailOp::ConstraintAdded { var });
1063 // Parse and store linear constraint for ArithSolver
1064 if let Some(parsed) =
1065 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Ge, term, manager)
1066 {
1067 self.var_to_parsed_arith.insert(var, parsed);
1068 }
1069 // Track theory variables for model extraction
1070 self.track_theory_vars(*lhs, manager);
1071 self.track_theory_vars(*rhs, manager);
1072 Lit::pos(var)
1073 }
1074 TermKind::BvConcat(_, _)
1075 | TermKind::BvExtract { .. }
1076 | TermKind::BvNot(_)
1077 | TermKind::BvAnd(_, _)
1078 | TermKind::BvOr(_, _)
1079 | TermKind::BvXor(_, _)
1080 | TermKind::BvAdd(_, _)
1081 | TermKind::BvSub(_, _)
1082 | TermKind::BvMul(_, _)
1083 | TermKind::BvShl(_, _)
1084 | TermKind::BvLshr(_, _)
1085 | TermKind::BvAshr(_, _) => {
1086 // Bitvector terms - should not appear at boolean top level
1087 let var = self.get_or_create_var(term);
1088 Lit::pos(var)
1089 }
1090 TermKind::BvUdiv(_, _)
1091 | TermKind::BvSdiv(_, _)
1092 | TermKind::BvUrem(_, _)
1093 | TermKind::BvSrem(_, _) => {
1094 // Bitvector arithmetic terms (division/remainder)
1095 // Mark that we have arithmetic BV ops for conflict checking
1096 self.has_bv_arith_ops = true;
1097 let var = self.get_or_create_var(term);
1098 Lit::pos(var)
1099 }
1100 TermKind::BvUlt(lhs, rhs) => {
1101 // Bitvector unsigned less-than: treat as integer comparison
1102 let var = self.get_or_create_var(term);
1103 self.var_to_constraint
1104 .insert(var, Constraint::Lt(*lhs, *rhs));
1105 self.trail.push(TrailOp::ConstraintAdded { var });
1106 // Parse as arithmetic constraint (bitvector as bounded integer)
1107 if let Some(parsed) =
1108 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, term, manager)
1109 {
1110 self.var_to_parsed_arith.insert(var, parsed);
1111 }
1112 // Track theory variables for model extraction
1113 self.track_theory_vars(*lhs, manager);
1114 self.track_theory_vars(*rhs, manager);
1115 Lit::pos(var)
1116 }
1117 TermKind::BvUle(lhs, rhs) => {
1118 // Bitvector unsigned less-than-or-equal: treat as integer comparison
1119 let var = self.get_or_create_var(term);
1120 self.var_to_constraint
1121 .insert(var, Constraint::Le(*lhs, *rhs));
1122 self.trail.push(TrailOp::ConstraintAdded { var });
1123 if let Some(parsed) =
1124 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, term, manager)
1125 {
1126 self.var_to_parsed_arith.insert(var, parsed);
1127 }
1128 // Track theory variables for model extraction
1129 self.track_theory_vars(*lhs, manager);
1130 self.track_theory_vars(*rhs, manager);
1131 Lit::pos(var)
1132 }
1133 TermKind::BvSlt(lhs, rhs) => {
1134 // Bitvector signed less-than: treat as integer comparison
1135 let var = self.get_or_create_var(term);
1136 self.var_to_constraint
1137 .insert(var, Constraint::Lt(*lhs, *rhs));
1138 self.trail.push(TrailOp::ConstraintAdded { var });
1139 if let Some(parsed) =
1140 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, term, manager)
1141 {
1142 self.var_to_parsed_arith.insert(var, parsed);
1143 }
1144 // Track theory variables for model extraction
1145 self.track_theory_vars(*lhs, manager);
1146 self.track_theory_vars(*rhs, manager);
1147 Lit::pos(var)
1148 }
1149 TermKind::BvSle(lhs, rhs) => {
1150 // Bitvector signed less-than-or-equal: treat as integer comparison
1151 let var = self.get_or_create_var(term);
1152 self.var_to_constraint
1153 .insert(var, Constraint::Le(*lhs, *rhs));
1154 self.trail.push(TrailOp::ConstraintAdded { var });
1155 if let Some(parsed) =
1156 self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, term, manager)
1157 {
1158 self.var_to_parsed_arith.insert(var, parsed);
1159 }
1160 // Track theory variables for model extraction
1161 self.track_theory_vars(*lhs, manager);
1162 self.track_theory_vars(*rhs, manager);
1163 Lit::pos(var)
1164 }
1165 TermKind::Select(_, _) | TermKind::Store(_, _, _) => {
1166 // Array operations - theory terms
1167 let var = self.get_or_create_var(term);
1168 Lit::pos(var)
1169 }
1170 TermKind::Apply { .. } => {
1171 // Uninterpreted function application - theory term
1172 let var = self.get_or_create_var(term);
1173 // Register Bool-valued function applications as theory
1174 // constraints so that EUF congruence closure can detect
1175 // conflicts when the SAT solver assigns opposite polarities
1176 // to congruent applications (e.g., t(m)=true, t(co)=false,
1177 // but m=co implies t(m)=t(co)).
1178 if t.sort == manager.sorts.bool_sort {
1179 self.var_to_constraint
1180 .insert(var, Constraint::BoolApp(term));
1181 self.trail.push(TrailOp::ConstraintAdded { var });
1182 }
1183 Lit::pos(var)
1184 }
1185 TermKind::Forall {
1186 patterns,
1187 body,
1188 vars,
1189 } => {
1190 // Universal quantifiers: register with MBQI
1191 self.has_quantifiers = true;
1192
1193 // Check if body is Exists — if so, Skolemize the nested existential.
1194 // This handles the Forall-Exists pattern: ∀x. ∃y. φ(x,y) → ∀x. φ(x, f(x))
1195 let body_id = *body;
1196 let _vars_clone = vars.clone();
1197 let patterns_clone = patterns.clone();
1198 let body_is_exists = manager
1199 .get(body_id)
1200 .map(|t| matches!(t.kind, TermKind::Exists { .. }))
1201 .unwrap_or(false);
1202
1203 if body_is_exists {
1204 // Skolemize: ∀x. ∃y. φ(x,y) → ∀x. φ(x, sk(x))
1205 // This eliminates the nested existential so MBQI can handle
1206 // the resulting universal quantifier directly.
1207 #[cfg(feature = "std")]
1208 {
1209 let mut sk_ctx = crate::skolemization::SkolemizationContext::new();
1210 if let Ok(skolemized) = sk_ctx.skolemize(manager, term) {
1211 // Register the Skolemized version with MBQI
1212 self.mbqi.add_quantifier(skolemized, manager);
1213 // Register with E-matching engine
1214 let _ = self.ematch_engine.register_quantifier(skolemized, manager);
1215
1216 // Also collect Skolem function application terms from the
1217 // Skolemized body as MBQI candidates. These terms (e.g.
1218 // sk(x)) must appear in the candidate pool so that other
1219 // universal quantifiers can be instantiated with them.
1220 self.collect_skolem_candidates(skolemized, manager);
1221 } else {
1222 // Skolemization failed — fall back to original
1223 self.mbqi.add_quantifier(term, manager);
1224 let _ = self.ematch_engine.register_quantifier(term, manager);
1225 }
1226 }
1227 #[cfg(not(feature = "std"))]
1228 {
1229 self.mbqi.add_quantifier(term, manager);
1230 let _ = self.ematch_engine.register_quantifier(term, manager);
1231 }
1232 } else {
1233 self.mbqi.add_quantifier(term, manager);
1234 // Register with E-matching engine for trigger-based instantiation
1235 let _ = self.ematch_engine.register_quantifier(term, manager);
1236 }
1237
1238 // Collect ground terms from patterns as candidates
1239 for pattern in &patterns_clone {
1240 for &trigger in pattern {
1241 self.mbqi.collect_ground_terms(trigger, manager);
1242 }
1243 }
1244 // Create a boolean variable for the quantifier
1245 let var = self.get_or_create_var(term);
1246 Lit::pos(var)
1247 }
1248 TermKind::Exists { patterns, .. } => {
1249 // Existential quantifiers: register with MBQI for tracking
1250 self.has_quantifiers = true;
1251 self.mbqi.add_quantifier(term, manager);
1252 // Collect ground terms from patterns
1253 for pattern in patterns {
1254 for &trigger in pattern {
1255 self.mbqi.collect_ground_terms(trigger, manager);
1256 }
1257 }
1258 // Create a boolean variable for the quantifier
1259 let var = self.get_or_create_var(term);
1260 Lit::pos(var)
1261 }
1262 // String operations - theory terms and predicates
1263 TermKind::StringLit(_)
1264 | TermKind::StrConcat(_, _)
1265 | TermKind::StrLen(_)
1266 | TermKind::StrSubstr(_, _, _)
1267 | TermKind::StrAt(_, _)
1268 | TermKind::StrReplace(_, _, _)
1269 | TermKind::StrReplaceAll(_, _, _)
1270 | TermKind::StrToInt(_)
1271 | TermKind::IntToStr(_)
1272 | TermKind::StrInRe(_, _) => {
1273 // String terms - theory solver handles these
1274 let var = self.get_or_create_var(term);
1275 Lit::pos(var)
1276 }
1277 TermKind::StrContains(_, _)
1278 | TermKind::StrPrefixOf(_, _)
1279 | TermKind::StrSuffixOf(_, _)
1280 | TermKind::StrIndexOf(_, _, _) => {
1281 // String predicates - theory atoms
1282 let var = self.get_or_create_var(term);
1283 Lit::pos(var)
1284 }
1285 // Floating-point constants and special values
1286 TermKind::FpLit { .. }
1287 | TermKind::FpPlusInfinity { .. }
1288 | TermKind::FpMinusInfinity { .. }
1289 | TermKind::FpPlusZero { .. }
1290 | TermKind::FpMinusZero { .. }
1291 | TermKind::FpNaN { .. } => {
1292 // FP constants - theory terms
1293 let var = self.get_or_create_var(term);
1294 Lit::pos(var)
1295 }
1296 // Floating-point operations
1297 TermKind::FpAbs(_)
1298 | TermKind::FpNeg(_)
1299 | TermKind::FpSqrt(_, _)
1300 | TermKind::FpRoundToIntegral(_, _)
1301 | TermKind::FpAdd(_, _, _)
1302 | TermKind::FpSub(_, _, _)
1303 | TermKind::FpMul(_, _, _)
1304 | TermKind::FpDiv(_, _, _)
1305 | TermKind::FpRem(_, _)
1306 | TermKind::FpMin(_, _)
1307 | TermKind::FpMax(_, _)
1308 | TermKind::FpFma(_, _, _, _) => {
1309 // FP operations - theory terms
1310 let var = self.get_or_create_var(term);
1311 Lit::pos(var)
1312 }
1313 // Floating-point predicates
1314 TermKind::FpLeq(_, _)
1315 | TermKind::FpLt(_, _)
1316 | TermKind::FpGeq(_, _)
1317 | TermKind::FpGt(_, _)
1318 | TermKind::FpEq(_, _)
1319 | TermKind::FpIsNormal(_)
1320 | TermKind::FpIsSubnormal(_)
1321 | TermKind::FpIsZero(_)
1322 | TermKind::FpIsInfinite(_)
1323 | TermKind::FpIsNaN(_)
1324 | TermKind::FpIsNegative(_)
1325 | TermKind::FpIsPositive(_) => {
1326 // FP predicates - theory atoms that return bool
1327 let var = self.get_or_create_var(term);
1328 Lit::pos(var)
1329 }
1330 // Floating-point conversions
1331 TermKind::FpToFp { .. }
1332 | TermKind::FpToSBV { .. }
1333 | TermKind::FpToUBV { .. }
1334 | TermKind::FpToReal(_)
1335 | TermKind::RealToFp { .. }
1336 | TermKind::SBVToFp { .. }
1337 | TermKind::UBVToFp { .. } => {
1338 // FP conversions - theory terms
1339 let var = self.get_or_create_var(term);
1340 Lit::pos(var)
1341 }
1342 // Datatype operations
1343 TermKind::DtConstructor { .. }
1344 | TermKind::DtTester { .. }
1345 | TermKind::DtSelector { .. } => {
1346 // Datatype operations - theory terms
1347 let var = self.get_or_create_var(term);
1348 Lit::pos(var)
1349 }
1350 // Match expressions on datatypes
1351 TermKind::Match { .. } => {
1352 // Match expressions - theory terms
1353 let var = self.get_or_create_var(term);
1354 Lit::pos(var)
1355 }
1356 }
1357 }
1358
1359 /// Walk a (possibly Skolemized) quantifier term and collect Apply terms
1360 /// whose function name starts with "sk" as MBQI instantiation candidates.
1361 ///
1362 /// These Skolem function applications (e.g. `sk!0(x)`) must be in the
1363 /// candidate pool so that MBQI can instantiate other universal quantifiers
1364 /// with Skolem terms, enabling cross-quantifier contradictions.
1365 fn collect_skolem_candidates(&mut self, term: TermId, manager: &TermManager) {
1366 let mut visited = FxHashSet::default();
1367 self.collect_skolem_candidates_rec(term, manager, &mut visited);
1368 }
1369
1370 fn collect_skolem_candidates_rec(
1371 &mut self,
1372 term: TermId,
1373 manager: &TermManager,
1374 visited: &mut FxHashSet<TermId>,
1375 ) {
1376 if !visited.insert(term) {
1377 return;
1378 }
1379 let Some(t) = manager.get(term) else {
1380 return;
1381 };
1382 match &t.kind {
1383 TermKind::Apply { func, args } => {
1384 let fname = manager.resolve_str(*func);
1385 if fname.starts_with("sk") || fname.starts_with("skf") {
1386 // Register the whole application as a candidate
1387 self.mbqi.add_candidate(term, t.sort);
1388 }
1389 for &arg in args.iter() {
1390 self.collect_skolem_candidates_rec(arg, manager, visited);
1391 }
1392 }
1393 TermKind::Forall { body, .. } | TermKind::Exists { body, .. } => {
1394 self.collect_skolem_candidates_rec(*body, manager, visited);
1395 }
1396 TermKind::And(args) | TermKind::Or(args) => {
1397 for &a in args {
1398 self.collect_skolem_candidates_rec(a, manager, visited);
1399 }
1400 }
1401 TermKind::Not(a) | TermKind::Neg(a) => {
1402 self.collect_skolem_candidates_rec(*a, manager, visited);
1403 }
1404 TermKind::Implies(a, b)
1405 | TermKind::Eq(a, b)
1406 | TermKind::Lt(a, b)
1407 | TermKind::Le(a, b)
1408 | TermKind::Gt(a, b)
1409 | TermKind::Ge(a, b)
1410 | TermKind::Sub(a, b)
1411 | TermKind::Div(a, b)
1412 | TermKind::Mod(a, b) => {
1413 self.collect_skolem_candidates_rec(*a, manager, visited);
1414 self.collect_skolem_candidates_rec(*b, manager, visited);
1415 }
1416 TermKind::Add(args) | TermKind::Mul(args) => {
1417 for &a in args.iter() {
1418 self.collect_skolem_candidates_rec(a, manager, visited);
1419 }
1420 }
1421 TermKind::Ite(c, t_br, e) => {
1422 self.collect_skolem_candidates_rec(*c, manager, visited);
1423 self.collect_skolem_candidates_rec(*t_br, manager, visited);
1424 self.collect_skolem_candidates_rec(*e, manager, visited);
1425 }
1426 TermKind::Select(a, i) => {
1427 self.collect_skolem_candidates_rec(*a, manager, visited);
1428 self.collect_skolem_candidates_rec(*i, manager, visited);
1429 }
1430 TermKind::Store(a, i, v) => {
1431 self.collect_skolem_candidates_rec(*a, manager, visited);
1432 self.collect_skolem_candidates_rec(*i, manager, visited);
1433 self.collect_skolem_candidates_rec(*v, manager, visited);
1434 }
1435 _ => {}
1436 }
1437 }
1438
1439 /// Scan all Constraint::Eq entries in var_to_constraint that are currently
1440 /// assigned False by the SAT model and add arithmetic splits `(lhs < rhs)
1441 /// OR (lhs > rhs)` for each. This ensures ArithSolver knows about
1442 /// disequalities that arise from SAT-level implication propagation (e.g.
1443 /// from MBQI-generated instantiations like `(=> (= f(a) f(b)) (= a b))`).
1444 #[allow(dead_code)]
1445 pub(super) fn add_arith_diseq_splits_for_sat_model(&mut self, manager: &mut TermManager) {
1446 use super::types::Constraint;
1447 use oxiz_sat::LBool;
1448
1449 let pairs: Vec<(TermId, TermId)> = self
1450 .var_to_constraint
1451 .iter()
1452 .filter_map(|(&var, constraint)| {
1453 if let Constraint::Eq(lhs, rhs) = constraint {
1454 // Only Int or Real sorts
1455 let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1456 lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1457 });
1458 if lhs_is_numeric && self.sat.model_value(var) == LBool::False {
1459 Some((*lhs, *rhs))
1460 } else {
1461 None
1462 }
1463 } else {
1464 None
1465 }
1466 })
1467 .collect();
1468
1469 for (lhs, rhs) in pairs {
1470 let lt_term = manager.mk_lt(lhs, rhs);
1471 let gt_term = manager.mk_gt(lhs, rhs);
1472 // Only add if the clause isn't already a tautology or unit-forced
1473 let lt_lit = self.encode(lt_term, manager);
1474 let gt_lit = self.encode(gt_term, manager);
1475 self.sat.add_clause([lt_lit, gt_lit]);
1476 }
1477 }
1478
1479 /// When a MBQI instantiation result is `(not (= a b))` where a and b have
1480 /// Int sort, add the arithmetic split `(a < b) OR (a > b)` as a SAT clause.
1481 /// This ensures the ArithSolver knows about the disequality and doesn't
1482 /// assign both a and b to equal values.
1483 pub(super) fn add_arith_diseq_split(&mut self, term: TermId, manager: &mut TermManager) {
1484 let mut visited = FxHashSet::default();
1485 self.add_arith_diseq_split_recursive(term, manager, &mut visited);
1486 }
1487
1488 /// Add trichotomy clauses `Eq(a,b) OR Lt(a,b) OR Gt(a,b)` for every
1489 /// arithmetic `Eq(a,b)` sub-term in the given MBQI instantiation result.
1490 ///
1491 /// This ensures that when the SAT solver assigns an arithmetic Eq to false
1492 /// (disequality), the ArithSolver learns a strict ordering constraint
1493 /// (Lt or Gt) and doesn't assign equal values.
1494 ///
1495 /// Only called for MBQI instantiation results, not for all assertions,
1496 /// to avoid blowing up the clause database on non-quantified problems.
1497 pub(super) fn add_arith_eq_trichotomy(&mut self, term: TermId, manager: &mut TermManager) {
1498 let mut visited = FxHashSet::default();
1499 self.add_arith_eq_trichotomy_recursive(term, manager, &mut visited);
1500 }
1501
1502 fn add_arith_eq_trichotomy_recursive(
1503 &mut self,
1504 term: TermId,
1505 manager: &mut TermManager,
1506 visited: &mut FxHashSet<TermId>,
1507 ) {
1508 if !visited.insert(term) {
1509 return;
1510 }
1511
1512 let Some(t) = manager.get(term).cloned() else {
1513 return;
1514 };
1515
1516 match &t.kind {
1517 TermKind::Eq(lhs, rhs) => {
1518 let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1519 lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1520 });
1521 // Only add trichotomy when at least one side is an
1522 // uninterpreted function application (Apply). This is the
1523 // pattern that appears in injectivity / congruence axioms
1524 // where f(a)=f(b) needs to be split into f(a)<f(b) or
1525 // f(a)>f(b) when the equality is false.
1526 // Avoid Select terms -- the array theory handles those.
1527 let lhs_is_apply = manager
1528 .get(*lhs)
1529 .is_some_and(|lt| matches!(lt.kind, TermKind::Apply { .. }));
1530 let rhs_is_apply = manager
1531 .get(*rhs)
1532 .is_some_and(|rt| matches!(rt.kind, TermKind::Apply { .. }));
1533 if lhs_is_numeric && (lhs_is_apply || rhs_is_apply) {
1534 let (l, r) = (*lhs, *rhs);
1535 // Add trichotomy: Eq(a,b) OR Lt(a,b) OR Gt(a,b)
1536 let eq_var = self.get_or_create_var(term);
1537 let eq_lit = Lit::pos(eq_var);
1538 let lt_term = manager.mk_lt(l, r);
1539 let gt_term = manager.mk_gt(l, r);
1540 let lt_lit = self.encode(lt_term, manager);
1541 let gt_lit = self.encode(gt_term, manager);
1542 self.sat.add_clause([eq_lit, lt_lit, gt_lit]);
1543 }
1544 }
1545 TermKind::Not(arg) => {
1546 self.add_arith_eq_trichotomy_recursive(*arg, manager, visited);
1547 }
1548 TermKind::And(args) => {
1549 let args_clone: Vec<TermId> = args.iter().copied().collect();
1550 for arg in args_clone {
1551 self.add_arith_eq_trichotomy_recursive(arg, manager, visited);
1552 }
1553 }
1554 TermKind::Or(args) => {
1555 let args_clone: Vec<TermId> = args.iter().copied().collect();
1556 for arg in args_clone {
1557 self.add_arith_eq_trichotomy_recursive(arg, manager, visited);
1558 }
1559 }
1560 TermKind::Implies(lhs, rhs) => {
1561 let (l, r) = (*lhs, *rhs);
1562 self.add_arith_eq_trichotomy_recursive(l, manager, visited);
1563 self.add_arith_eq_trichotomy_recursive(r, manager, visited);
1564 }
1565 TermKind::Ite(_, then_br, else_br) => {
1566 let (t, e) = (*then_br, *else_br);
1567 self.add_arith_eq_trichotomy_recursive(t, manager, visited);
1568 self.add_arith_eq_trichotomy_recursive(e, manager, visited);
1569 }
1570 _ => {}
1571 }
1572 }
1573
1574 /// Recursively walk a term to find all `Not(Eq(a, b))` sub-terms with
1575 /// arithmetic sorts and add the split `(a < b) OR (a > b)` for each.
1576 ///
1577 /// This handles MBQI instantiation results that are implications like
1578 /// `(=> guard (not (= a b)))` where the disequality is nested inside
1579 /// the formula rather than at the top level.
1580 fn add_arith_diseq_split_recursive(
1581 &mut self,
1582 term: TermId,
1583 manager: &mut TermManager,
1584 visited: &mut FxHashSet<TermId>,
1585 ) {
1586 if !visited.insert(term) {
1587 return;
1588 }
1589
1590 let Some(t) = manager.get(term).cloned() else {
1591 return;
1592 };
1593
1594 match &t.kind {
1595 TermKind::Not(inner) => {
1596 let inner_id = *inner;
1597 if let Some(inner_t) = manager.get(inner_id).cloned() {
1598 if let TermKind::Eq(lhs, rhs) = &inner_t.kind {
1599 let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1600 lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1601 });
1602 if lhs_is_numeric {
1603 let (l, r) = (*lhs, *rhs);
1604 // Build Lt(lhs, rhs) and Gt(lhs, rhs)
1605 let lt_term = manager.mk_lt(l, r);
1606 let gt_term = manager.mk_gt(l, r);
1607
1608 // Encode both and add the disjunction
1609 let lt_lit = self.encode(lt_term, manager);
1610 let gt_lit = self.encode(gt_term, manager);
1611 self.sat.add_clause([lt_lit, gt_lit]);
1612 }
1613 }
1614 }
1615 // Also recurse into the inner term
1616 self.add_arith_diseq_split_recursive(inner_id, manager, visited);
1617 }
1618 TermKind::And(args) => {
1619 let args_clone: Vec<TermId> = args.iter().copied().collect();
1620 for arg in args_clone {
1621 self.add_arith_diseq_split_recursive(arg, manager, visited);
1622 }
1623 }
1624 TermKind::Or(args) => {
1625 let args_clone: Vec<TermId> = args.iter().copied().collect();
1626 for arg in args_clone {
1627 self.add_arith_diseq_split_recursive(arg, manager, visited);
1628 }
1629 }
1630 TermKind::Implies(_, rhs) => {
1631 // Recurse into the consequent -- that's where the disequality
1632 // typically lives in quantifier instantiation lemmas
1633 let rhs_id = *rhs;
1634 self.add_arith_diseq_split_recursive(rhs_id, manager, visited);
1635 }
1636 TermKind::Ite(_, then_br, else_br) => {
1637 let (t, e) = (*then_br, *else_br);
1638 self.add_arith_diseq_split_recursive(t, manager, visited);
1639 self.add_arith_diseq_split_recursive(e, manager, visited);
1640 }
1641 _ => {}
1642 }
1643 }
1644}