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