Skip to main content

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 by constant
422            TermKind::Mul(args) => {
423                // Check if all but one are constants
424                let mut const_product = Rational64::one();
425                let mut var_term: Option<TermId> = None;
426
427                for &arg in args {
428                    let arg_term = manager.get(arg)?;
429                    match &arg_term.kind {
430                        TermKind::IntConst(n) => {
431                            if let Some(val) = n.to_i64() {
432                                const_product *= Rational64::from_integer(val);
433                            } else {
434                                return None; // BigInt too large
435                            }
436                        }
437                        TermKind::RealConst(r) => {
438                            const_product *= *r;
439                        }
440                        _ => {
441                            if var_term.is_some() {
442                                // Multiple non-constant terms - not linear
443                                return None;
444                            }
445                            var_term = Some(arg);
446                        }
447                    }
448                }
449
450                let new_scale = scale * const_product;
451                match var_term {
452                    Some(v) => self.extract_linear_terms(v, new_scale, terms, constant, manager),
453                    None => {
454                        // All constants
455                        *constant += new_scale;
456                        Some(())
457                    }
458                }
459            }
460
461            // Not linear
462            _ => None,
463        }
464    }
465
466    /// Assert a term
467    pub fn assert(&mut self, term: TermId, manager: &mut TermManager) {
468        let index = self.assertions.len();
469        self.assertions.push(term);
470        self.trail.push(TrailOp::AssertionAdded { index });
471        self.invalidate_fp_cache();
472
473        // Check if this is a boolean constant first
474        if let Some(t) = manager.get(term) {
475            match t.kind {
476                TermKind::False => {
477                    // Mark that we have a false assertion
478                    if !self.has_false_assertion {
479                        self.has_false_assertion = true;
480                        self.trail.push(TrailOp::FalseAssertionSet);
481                    }
482                    if self.produce_unsat_cores {
483                        let na_index = self.named_assertions.len();
484                        self.named_assertions.push(NamedAssertion {
485                            term,
486                            name: None,
487                            index: index as u32,
488                        });
489                        self.trail
490                            .push(TrailOp::NamedAssertionAdded { index: na_index });
491                    }
492                    return;
493                }
494                TermKind::True => {
495                    // True is always satisfied, no need to encode
496                    if self.produce_unsat_cores {
497                        let na_index = self.named_assertions.len();
498                        self.named_assertions.push(NamedAssertion {
499                            term,
500                            name: None,
501                            index: index as u32,
502                        });
503                        self.trail
504                            .push(TrailOp::NamedAssertionAdded { index: na_index });
505                    }
506                    return;
507                }
508                _ => {}
509            }
510        }
511
512        // Apply simplification if enabled
513        let term_to_encode = if self.config.simplify {
514            self.simplifier.simplify(term, manager)
515        } else {
516            term
517        };
518
519        // Check again if simplification produced a constant
520        if let Some(t) = manager.get(term_to_encode) {
521            match t.kind {
522                TermKind::False => {
523                    if !self.has_false_assertion {
524                        self.has_false_assertion = true;
525                        self.trail.push(TrailOp::FalseAssertionSet);
526                    }
527                    return;
528                }
529                TermKind::True => {
530                    // Simplified to true, no need to encode
531                    return;
532                }
533                _ => {}
534            }
535        }
536
537        // Check for datatype constructor mutual exclusivity
538        // If we see (= var Constructor), track it and check for conflicts
539        if let Some(t) = manager.get(term_to_encode).cloned() {
540            if let TermKind::Eq(lhs, rhs) = &t.kind {
541                if let Some((var_term, constructor)) =
542                    self.extract_dt_var_constructor(*lhs, *rhs, manager)
543                {
544                    if let Some(&existing_con) = self.dt_var_constructors.get(&var_term) {
545                        if existing_con != constructor {
546                            // Variable constrained to two different constructors - UNSAT
547                            if !self.has_false_assertion {
548                                self.has_false_assertion = true;
549                                self.trail.push(TrailOp::FalseAssertionSet);
550                            }
551                            return;
552                        }
553                    } else {
554                        self.dt_var_constructors.insert(var_term, constructor);
555                    }
556                }
557            }
558        }
559
560        // Collect polarity information if polarity-aware encoding is enabled
561        if self.polarity_aware {
562            self.collect_polarities(term_to_encode, Polarity::Positive, manager);
563        }
564
565        // Encode the assertion immediately
566        let lit = self.encode(term_to_encode, manager);
567        self.sat.add_clause([lit]);
568
569        // For Not(Eq(a,b)) assertions on arithmetic terms, eagerly add the
570        // arithmetic disequality split (a<b OR a>b) so that ArithSolver assigns
571        // distinct values from the very first SAT solve iteration.  Without this,
572        // the ArithSolver may not enforce disequalities correctly.
573        self.add_arith_diseq_split(term_to_encode, manager);
574
575        if self.produce_unsat_cores {
576            let na_index = self.named_assertions.len();
577            self.named_assertions.push(NamedAssertion {
578                term,
579                name: None,
580                index: index as u32,
581            });
582            self.trail
583                .push(TrailOp::NamedAssertionAdded { index: na_index });
584        }
585    }
586
587    /// Assert a named term (for unsat core tracking)
588    pub fn assert_named(&mut self, term: TermId, name: &str, manager: &mut TermManager) {
589        let index = self.assertions.len();
590        self.assertions.push(term);
591        self.trail.push(TrailOp::AssertionAdded { index });
592        self.invalidate_fp_cache();
593
594        // Check if this is a boolean constant first
595        if let Some(t) = manager.get(term) {
596            match t.kind {
597                TermKind::False => {
598                    // Mark that we have a false assertion
599                    if !self.has_false_assertion {
600                        self.has_false_assertion = true;
601                        self.trail.push(TrailOp::FalseAssertionSet);
602                    }
603                    if self.produce_unsat_cores {
604                        let na_index = self.named_assertions.len();
605                        self.named_assertions.push(NamedAssertion {
606                            term,
607                            name: Some(name.to_string()),
608                            index: index as u32,
609                        });
610                        self.trail
611                            .push(TrailOp::NamedAssertionAdded { index: na_index });
612                    }
613                    return;
614                }
615                TermKind::True => {
616                    // True is always satisfied, no need to encode
617                    if self.produce_unsat_cores {
618                        let na_index = self.named_assertions.len();
619                        self.named_assertions.push(NamedAssertion {
620                            term,
621                            name: Some(name.to_string()),
622                            index: index as u32,
623                        });
624                        self.trail
625                            .push(TrailOp::NamedAssertionAdded { index: na_index });
626                    }
627                    return;
628                }
629                _ => {}
630            }
631        }
632
633        // Collect polarity information if polarity-aware encoding is enabled
634        if self.polarity_aware {
635            self.collect_polarities(term, Polarity::Positive, manager);
636        }
637
638        // Encode the assertion immediately
639        let lit = self.encode(term, manager);
640        self.sat.add_clause([lit]);
641
642        // Eagerly add arith diseq split for Not(Eq(a,b)) assertions
643        self.add_arith_diseq_split(term, manager);
644
645        if self.produce_unsat_cores {
646            let na_index = self.named_assertions.len();
647            self.named_assertions.push(NamedAssertion {
648                term,
649                name: Some(name.to_string()),
650                index: index as u32,
651            });
652            self.trail
653                .push(TrailOp::NamedAssertionAdded { index: na_index });
654        }
655    }
656
657    /// Get the unsat core (after check() returned Unsat)
658    #[must_use]
659    pub fn get_unsat_core(&self) -> Option<&UnsatCore> {
660        self.unsat_core.as_ref()
661    }
662
663    /// Encode a term into SAT clauses using Tseitin transformation
664    pub(super) fn encode(&mut self, term: TermId, manager: &mut TermManager) -> Lit {
665        // Clone the term data to avoid borrowing issues
666        let Some(t) = manager.get(term).cloned() else {
667            let var = self.get_or_create_var(term);
668            return Lit::pos(var);
669        };
670
671        match &t.kind {
672            TermKind::True => {
673                let var = self.get_or_create_var(manager.mk_true());
674                self.sat.add_clause([Lit::pos(var)]);
675                Lit::pos(var)
676            }
677            TermKind::False => {
678                let var = self.get_or_create_var(manager.mk_false());
679                self.sat.add_clause([Lit::neg(var)]);
680                Lit::neg(var)
681            }
682            TermKind::Var(_) => {
683                let var = self.get_or_create_var(term);
684                // Track theory terms for model extraction
685                let is_int = t.sort == manager.sorts.int_sort;
686                let is_real = t.sort == manager.sorts.real_sort;
687
688                if is_int || is_real {
689                    // Track arithmetic terms
690                    if !self.arith_terms.contains(&term) {
691                        self.arith_terms.insert(term);
692                        self.trail.push(TrailOp::ArithTermAdded { term });
693                        // Register with arithmetic solver
694                        self.arith.intern(term);
695                    }
696                } else if let Some(sort) = manager.sorts.get(t.sort)
697                    && sort.is_bitvec()
698                    && !self.bv_terms.contains(&term)
699                {
700                    self.bv_terms.insert(term);
701                    self.trail.push(TrailOp::BvTermAdded { term });
702                    // Register with BV solver if not already registered
703                    if let Some(width) = sort.bitvec_width() {
704                        self.bv.new_bv(term, width);
705                    }
706                }
707                Lit::pos(var)
708            }
709            TermKind::Not(arg) => {
710                let arg_lit = self.encode(*arg, manager);
711                arg_lit.negate()
712            }
713            TermKind::And(args) => {
714                let result_var = self.get_or_create_var(term);
715                let result = Lit::pos(result_var);
716
717                let mut arg_lits: Vec<Lit> = Vec::new();
718                for &arg in args {
719                    arg_lits.push(self.encode(arg, manager));
720                }
721
722                // Get polarity for optimization
723                let polarity = if self.polarity_aware {
724                    self.polarities
725                        .get(&term)
726                        .copied()
727                        .unwrap_or(Polarity::Both)
728                } else {
729                    Polarity::Both
730                };
731
732                // result => all args (needed when result is positive)
733                // ~result or arg1, ~result or arg2, ...
734                if polarity != Polarity::Negative {
735                    for &arg in &arg_lits {
736                        self.sat.add_clause([result.negate(), arg]);
737                    }
738                }
739
740                // all args => result (needed when result is negative)
741                // ~arg1 or ~arg2 or ... or result
742                if polarity != Polarity::Positive {
743                    let mut clause: Vec<Lit> = arg_lits.iter().map(|l| l.negate()).collect();
744                    clause.push(result);
745                    self.sat.add_clause(clause);
746                }
747
748                result
749            }
750            TermKind::Or(args) => {
751                let result_var = self.get_or_create_var(term);
752                let result = Lit::pos(result_var);
753
754                let mut arg_lits: Vec<Lit> = Vec::new();
755                for &arg in args {
756                    arg_lits.push(self.encode(arg, manager));
757                }
758
759                // Get polarity for optimization
760                let polarity = if self.polarity_aware {
761                    self.polarities
762                        .get(&term)
763                        .copied()
764                        .unwrap_or(Polarity::Both)
765                } else {
766                    Polarity::Both
767                };
768
769                // result => some arg (needed when result is positive)
770                // ~result or arg1 or arg2 or ...
771                if polarity != Polarity::Negative {
772                    let mut clause: Vec<Lit> = vec![result.negate()];
773                    clause.extend(arg_lits.iter().copied());
774                    self.sat.add_clause(clause);
775                }
776
777                // some arg => result (needed when result is negative)
778                // ~arg1 or result, ~arg2 or result, ...
779                if polarity != Polarity::Positive {
780                    for &arg in &arg_lits {
781                        self.sat.add_clause([arg.negate(), result]);
782                    }
783                }
784
785                result
786            }
787            TermKind::Xor(lhs, rhs) => {
788                let lhs_lit = self.encode(*lhs, manager);
789                let rhs_lit = self.encode(*rhs, manager);
790
791                let result_var = self.get_or_create_var(term);
792                let result = Lit::pos(result_var);
793
794                // result <=> (lhs xor rhs)
795                // result <=> (lhs and ~rhs) or (~lhs and rhs)
796
797                // result => (lhs or rhs)
798                self.sat.add_clause([result.negate(), lhs_lit, rhs_lit]);
799                // result => (~lhs or ~rhs)
800                self.sat
801                    .add_clause([result.negate(), lhs_lit.negate(), rhs_lit.negate()]);
802
803                // (lhs and ~rhs) => result
804                self.sat.add_clause([lhs_lit.negate(), rhs_lit, result]);
805                // (~lhs and rhs) => result
806                self.sat.add_clause([lhs_lit, rhs_lit.negate(), result]);
807
808                result
809            }
810            TermKind::Implies(lhs, rhs) => {
811                let lhs_lit = self.encode(*lhs, manager);
812                let rhs_lit = self.encode(*rhs, manager);
813
814                let result_var = self.get_or_create_var(term);
815                let result = Lit::pos(result_var);
816
817                // result <=> (~lhs or rhs)
818                // result => ~lhs or rhs
819                self.sat
820                    .add_clause([result.negate(), lhs_lit.negate(), rhs_lit]);
821
822                // (~lhs or rhs) => result
823                // lhs or result, ~rhs or result
824                self.sat.add_clause([lhs_lit, result]);
825                self.sat.add_clause([rhs_lit.negate(), result]);
826
827                result
828            }
829            TermKind::Ite(cond, then_br, else_br) => {
830                let cond_lit = self.encode(*cond, manager);
831                let then_lit = self.encode(*then_br, manager);
832                let else_lit = self.encode(*else_br, manager);
833
834                let result_var = self.get_or_create_var(term);
835                let result = Lit::pos(result_var);
836
837                // result <=> (cond ? then : else)
838                // cond and result => then
839                self.sat
840                    .add_clause([cond_lit.negate(), result.negate(), then_lit]);
841                // cond and then => result
842                self.sat
843                    .add_clause([cond_lit.negate(), then_lit.negate(), result]);
844
845                // ~cond and result => else
846                self.sat.add_clause([cond_lit, result.negate(), else_lit]);
847                // ~cond and else => result
848                self.sat.add_clause([cond_lit, else_lit.negate(), result]);
849
850                result
851            }
852            TermKind::Eq(lhs, rhs) => {
853                // Check if this is a boolean equality or theory equality
854                let lhs_term = manager.get(*lhs);
855                let is_bool_eq = lhs_term.is_some_and(|t| t.sort == manager.sorts.bool_sort);
856
857                if is_bool_eq {
858                    // Boolean equality: encode as iff
859                    let lhs_lit = self.encode(*lhs, manager);
860                    let rhs_lit = self.encode(*rhs, manager);
861
862                    let result_var = self.get_or_create_var(term);
863                    let result = Lit::pos(result_var);
864
865                    // result <=> (lhs <=> rhs)
866                    // result => (lhs => rhs) and (rhs => lhs)
867                    self.sat
868                        .add_clause([result.negate(), lhs_lit.negate(), rhs_lit]);
869                    self.sat
870                        .add_clause([result.negate(), rhs_lit.negate(), lhs_lit]);
871
872                    // (lhs <=> rhs) => result
873                    self.sat.add_clause([lhs_lit, rhs_lit, result]);
874                    self.sat
875                        .add_clause([lhs_lit.negate(), rhs_lit.negate(), result]);
876
877                    result
878                } else {
879                    // Theory equality: create a fresh boolean variable
880                    // Store the constraint for theory propagation
881                    let var = self.get_or_create_var(term);
882                    self.var_to_constraint
883                        .insert(var, Constraint::Eq(*lhs, *rhs));
884                    self.trail.push(TrailOp::ConstraintAdded { var });
885
886                    // Track theory variables for model extraction
887                    self.track_theory_vars(*lhs, manager);
888                    self.track_theory_vars(*rhs, manager);
889
890                    // Pre-parse arithmetic equality for ArithSolver
891                    // Only for Int/Real sorts, not BitVec
892                    let is_arith = lhs_term.is_some_and(|t| {
893                        t.sort == manager.sorts.int_sort || t.sort == manager.sorts.real_sort
894                    });
895                    if is_arith {
896                        // We use Le type as placeholder since equality will be asserted
897                        // as both Le and Ge
898                        if let Some(parsed) = self.parse_arith_comparison(
899                            *lhs,
900                            *rhs,
901                            ArithConstraintType::Le,
902                            term,
903                            manager,
904                        ) {
905                            self.var_to_parsed_arith.insert(var, parsed);
906                        }
907                    }
908
909                    Lit::pos(var)
910                }
911            }
912            TermKind::Distinct(args) => {
913                // Encode distinct as pairwise disequalities
914                // distinct(a,b,c) <=> (a!=b) and (a!=c) and (b!=c)
915                if args.len() <= 1 {
916                    // trivially true
917                    let var = self.get_or_create_var(manager.mk_true());
918                    return Lit::pos(var);
919                }
920
921                let result_var = self.get_or_create_var(term);
922                let result = Lit::pos(result_var);
923
924                let mut diseq_lits = Vec::new();
925                for i in 0..args.len() {
926                    for j in (i + 1)..args.len() {
927                        let eq = manager.mk_eq(args[i], args[j]);
928                        let eq_lit = self.encode(eq, manager);
929                        diseq_lits.push(eq_lit.negate());
930                    }
931                }
932
933                // result => all disequalities
934                for &diseq in &diseq_lits {
935                    self.sat.add_clause([result.negate(), diseq]);
936                }
937
938                // all disequalities => result
939                let mut clause: Vec<Lit> = diseq_lits.iter().map(|l| l.negate()).collect();
940                clause.push(result);
941                self.sat.add_clause(clause);
942
943                result
944            }
945            TermKind::Let { bindings, body } => {
946                // For encoding, we can substitute the bindings into the body
947                // This is a simplification - a more sophisticated approach would
948                // memoize the bindings
949                let substituted = *body;
950                for (name, value) in bindings.iter().rev() {
951                    // In a full implementation, we'd perform proper substitution
952                    // For now, just encode the body directly
953                    let _ = (name, value);
954                }
955                self.encode(substituted, manager)
956            }
957            // Theory atoms (arithmetic, bitvec, arrays, UF)
958            // These get fresh boolean variables - the theory solver handles the semantics
959            TermKind::IntConst(_) | TermKind::RealConst(_) | TermKind::BitVecConst { .. } => {
960                // Constants are theory terms, not boolean formulas
961                // Should not appear at top level in boolean context
962                let var = self.get_or_create_var(term);
963                Lit::pos(var)
964            }
965            TermKind::Neg(_)
966            | TermKind::Add(_)
967            | TermKind::Sub(_, _)
968            | TermKind::Mul(_)
969            | TermKind::Div(_, _)
970            | TermKind::Mod(_, _) => {
971                // Arithmetic terms - should not appear at boolean top level
972                let var = self.get_or_create_var(term);
973                Lit::pos(var)
974            }
975            TermKind::Lt(lhs, rhs) => {
976                // Arithmetic predicate: lhs < rhs
977                let var = self.get_or_create_var(term);
978                self.var_to_constraint
979                    .insert(var, Constraint::Lt(*lhs, *rhs));
980                self.trail.push(TrailOp::ConstraintAdded { var });
981                // Parse and store linear constraint for ArithSolver
982                if let Some(parsed) =
983                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, term, manager)
984                {
985                    self.var_to_parsed_arith.insert(var, parsed);
986                }
987                // Track theory variables for model extraction
988                self.track_theory_vars(*lhs, manager);
989                self.track_theory_vars(*rhs, manager);
990                Lit::pos(var)
991            }
992            TermKind::Le(lhs, rhs) => {
993                // Arithmetic predicate: lhs <= rhs
994                let var = self.get_or_create_var(term);
995                self.var_to_constraint
996                    .insert(var, Constraint::Le(*lhs, *rhs));
997                self.trail.push(TrailOp::ConstraintAdded { var });
998                // Parse and store linear constraint for ArithSolver
999                if let Some(parsed) =
1000                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, term, manager)
1001                {
1002                    self.var_to_parsed_arith.insert(var, parsed);
1003                }
1004                // Track theory variables for model extraction
1005                self.track_theory_vars(*lhs, manager);
1006                self.track_theory_vars(*rhs, manager);
1007                Lit::pos(var)
1008            }
1009            TermKind::Gt(lhs, rhs) => {
1010                // Arithmetic predicate: lhs > rhs
1011                let var = self.get_or_create_var(term);
1012                self.var_to_constraint
1013                    .insert(var, Constraint::Gt(*lhs, *rhs));
1014                self.trail.push(TrailOp::ConstraintAdded { var });
1015                // Parse and store linear constraint for ArithSolver
1016                if let Some(parsed) =
1017                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Gt, term, manager)
1018                {
1019                    self.var_to_parsed_arith.insert(var, parsed);
1020                }
1021                // Track theory variables for model extraction
1022                self.track_theory_vars(*lhs, manager);
1023                self.track_theory_vars(*rhs, manager);
1024                Lit::pos(var)
1025            }
1026            TermKind::Ge(lhs, rhs) => {
1027                // Arithmetic predicate: lhs >= rhs
1028                let var = self.get_or_create_var(term);
1029                self.var_to_constraint
1030                    .insert(var, Constraint::Ge(*lhs, *rhs));
1031                self.trail.push(TrailOp::ConstraintAdded { var });
1032                // Parse and store linear constraint for ArithSolver
1033                if let Some(parsed) =
1034                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Ge, term, manager)
1035                {
1036                    self.var_to_parsed_arith.insert(var, parsed);
1037                }
1038                // Track theory variables for model extraction
1039                self.track_theory_vars(*lhs, manager);
1040                self.track_theory_vars(*rhs, manager);
1041                Lit::pos(var)
1042            }
1043            TermKind::BvConcat(_, _)
1044            | TermKind::BvExtract { .. }
1045            | TermKind::BvNot(_)
1046            | TermKind::BvAnd(_, _)
1047            | TermKind::BvOr(_, _)
1048            | TermKind::BvXor(_, _)
1049            | TermKind::BvAdd(_, _)
1050            | TermKind::BvSub(_, _)
1051            | TermKind::BvMul(_, _)
1052            | TermKind::BvShl(_, _)
1053            | TermKind::BvLshr(_, _)
1054            | TermKind::BvAshr(_, _) => {
1055                // Bitvector terms - should not appear at boolean top level
1056                let var = self.get_or_create_var(term);
1057                Lit::pos(var)
1058            }
1059            TermKind::BvUdiv(_, _)
1060            | TermKind::BvSdiv(_, _)
1061            | TermKind::BvUrem(_, _)
1062            | TermKind::BvSrem(_, _) => {
1063                // Bitvector arithmetic terms (division/remainder)
1064                // Mark that we have arithmetic BV ops for conflict checking
1065                self.has_bv_arith_ops = true;
1066                let var = self.get_or_create_var(term);
1067                Lit::pos(var)
1068            }
1069            TermKind::BvUlt(lhs, rhs) => {
1070                // Bitvector unsigned less-than: treat as integer comparison
1071                let var = self.get_or_create_var(term);
1072                self.var_to_constraint
1073                    .insert(var, Constraint::Lt(*lhs, *rhs));
1074                self.trail.push(TrailOp::ConstraintAdded { var });
1075                // Parse as arithmetic constraint (bitvector as bounded integer)
1076                if let Some(parsed) =
1077                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, term, manager)
1078                {
1079                    self.var_to_parsed_arith.insert(var, parsed);
1080                }
1081                // Track theory variables for model extraction
1082                self.track_theory_vars(*lhs, manager);
1083                self.track_theory_vars(*rhs, manager);
1084                Lit::pos(var)
1085            }
1086            TermKind::BvUle(lhs, rhs) => {
1087                // Bitvector unsigned less-than-or-equal: treat as integer comparison
1088                let var = self.get_or_create_var(term);
1089                self.var_to_constraint
1090                    .insert(var, Constraint::Le(*lhs, *rhs));
1091                self.trail.push(TrailOp::ConstraintAdded { var });
1092                if let Some(parsed) =
1093                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, 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::BvSlt(lhs, rhs) => {
1103                // Bitvector signed less-than: treat as integer comparison
1104                let var = self.get_or_create_var(term);
1105                self.var_to_constraint
1106                    .insert(var, Constraint::Lt(*lhs, *rhs));
1107                self.trail.push(TrailOp::ConstraintAdded { var });
1108                if let Some(parsed) =
1109                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Lt, 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::BvSle(lhs, rhs) => {
1119                // Bitvector signed less-than-or-equal: treat as integer comparison
1120                let var = self.get_or_create_var(term);
1121                self.var_to_constraint
1122                    .insert(var, Constraint::Le(*lhs, *rhs));
1123                self.trail.push(TrailOp::ConstraintAdded { var });
1124                if let Some(parsed) =
1125                    self.parse_arith_comparison(*lhs, *rhs, ArithConstraintType::Le, 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::Select(_, _) | TermKind::Store(_, _, _) => {
1135                // Array operations - theory terms
1136                let var = self.get_or_create_var(term);
1137                Lit::pos(var)
1138            }
1139            TermKind::Apply { .. } => {
1140                // Uninterpreted function application - theory term
1141                let var = self.get_or_create_var(term);
1142                // Register Bool-valued function applications as theory
1143                // constraints so that EUF congruence closure can detect
1144                // conflicts when the SAT solver assigns opposite polarities
1145                // to congruent applications (e.g., t(m)=true, t(co)=false,
1146                // but m=co implies t(m)=t(co)).
1147                if t.sort == manager.sorts.bool_sort {
1148                    self.var_to_constraint
1149                        .insert(var, Constraint::BoolApp(term));
1150                    self.trail.push(TrailOp::ConstraintAdded { var });
1151                }
1152                Lit::pos(var)
1153            }
1154            TermKind::Forall {
1155                patterns,
1156                body,
1157                vars,
1158            } => {
1159                // Universal quantifiers: register with MBQI
1160                self.has_quantifiers = true;
1161
1162                // Check if body is Exists — if so, Skolemize the nested existential.
1163                // This handles the Forall-Exists pattern: ∀x. ∃y. φ(x,y) → ∀x. φ(x, f(x))
1164                let body_id = *body;
1165                let vars_clone = vars.clone();
1166                let patterns_clone = patterns.clone();
1167                let body_is_exists = manager
1168                    .get(body_id)
1169                    .map(|t| matches!(t.kind, TermKind::Exists { .. }))
1170                    .unwrap_or(false);
1171
1172                if body_is_exists {
1173                    // Skolemize: ∀x. ∃y. φ(x,y) → ∀x. φ(x, sk(x))
1174                    // This eliminates the nested existential so MBQI can handle
1175                    // the resulting universal quantifier directly.
1176                    #[cfg(feature = "std")]
1177                    {
1178                        let mut sk_ctx = crate::skolemization::SkolemizationContext::new();
1179                        if let Ok(skolemized) = sk_ctx.skolemize(manager, term) {
1180                            // Register the Skolemized version with MBQI
1181                            self.mbqi.add_quantifier(skolemized, manager);
1182                            // Register with E-matching engine
1183                            let _ = self.ematch_engine.register_quantifier(skolemized, manager);
1184
1185                            // Also collect Skolem function application terms from the
1186                            // Skolemized body as MBQI candidates.  These terms (e.g.
1187                            // sk(x)) must appear in the candidate pool so that other
1188                            // universal quantifiers can be instantiated with them.
1189                            self.collect_skolem_candidates(skolemized, manager);
1190                        } else {
1191                            // Skolemization failed — fall back to original
1192                            self.mbqi.add_quantifier(term, manager);
1193                            let _ = self.ematch_engine.register_quantifier(term, manager);
1194                        }
1195                    }
1196                    #[cfg(not(feature = "std"))]
1197                    {
1198                        self.mbqi.add_quantifier(term, manager);
1199                        let _ = self.ematch_engine.register_quantifier(term, manager);
1200                    }
1201                } else {
1202                    self.mbqi.add_quantifier(term, manager);
1203                    // Register with E-matching engine for trigger-based instantiation
1204                    let _ = self.ematch_engine.register_quantifier(term, manager);
1205                }
1206
1207                // Collect ground terms from patterns as candidates
1208                for pattern in &patterns_clone {
1209                    for &trigger in pattern {
1210                        self.mbqi.collect_ground_terms(trigger, manager);
1211                    }
1212                }
1213                // Create a boolean variable for the quantifier
1214                let var = self.get_or_create_var(term);
1215                Lit::pos(var)
1216            }
1217            TermKind::Exists { patterns, .. } => {
1218                // Existential quantifiers: register with MBQI for tracking
1219                self.has_quantifiers = true;
1220                self.mbqi.add_quantifier(term, manager);
1221                // Collect ground terms from patterns
1222                for pattern in patterns {
1223                    for &trigger in pattern {
1224                        self.mbqi.collect_ground_terms(trigger, manager);
1225                    }
1226                }
1227                // Create a boolean variable for the quantifier
1228                let var = self.get_or_create_var(term);
1229                Lit::pos(var)
1230            }
1231            // String operations - theory terms and predicates
1232            TermKind::StringLit(_)
1233            | TermKind::StrConcat(_, _)
1234            | TermKind::StrLen(_)
1235            | TermKind::StrSubstr(_, _, _)
1236            | TermKind::StrAt(_, _)
1237            | TermKind::StrReplace(_, _, _)
1238            | TermKind::StrReplaceAll(_, _, _)
1239            | TermKind::StrToInt(_)
1240            | TermKind::IntToStr(_)
1241            | TermKind::StrInRe(_, _) => {
1242                // String terms - theory solver handles these
1243                let var = self.get_or_create_var(term);
1244                Lit::pos(var)
1245            }
1246            TermKind::StrContains(_, _)
1247            | TermKind::StrPrefixOf(_, _)
1248            | TermKind::StrSuffixOf(_, _)
1249            | TermKind::StrIndexOf(_, _, _) => {
1250                // String predicates - theory atoms
1251                let var = self.get_or_create_var(term);
1252                Lit::pos(var)
1253            }
1254            // Floating-point constants and special values
1255            TermKind::FpLit { .. }
1256            | TermKind::FpPlusInfinity { .. }
1257            | TermKind::FpMinusInfinity { .. }
1258            | TermKind::FpPlusZero { .. }
1259            | TermKind::FpMinusZero { .. }
1260            | TermKind::FpNaN { .. } => {
1261                // FP constants - theory terms
1262                let var = self.get_or_create_var(term);
1263                Lit::pos(var)
1264            }
1265            // Floating-point operations
1266            TermKind::FpAbs(_)
1267            | TermKind::FpNeg(_)
1268            | TermKind::FpSqrt(_, _)
1269            | TermKind::FpRoundToIntegral(_, _)
1270            | TermKind::FpAdd(_, _, _)
1271            | TermKind::FpSub(_, _, _)
1272            | TermKind::FpMul(_, _, _)
1273            | TermKind::FpDiv(_, _, _)
1274            | TermKind::FpRem(_, _)
1275            | TermKind::FpMin(_, _)
1276            | TermKind::FpMax(_, _)
1277            | TermKind::FpFma(_, _, _, _) => {
1278                // FP operations - theory terms
1279                let var = self.get_or_create_var(term);
1280                Lit::pos(var)
1281            }
1282            // Floating-point predicates
1283            TermKind::FpLeq(_, _)
1284            | TermKind::FpLt(_, _)
1285            | TermKind::FpGeq(_, _)
1286            | TermKind::FpGt(_, _)
1287            | TermKind::FpEq(_, _)
1288            | TermKind::FpIsNormal(_)
1289            | TermKind::FpIsSubnormal(_)
1290            | TermKind::FpIsZero(_)
1291            | TermKind::FpIsInfinite(_)
1292            | TermKind::FpIsNaN(_)
1293            | TermKind::FpIsNegative(_)
1294            | TermKind::FpIsPositive(_) => {
1295                // FP predicates - theory atoms that return bool
1296                let var = self.get_or_create_var(term);
1297                Lit::pos(var)
1298            }
1299            // Floating-point conversions
1300            TermKind::FpToFp { .. }
1301            | TermKind::FpToSBV { .. }
1302            | TermKind::FpToUBV { .. }
1303            | TermKind::FpToReal(_)
1304            | TermKind::RealToFp { .. }
1305            | TermKind::SBVToFp { .. }
1306            | TermKind::UBVToFp { .. } => {
1307                // FP conversions - theory terms
1308                let var = self.get_or_create_var(term);
1309                Lit::pos(var)
1310            }
1311            // Datatype operations
1312            TermKind::DtConstructor { .. }
1313            | TermKind::DtTester { .. }
1314            | TermKind::DtSelector { .. } => {
1315                // Datatype operations - theory terms
1316                let var = self.get_or_create_var(term);
1317                Lit::pos(var)
1318            }
1319            // Match expressions on datatypes
1320            TermKind::Match { .. } => {
1321                // Match expressions - theory terms
1322                let var = self.get_or_create_var(term);
1323                Lit::pos(var)
1324            }
1325        }
1326    }
1327
1328    /// Walk a (possibly Skolemized) quantifier term and collect Apply terms
1329    /// whose function name starts with "sk" as MBQI instantiation candidates.
1330    ///
1331    /// These Skolem function applications (e.g. `sk!0(x)`) must be in the
1332    /// candidate pool so that MBQI can instantiate other universal quantifiers
1333    /// with Skolem terms, enabling cross-quantifier contradictions.
1334    fn collect_skolem_candidates(&mut self, term: TermId, manager: &TermManager) {
1335        let mut visited = FxHashSet::default();
1336        self.collect_skolem_candidates_rec(term, manager, &mut visited);
1337    }
1338
1339    fn collect_skolem_candidates_rec(
1340        &mut self,
1341        term: TermId,
1342        manager: &TermManager,
1343        visited: &mut FxHashSet<TermId>,
1344    ) {
1345        if !visited.insert(term) {
1346            return;
1347        }
1348        let Some(t) = manager.get(term) else {
1349            return;
1350        };
1351        match &t.kind {
1352            TermKind::Apply { func, args } => {
1353                let fname = manager.resolve_str(*func);
1354                if fname.starts_with("sk") || fname.starts_with("skf") {
1355                    // Register the whole application as a candidate
1356                    self.mbqi.add_candidate(term, t.sort);
1357                }
1358                for &arg in args.iter() {
1359                    self.collect_skolem_candidates_rec(arg, manager, visited);
1360                }
1361            }
1362            TermKind::Forall { body, .. } | TermKind::Exists { body, .. } => {
1363                self.collect_skolem_candidates_rec(*body, manager, visited);
1364            }
1365            TermKind::And(args) | TermKind::Or(args) => {
1366                for &a in args {
1367                    self.collect_skolem_candidates_rec(a, manager, visited);
1368                }
1369            }
1370            TermKind::Not(a) | TermKind::Neg(a) => {
1371                self.collect_skolem_candidates_rec(*a, manager, visited);
1372            }
1373            TermKind::Implies(a, b)
1374            | TermKind::Eq(a, b)
1375            | TermKind::Lt(a, b)
1376            | TermKind::Le(a, b)
1377            | TermKind::Gt(a, b)
1378            | TermKind::Ge(a, b)
1379            | TermKind::Sub(a, b)
1380            | TermKind::Div(a, b)
1381            | TermKind::Mod(a, b) => {
1382                self.collect_skolem_candidates_rec(*a, manager, visited);
1383                self.collect_skolem_candidates_rec(*b, manager, visited);
1384            }
1385            TermKind::Add(args) | TermKind::Mul(args) => {
1386                for &a in args.iter() {
1387                    self.collect_skolem_candidates_rec(a, manager, visited);
1388                }
1389            }
1390            TermKind::Ite(c, t_br, e) => {
1391                self.collect_skolem_candidates_rec(*c, manager, visited);
1392                self.collect_skolem_candidates_rec(*t_br, manager, visited);
1393                self.collect_skolem_candidates_rec(*e, manager, visited);
1394            }
1395            TermKind::Select(a, i) => {
1396                self.collect_skolem_candidates_rec(*a, manager, visited);
1397                self.collect_skolem_candidates_rec(*i, manager, visited);
1398            }
1399            TermKind::Store(a, i, v) => {
1400                self.collect_skolem_candidates_rec(*a, manager, visited);
1401                self.collect_skolem_candidates_rec(*i, manager, visited);
1402                self.collect_skolem_candidates_rec(*v, manager, visited);
1403            }
1404            _ => {}
1405        }
1406    }
1407
1408    /// Scan all Constraint::Eq entries in var_to_constraint that are currently
1409    /// assigned False by the SAT model and add arithmetic splits `(lhs < rhs)
1410    /// OR (lhs > rhs)` for each.  This ensures ArithSolver knows about
1411    /// disequalities that arise from SAT-level implication propagation (e.g.
1412    /// from MBQI-generated instantiations like `(=> (= f(a) f(b)) (= a b))`).
1413    pub(super) fn add_arith_diseq_splits_for_sat_model(&mut self, manager: &mut TermManager) {
1414        use super::types::Constraint;
1415        use oxiz_sat::LBool;
1416
1417        let pairs: Vec<(TermId, TermId)> = self
1418            .var_to_constraint
1419            .iter()
1420            .filter_map(|(&var, constraint)| {
1421                if let Constraint::Eq(lhs, rhs) = constraint {
1422                    // Only Int or Real sorts
1423                    let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1424                        lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1425                    });
1426                    if lhs_is_numeric && self.sat.model_value(var) == LBool::False {
1427                        Some((*lhs, *rhs))
1428                    } else {
1429                        None
1430                    }
1431                } else {
1432                    None
1433                }
1434            })
1435            .collect();
1436
1437        for (lhs, rhs) in pairs {
1438            let lt_term = manager.mk_lt(lhs, rhs);
1439            let gt_term = manager.mk_gt(lhs, rhs);
1440            // Only add if the clause isn't already a tautology or unit-forced
1441            let lt_lit = self.encode(lt_term, manager);
1442            let gt_lit = self.encode(gt_term, manager);
1443            self.sat.add_clause([lt_lit, gt_lit]);
1444        }
1445    }
1446
1447    /// When a MBQI instantiation result is `(not (= a b))` where a and b have
1448    /// Int sort, add the arithmetic split `(a < b) OR (a > b)` as a SAT clause.
1449    /// This ensures the ArithSolver knows about the disequality and doesn't
1450    /// assign both a and b to equal values.
1451    pub(super) fn add_arith_diseq_split(&mut self, term: TermId, manager: &mut TermManager) {
1452        let mut visited = FxHashSet::default();
1453        self.add_arith_diseq_split_recursive(term, manager, &mut visited);
1454    }
1455
1456    /// Add trichotomy clauses `Eq(a,b) OR Lt(a,b) OR Gt(a,b)` for every
1457    /// arithmetic `Eq(a,b)` sub-term in the given MBQI instantiation result.
1458    ///
1459    /// This ensures that when the SAT solver assigns an arithmetic Eq to false
1460    /// (disequality), the ArithSolver learns a strict ordering constraint
1461    /// (Lt or Gt) and doesn't assign equal values.
1462    ///
1463    /// Only called for MBQI instantiation results, not for all assertions,
1464    /// to avoid blowing up the clause database on non-quantified problems.
1465    pub(super) fn add_arith_eq_trichotomy(&mut self, term: TermId, manager: &mut TermManager) {
1466        let mut visited = FxHashSet::default();
1467        self.add_arith_eq_trichotomy_recursive(term, manager, &mut visited);
1468    }
1469
1470    fn add_arith_eq_trichotomy_recursive(
1471        &mut self,
1472        term: TermId,
1473        manager: &mut TermManager,
1474        visited: &mut FxHashSet<TermId>,
1475    ) {
1476        if !visited.insert(term) {
1477            return;
1478        }
1479
1480        let Some(t) = manager.get(term).cloned() else {
1481            return;
1482        };
1483
1484        match &t.kind {
1485            TermKind::Eq(lhs, rhs) => {
1486                let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1487                    lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1488                });
1489                // Only add trichotomy when at least one side is an
1490                // uninterpreted function application (Apply). This is the
1491                // pattern that appears in injectivity / congruence axioms
1492                // where f(a)=f(b) needs to be split into f(a)<f(b) or
1493                // f(a)>f(b) when the equality is false.
1494                // Avoid Select terms -- the array theory handles those.
1495                let lhs_is_apply = manager
1496                    .get(*lhs)
1497                    .is_some_and(|lt| matches!(lt.kind, TermKind::Apply { .. }));
1498                let rhs_is_apply = manager
1499                    .get(*rhs)
1500                    .is_some_and(|rt| matches!(rt.kind, TermKind::Apply { .. }));
1501                if lhs_is_numeric && (lhs_is_apply || rhs_is_apply) {
1502                    let (l, r) = (*lhs, *rhs);
1503                    // Add trichotomy: Eq(a,b) OR Lt(a,b) OR Gt(a,b)
1504                    let eq_var = self.get_or_create_var(term);
1505                    let eq_lit = Lit::pos(eq_var);
1506                    let lt_term = manager.mk_lt(l, r);
1507                    let gt_term = manager.mk_gt(l, r);
1508                    let lt_lit = self.encode(lt_term, manager);
1509                    let gt_lit = self.encode(gt_term, manager);
1510                    self.sat.add_clause([eq_lit, lt_lit, gt_lit]);
1511                }
1512            }
1513            TermKind::Not(arg) => {
1514                self.add_arith_eq_trichotomy_recursive(*arg, manager, visited);
1515            }
1516            TermKind::And(args) => {
1517                let args_clone: Vec<TermId> = args.iter().copied().collect();
1518                for arg in args_clone {
1519                    self.add_arith_eq_trichotomy_recursive(arg, manager, visited);
1520                }
1521            }
1522            TermKind::Or(args) => {
1523                let args_clone: Vec<TermId> = args.iter().copied().collect();
1524                for arg in args_clone {
1525                    self.add_arith_eq_trichotomy_recursive(arg, manager, visited);
1526                }
1527            }
1528            TermKind::Implies(lhs, rhs) => {
1529                let (l, r) = (*lhs, *rhs);
1530                self.add_arith_eq_trichotomy_recursive(l, manager, visited);
1531                self.add_arith_eq_trichotomy_recursive(r, manager, visited);
1532            }
1533            TermKind::Ite(_, then_br, else_br) => {
1534                let (t, e) = (*then_br, *else_br);
1535                self.add_arith_eq_trichotomy_recursive(t, manager, visited);
1536                self.add_arith_eq_trichotomy_recursive(e, manager, visited);
1537            }
1538            _ => {}
1539        }
1540    }
1541
1542    /// Recursively walk a term to find all `Not(Eq(a, b))` sub-terms with
1543    /// arithmetic sorts and add the split `(a < b) OR (a > b)` for each.
1544    ///
1545    /// This handles MBQI instantiation results that are implications like
1546    /// `(=> guard (not (= a b)))` where the disequality is nested inside
1547    /// the formula rather than at the top level.
1548    fn add_arith_diseq_split_recursive(
1549        &mut self,
1550        term: TermId,
1551        manager: &mut TermManager,
1552        visited: &mut FxHashSet<TermId>,
1553    ) {
1554        if !visited.insert(term) {
1555            return;
1556        }
1557
1558        let Some(t) = manager.get(term).cloned() else {
1559            return;
1560        };
1561
1562        match &t.kind {
1563            TermKind::Not(inner) => {
1564                let inner_id = *inner;
1565                if let Some(inner_t) = manager.get(inner_id).cloned() {
1566                    if let TermKind::Eq(lhs, rhs) = &inner_t.kind {
1567                        let lhs_is_numeric = manager.get(*lhs).is_some_and(|lt| {
1568                            lt.sort == manager.sorts.int_sort || lt.sort == manager.sorts.real_sort
1569                        });
1570                        if lhs_is_numeric {
1571                            let (l, r) = (*lhs, *rhs);
1572                            // Build Lt(lhs, rhs) and Gt(lhs, rhs)
1573                            let lt_term = manager.mk_lt(l, r);
1574                            let gt_term = manager.mk_gt(l, r);
1575
1576                            // Encode both and add the disjunction
1577                            let lt_lit = self.encode(lt_term, manager);
1578                            let gt_lit = self.encode(gt_term, manager);
1579                            self.sat.add_clause([lt_lit, gt_lit]);
1580                        }
1581                    }
1582                }
1583                // Also recurse into the inner term
1584                self.add_arith_diseq_split_recursive(inner_id, manager, visited);
1585            }
1586            TermKind::And(args) => {
1587                let args_clone: Vec<TermId> = args.iter().copied().collect();
1588                for arg in args_clone {
1589                    self.add_arith_diseq_split_recursive(arg, manager, visited);
1590                }
1591            }
1592            TermKind::Or(args) => {
1593                let args_clone: Vec<TermId> = args.iter().copied().collect();
1594                for arg in args_clone {
1595                    self.add_arith_diseq_split_recursive(arg, manager, visited);
1596                }
1597            }
1598            TermKind::Implies(_, rhs) => {
1599                // Recurse into the consequent -- that's where the disequality
1600                // typically lives in quantifier instantiation lemmas
1601                let rhs_id = *rhs;
1602                self.add_arith_diseq_split_recursive(rhs_id, manager, visited);
1603            }
1604            TermKind::Ite(_, then_br, else_br) => {
1605                let (t, e) = (*then_br, *else_br);
1606                self.add_arith_diseq_split_recursive(t, manager, visited);
1607                self.add_arith_diseq_split_recursive(e, manager, visited);
1608            }
1609            _ => {}
1610        }
1611    }
1612}