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