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