Skip to main content

oxiz_solver/mbqi/
counterexample.rs

1//! Counter-example Generation and Refinement
2//!
3//! This module implements counterexample generation for MBQI. A counterexample
4//! is an assignment to quantified variables that falsifies the quantifier body
5//! under the current model.
6//!
7//! For a universal quantifier ∀x.φ(x), a counterexample is an assignment σ such
8//! that ¬φ(σ(x)) holds in the current model.
9//!
10//! # Strategy
11//!
12//! 1. **Model Evaluation**: Evaluate quantifier body under candidate assignments
13//! 2. **Satisfiability Checking**: Use auxiliary solver to find counterexamples
14//! 3. **Conflict Analysis**: When no counterexamples exist, analyze why
15//! 4. **Refinement**: Use counterexamples to refine the search space
16
17#[allow(unused_imports)]
18use crate::prelude::*;
19use core::fmt;
20use num_bigint::BigInt;
21use oxiz_core::ast::{TermId, TermKind, TermManager};
22use oxiz_core::interner::Spur;
23use oxiz_core::sort::SortId;
24use smallvec::SmallVec;
25#[cfg(feature = "std")]
26use std::time::{Duration, Instant};
27
28use super::model_completion::CompletedModel;
29use super::{Instantiation, InstantiationReason, QuantifiedFormula};
30
31/// A counter-example to a quantified formula
32#[derive(Debug, Clone)]
33pub struct CounterExample {
34    /// The quantifier this is a counterexample for
35    pub quantifier: TermId,
36    /// Assignment to bound variables
37    pub assignment: FxHashMap<Spur, TermId>,
38    /// Witness terms (the concrete values assigned)
39    pub witnesses: Vec<TermId>,
40    /// Evaluation of the body under this assignment
41    pub body_value: Option<TermId>,
42    /// Quality score (higher = better counterexample)
43    pub quality: f64,
44    /// Generation at which this was found
45    pub generation: u32,
46}
47
48impl CounterExample {
49    /// Create a new counter-example
50    pub fn new(
51        quantifier: TermId,
52        assignment: FxHashMap<Spur, TermId>,
53        witnesses: Vec<TermId>,
54        generation: u32,
55    ) -> Self {
56        Self {
57            quantifier,
58            assignment,
59            witnesses,
60            body_value: None,
61            quality: 1.0,
62            generation,
63        }
64    }
65
66    /// Convert to an instantiation
67    pub fn to_instantiation(&self, result: TermId) -> Instantiation {
68        Instantiation::with_reason(
69            self.quantifier,
70            self.assignment.clone(),
71            result,
72            self.generation,
73            InstantiationReason::Conflict,
74        )
75    }
76
77    /// Calculate quality score based on term complexity
78    pub fn calculate_quality(&mut self, manager: &TermManager) {
79        let mut total_size = 0;
80        let mut num_constants = 0;
81
82        for &witness in &self.witnesses {
83            let size = self.term_size(witness, manager);
84            total_size += size;
85
86            if self.is_constant(witness, manager) {
87                num_constants += 1;
88            }
89        }
90
91        // Prefer simpler terms (smaller size)
92        let size_factor = 1.0 / (1.0 + total_size as f64);
93        // Prefer more constants (ground terms)
94        let const_factor = 1.0 + (num_constants as f64 / self.witnesses.len().max(1) as f64);
95
96        self.quality = size_factor * const_factor;
97    }
98
99    fn term_size(&self, term: TermId, manager: &TermManager) -> usize {
100        let mut visited = FxHashSet::default();
101        self.term_size_rec(term, manager, &mut visited)
102    }
103
104    fn term_size_rec(
105        &self,
106        term: TermId,
107        manager: &TermManager,
108        visited: &mut FxHashSet<TermId>,
109    ) -> usize {
110        if visited.contains(&term) {
111            return 0;
112        }
113        visited.insert(term);
114
115        let Some(t) = manager.get(term) else {
116            return 1;
117        };
118
119        let children_size = match &t.kind {
120            TermKind::And(args) | TermKind::Or(args) => args
121                .iter()
122                .map(|&arg| self.term_size_rec(arg, manager, visited))
123                .sum(),
124            TermKind::Not(arg) | TermKind::Neg(arg) => self.term_size_rec(*arg, manager, visited),
125            TermKind::Eq(lhs, rhs) | TermKind::Lt(lhs, rhs) => {
126                self.term_size_rec(*lhs, manager, visited)
127                    + self.term_size_rec(*rhs, manager, visited)
128            }
129            _ => 0,
130        };
131
132        1 + children_size
133    }
134
135    fn is_constant(&self, term: TermId, manager: &TermManager) -> bool {
136        let Some(t) = manager.get(term) else {
137            return false;
138        };
139
140        matches!(
141            t.kind,
142            TermKind::True
143                | TermKind::False
144                | TermKind::IntConst(_)
145                | TermKind::RealConst(_)
146                | TermKind::BitVecConst { .. }
147        )
148    }
149}
150
151/// Result of counterexample generation for a single quantifier
152#[derive(Debug, Clone)]
153pub struct CexGenerationResult {
154    /// Counterexamples found
155    pub counterexamples: Vec<CounterExample>,
156    /// Whether all candidate evaluations resolved to concrete boolean values.
157    /// If false, some evaluations produced symbolic residuals, meaning we cannot
158    /// be sure the quantifier is satisfied even if no counterexamples were found.
159    pub all_evaluations_ground: bool,
160}
161
162/// Counter-example generator
163#[derive(Debug)]
164pub struct CounterExampleGenerator {
165    /// Maximum number of counterexamples to generate per quantifier
166    max_cex_per_quantifier: usize,
167    /// Maximum number of candidates to try per variable
168    max_candidates_per_var: usize,
169    /// Maximum total search time
170    #[cfg(feature = "std")]
171    max_search_time: Duration,
172    /// Current generation bound for term selection
173    generation_bound: u32,
174    /// Statistics
175    stats: CexStats,
176    /// Candidate cache
177    candidate_cache: FxHashMap<SortId, Vec<TermId>>,
178}
179
180impl CounterExampleGenerator {
181    /// Create a new counterexample generator
182    pub fn new() -> Self {
183        Self {
184            max_cex_per_quantifier: 5,
185            max_candidates_per_var: 10,
186            #[cfg(feature = "std")]
187            max_search_time: Duration::from_secs(1),
188            generation_bound: 0,
189            stats: CexStats::default(),
190            candidate_cache: FxHashMap::default(),
191        }
192    }
193
194    /// Create with custom limits
195    #[cfg(feature = "std")]
196    pub fn with_limits(max_cex: usize, max_candidates: usize, max_time: Duration) -> Self {
197        let mut generator = Self::new();
198        generator.max_cex_per_quantifier = max_cex;
199        generator.max_candidates_per_var = max_candidates;
200        generator.max_search_time = max_time;
201        generator
202    }
203
204    /// Generate counterexamples for a quantifier.
205    ///
206    /// Returns a `CexGenerationResult` containing the counterexamples found and
207    /// a flag indicating whether all evaluations resolved to concrete booleans.
208    pub fn generate(
209        &mut self,
210        quantifier: &QuantifiedFormula,
211        model: &CompletedModel,
212        manager: &mut TermManager,
213    ) -> CexGenerationResult {
214        #[cfg(feature = "std")]
215        let start_time = Instant::now();
216        let mut counterexamples = Vec::new();
217        let mut all_ground = true;
218        self.stats.num_searches += 1;
219
220        // Build candidate lists for each bound variable
221        let candidates = self.build_candidate_lists(&quantifier.bound_vars, model, manager);
222
223        // Enumerate combinations of candidates
224        let combinations = self.enumerate_combinations(
225            &candidates,
226            self.max_candidates_per_var,
227            self.max_cex_per_quantifier * 20, // Generate more combinations than we need
228        );
229
230        self.stats.num_combinations_tried += combinations.len();
231
232        // If there are no combinations to try, we cannot verify the quantifier
233        // is satisfied -- mark as non-ground.
234        if combinations.is_empty() {
235            all_ground = false;
236        }
237
238        for combo in combinations {
239            #[cfg(feature = "std")]
240            if start_time.elapsed() > self.max_search_time {
241                self.stats.num_timeouts += 1;
242                // Timeout means we could not fully verify -- not ground.
243                all_ground = false;
244                break;
245            }
246
247            if counterexamples.len() >= self.max_cex_per_quantifier {
248                break;
249            }
250
251            // Build assignment from combination
252            let mut assignment = FxHashMap::default();
253            for (i, &candidate) in combo.iter().enumerate() {
254                if let Some(var_name) = quantifier.var_name(i) {
255                    assignment.insert(var_name, candidate);
256                }
257            }
258
259            // Apply substitution and evaluate
260            let substituted = self.apply_substitution(quantifier.body, &assignment, manager);
261            let evaluated = self.evaluate_under_model(substituted, model, manager);
262
263            // Track whether this evaluation resolved to a concrete boolean
264            if !self.is_ground_boolean(evaluated, manager) {
265                all_ground = false;
266            }
267
268            // Check if this is a counterexample
269            if self.is_counterexample(evaluated, quantifier.is_universal, manager) {
270                let mut cex =
271                    CounterExample::new(quantifier.term, assignment, combo, model.generation);
272                cex.body_value = Some(evaluated);
273                cex.calculate_quality(manager);
274                counterexamples.push(cex);
275                self.stats.num_counterexamples_found += 1;
276            }
277        }
278
279        // Sort by quality (best first)
280        counterexamples.sort_by(|a, b| {
281            b.quality
282                .partial_cmp(&a.quality)
283                .unwrap_or(core::cmp::Ordering::Equal)
284        });
285
286        // Limit to max
287        counterexamples.truncate(self.max_cex_per_quantifier);
288
289        #[cfg(feature = "std")]
290        {
291            self.stats.total_time += start_time.elapsed();
292        }
293
294        CexGenerationResult {
295            counterexamples,
296            all_evaluations_ground: all_ground,
297        }
298    }
299
300    /// Build candidate lists for bound variables
301    fn build_candidate_lists(
302        &mut self,
303        bound_vars: &[(Spur, SortId)],
304        model: &CompletedModel,
305        manager: &mut TermManager,
306    ) -> Vec<Vec<TermId>> {
307        let mut result = Vec::new();
308
309        for &(_var_name, sort) in bound_vars {
310            // Check cache first
311            if let Some(cached) = self.candidate_cache.get(&sort) {
312                result.push(cached.clone());
313                continue;
314            }
315
316            let mut candidates = Vec::new();
317
318            // Strategy 1: Use values from the universe (for uninterpreted sorts)
319            if let Some(universe) = model.universe(sort) {
320                candidates.extend_from_slice(universe);
321            }
322
323            // Strategy 2: Use values from the model
324            for (&term, &value) in &model.assignments {
325                if let Some(t) = manager.get(term)
326                    && t.sort == sort
327                    && !candidates.contains(&value)
328                {
329                    candidates.push(value);
330                }
331            }
332
333            // Strategy 3: Add default values based on sort
334            self.add_default_candidates(sort, &mut candidates, manager);
335
336            // Limit candidates
337            candidates.truncate(self.max_candidates_per_var);
338
339            // Cache for future use
340            self.candidate_cache.insert(sort, candidates.clone());
341
342            result.push(candidates);
343        }
344
345        result
346    }
347
348    /// Add default candidate values for a sort
349    fn add_default_candidates(
350        &self,
351        sort: SortId,
352        candidates: &mut Vec<TermId>,
353        manager: &mut TermManager,
354    ) {
355        if sort == manager.sorts.int_sort {
356            // Add small integers
357            for i in -2..=5 {
358                let val = manager.mk_int(BigInt::from(i));
359                if !candidates.contains(&val) {
360                    candidates.push(val);
361                }
362            }
363        } else if sort == manager.sorts.bool_sort {
364            let true_val = manager.mk_true();
365            let false_val = manager.mk_false();
366            if !candidates.contains(&true_val) {
367                candidates.push(true_val);
368            }
369            if !candidates.contains(&false_val) {
370                candidates.push(false_val);
371            }
372        }
373    }
374
375    /// Enumerate combinations of candidates
376    fn enumerate_combinations(
377        &self,
378        candidates: &[Vec<TermId>],
379        max_per_dim: usize,
380        max_total: usize,
381    ) -> Vec<Vec<TermId>> {
382        if candidates.is_empty() {
383            return vec![vec![]];
384        }
385
386        let mut results = Vec::new();
387        let mut indices = vec![0usize; candidates.len()];
388
389        loop {
390            // Build current combination
391            let combo: Vec<TermId> = indices
392                .iter()
393                .enumerate()
394                .filter_map(|(i, &idx)| candidates.get(i).and_then(|c| c.get(idx).copied()))
395                .collect();
396
397            if combo.len() == candidates.len() {
398                results.push(combo);
399            }
400
401            if results.len() >= max_total {
402                break;
403            }
404
405            // Increment indices (like odometer)
406            let mut carry = true;
407            for (i, idx) in indices.iter_mut().enumerate() {
408                if carry {
409                    *idx += 1;
410                    let limit = candidates.get(i).map_or(1, |c| c.len().min(max_per_dim));
411                    if *idx >= limit {
412                        *idx = 0;
413                    } else {
414                        carry = false;
415                    }
416                }
417            }
418
419            if carry {
420                // Overflow - tried all combinations
421                break;
422            }
423        }
424
425        results
426    }
427
428    /// Apply substitution to a term
429    fn apply_substitution(
430        &self,
431        term: TermId,
432        subst: &FxHashMap<Spur, TermId>,
433        manager: &mut TermManager,
434    ) -> TermId {
435        let mut cache = FxHashMap::default();
436        self.apply_substitution_cached(term, subst, manager, &mut cache)
437    }
438
439    fn apply_substitution_cached(
440        &self,
441        term: TermId,
442        subst: &FxHashMap<Spur, TermId>,
443        manager: &mut TermManager,
444        cache: &mut FxHashMap<TermId, TermId>,
445    ) -> TermId {
446        if let Some(&cached) = cache.get(&term) {
447            return cached;
448        }
449
450        let Some(t) = manager.get(term).cloned() else {
451            return term;
452        };
453
454        let result = match &t.kind {
455            TermKind::Var(name) => {
456                // Check if this variable should be substituted
457                subst.get(name).copied().unwrap_or(term)
458            }
459            TermKind::Not(arg) => {
460                let new_arg = self.apply_substitution_cached(*arg, subst, manager, cache);
461                manager.mk_not(new_arg)
462            }
463            TermKind::And(args) => {
464                let new_args: Vec<_> = args
465                    .iter()
466                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
467                    .collect();
468                manager.mk_and(new_args)
469            }
470            TermKind::Or(args) => {
471                let new_args: Vec<_> = args
472                    .iter()
473                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
474                    .collect();
475                manager.mk_or(new_args)
476            }
477            TermKind::Implies(lhs, rhs) => {
478                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
479                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
480                manager.mk_implies(new_lhs, new_rhs)
481            }
482            TermKind::Eq(lhs, rhs) => {
483                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
484                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
485                manager.mk_eq(new_lhs, new_rhs)
486            }
487            TermKind::Lt(lhs, rhs) => {
488                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
489                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
490                manager.mk_lt(new_lhs, new_rhs)
491            }
492            TermKind::Le(lhs, rhs) => {
493                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
494                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
495                manager.mk_le(new_lhs, new_rhs)
496            }
497            TermKind::Gt(lhs, rhs) => {
498                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
499                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
500                manager.mk_gt(new_lhs, new_rhs)
501            }
502            TermKind::Ge(lhs, rhs) => {
503                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
504                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
505                manager.mk_ge(new_lhs, new_rhs)
506            }
507            TermKind::Add(args) => {
508                let new_args: SmallVec<[TermId; 4]> = args
509                    .iter()
510                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
511                    .collect();
512                manager.mk_add(new_args)
513            }
514            TermKind::Sub(lhs, rhs) => {
515                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
516                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
517                manager.mk_sub(new_lhs, new_rhs)
518            }
519            TermKind::Mul(args) => {
520                let new_args: SmallVec<[TermId; 4]> = args
521                    .iter()
522                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
523                    .collect();
524                manager.mk_mul(new_args)
525            }
526            TermKind::Div(lhs, rhs) => {
527                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
528                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
529                manager.mk_div(new_lhs, new_rhs)
530            }
531            TermKind::Mod(lhs, rhs) => {
532                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
533                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
534                manager.mk_mod(new_lhs, new_rhs)
535            }
536            TermKind::Neg(arg) => {
537                let new_arg = self.apply_substitution_cached(*arg, subst, manager, cache);
538                manager.mk_neg(new_arg)
539            }
540            TermKind::Ite(cond, then_br, else_br) => {
541                let new_cond = self.apply_substitution_cached(*cond, subst, manager, cache);
542                let new_then = self.apply_substitution_cached(*then_br, subst, manager, cache);
543                let new_else = self.apply_substitution_cached(*else_br, subst, manager, cache);
544                manager.mk_ite(new_cond, new_then, new_else)
545            }
546            TermKind::Apply { func, args } => {
547                let func_name = manager.resolve_str(*func).to_string();
548                let new_args: SmallVec<[TermId; 4]> = args
549                    .iter()
550                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
551                    .collect();
552                manager.mk_apply(&func_name, new_args, t.sort)
553            }
554            // Array select: recurse into both array and index so that bound
555            // variables appearing in the index (e.g., `select(a, i)`) are
556            // properly substituted.
557            TermKind::Select(array, index) => {
558                let new_array = self.apply_substitution_cached(*array, subst, manager, cache);
559                let new_index = self.apply_substitution_cached(*index, subst, manager, cache);
560                manager.mk_select(new_array, new_index)
561            }
562            // Array store: substitute in array, index, and value.
563            TermKind::Store(array, index, value) => {
564                let new_array = self.apply_substitution_cached(*array, subst, manager, cache);
565                let new_index = self.apply_substitution_cached(*index, subst, manager, cache);
566                let new_value = self.apply_substitution_cached(*value, subst, manager, cache);
567                manager.mk_store(new_array, new_index, new_value)
568            }
569            // Constants and other terms don't need substitution
570            _ => term,
571        };
572
573        cache.insert(term, result);
574        result
575    }
576
577    /// Evaluate a term under a model
578    fn evaluate_under_model(
579        &self,
580        term: TermId,
581        model: &CompletedModel,
582        manager: &mut TermManager,
583    ) -> TermId {
584        let mut cache = FxHashMap::default();
585        self.evaluate_under_model_cached(term, model, manager, &mut cache)
586    }
587
588    fn evaluate_under_model_cached(
589        &self,
590        term: TermId,
591        model: &CompletedModel,
592        manager: &mut TermManager,
593        cache: &mut FxHashMap<TermId, TermId>,
594    ) -> TermId {
595        if let Some(&cached) = cache.get(&term) {
596            return cached;
597        }
598
599        // Check if we have a direct model value
600        if let Some(val) = model.eval(term) {
601            cache.insert(term, val);
602            return val;
603        }
604
605        let Some(t) = manager.get(term).cloned() else {
606            return term;
607        };
608
609        let result = match &t.kind {
610            // Constants evaluate to themselves
611            TermKind::True
612            | TermKind::False
613            | TermKind::IntConst(_)
614            | TermKind::RealConst(_)
615            | TermKind::BitVecConst { .. }
616            | TermKind::StringLit(_) => term,
617
618            // Boolean connectives
619            TermKind::Not(arg) => {
620                let eval_arg = self.evaluate_under_model_cached(*arg, model, manager, cache);
621                if let Some(arg_t) = manager.get(eval_arg) {
622                    match arg_t.kind {
623                        TermKind::True => manager.mk_false(),
624                        TermKind::False => manager.mk_true(),
625                        _ => manager.mk_not(eval_arg),
626                    }
627                } else {
628                    manager.mk_not(eval_arg)
629                }
630            }
631            TermKind::And(args) => {
632                let mut all_true = true;
633                for &arg in args.iter() {
634                    let eval_arg = self.evaluate_under_model_cached(arg, model, manager, cache);
635                    if let Some(arg_t) = manager.get(eval_arg) {
636                        match arg_t.kind {
637                            TermKind::False => {
638                                let false_val = manager.mk_false();
639                                cache.insert(term, false_val);
640                                return false_val;
641                            }
642                            TermKind::True => { /* continue */ }
643                            _ => {
644                                all_true = false;
645                            }
646                        }
647                    } else {
648                        all_true = false;
649                    }
650                }
651                if all_true {
652                    manager.mk_true()
653                } else {
654                    // Not fully evaluated -- return symbolic
655                    term
656                }
657            }
658            TermKind::Or(args) => {
659                let mut all_false = true;
660                for &arg in args.iter() {
661                    let eval_arg = self.evaluate_under_model_cached(arg, model, manager, cache);
662                    if let Some(arg_t) = manager.get(eval_arg) {
663                        match arg_t.kind {
664                            TermKind::True => {
665                                let true_val = manager.mk_true();
666                                cache.insert(term, true_val);
667                                return true_val;
668                            }
669                            TermKind::False => { /* continue */ }
670                            _ => {
671                                all_false = false;
672                            }
673                        }
674                    } else {
675                        all_false = false;
676                    }
677                }
678                if all_false { manager.mk_false() } else { term }
679            }
680            TermKind::Implies(lhs, rhs) => {
681                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
682                if let Some(lhs_t) = manager.get(eval_lhs) {
683                    match lhs_t.kind {
684                        TermKind::False => manager.mk_true(),
685                        TermKind::True => {
686                            self.evaluate_under_model_cached(*rhs, model, manager, cache)
687                        }
688                        _ => {
689                            let eval_rhs =
690                                self.evaluate_under_model_cached(*rhs, model, manager, cache);
691                            manager.mk_implies(eval_lhs, eval_rhs)
692                        }
693                    }
694                } else {
695                    let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
696                    manager.mk_implies(eval_lhs, eval_rhs)
697                }
698            }
699
700            // Comparisons
701            TermKind::Eq(lhs, rhs) => {
702                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
703                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
704                self.eval_eq(eval_lhs, eval_rhs, manager)
705            }
706            TermKind::Lt(lhs, rhs) => {
707                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
708                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
709                self.eval_lt(eval_lhs, eval_rhs, manager)
710            }
711            TermKind::Le(lhs, rhs) => {
712                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
713                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
714                self.eval_le(eval_lhs, eval_rhs, manager)
715            }
716            TermKind::Gt(lhs, rhs) => {
717                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
718                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
719                self.eval_gt(eval_lhs, eval_rhs, manager)
720            }
721            TermKind::Ge(lhs, rhs) => {
722                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
723                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
724                self.eval_ge(eval_lhs, eval_rhs, manager)
725            }
726
727            // Arithmetic
728            TermKind::Add(args) => {
729                let eval_args: SmallVec<[TermId; 4]> = args
730                    .iter()
731                    .map(|&a| self.evaluate_under_model_cached(a, model, manager, cache))
732                    .collect();
733                self.eval_add(&eval_args, manager)
734            }
735            TermKind::Mul(args) => {
736                let eval_args: SmallVec<[TermId; 4]> = args
737                    .iter()
738                    .map(|&a| self.evaluate_under_model_cached(a, model, manager, cache))
739                    .collect();
740                self.eval_mul(&eval_args, manager)
741            }
742            TermKind::Sub(lhs, rhs) => {
743                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
744                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
745                self.eval_sub(eval_lhs, eval_rhs, manager)
746            }
747            TermKind::Div(lhs, rhs) => {
748                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
749                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
750                self.eval_div(eval_lhs, eval_rhs, manager)
751            }
752            TermKind::Mod(lhs, rhs) => {
753                let eval_lhs = self.evaluate_under_model_cached(*lhs, model, manager, cache);
754                let eval_rhs = self.evaluate_under_model_cached(*rhs, model, manager, cache);
755                self.eval_modulo(eval_lhs, eval_rhs, manager)
756            }
757            TermKind::Neg(arg) => {
758                let eval_arg = self.evaluate_under_model_cached(*arg, model, manager, cache);
759                self.eval_neg(eval_arg, manager)
760            }
761
762            // If-then-else
763            TermKind::Ite(cond, then_br, else_br) => {
764                let eval_cond = self.evaluate_under_model_cached(*cond, model, manager, cache);
765                if let Some(cond_t) = manager.get(eval_cond) {
766                    match cond_t.kind {
767                        TermKind::True => {
768                            self.evaluate_under_model_cached(*then_br, model, manager, cache)
769                        }
770                        TermKind::False => {
771                            self.evaluate_under_model_cached(*else_br, model, manager, cache)
772                        }
773                        _ => {
774                            // Can't determine branch -- evaluate both and return ite
775                            let eval_then =
776                                self.evaluate_under_model_cached(*then_br, model, manager, cache);
777                            let eval_else =
778                                self.evaluate_under_model_cached(*else_br, model, manager, cache);
779                            manager.mk_ite(eval_cond, eval_then, eval_else)
780                        }
781                    }
782                } else {
783                    term
784                }
785            }
786
787            // Function applications: evaluate args, then look up in function table
788            TermKind::Apply { func, args } => {
789                let evaluated_args: Vec<TermId> = args
790                    .iter()
791                    .map(|&a| self.evaluate_under_model_cached(a, model, manager, cache))
792                    .collect();
793
794                // Try looking up the function in the model's interpretation table
795                if let Some(result) = model.eval_apply(*func, &evaluated_args) {
796                    result
797                } else {
798                    // Rebuild with evaluated args and check model for the new term
799                    let func_name = manager.resolve_str(*func).to_string();
800                    let new_term =
801                        manager.mk_apply(&func_name, evaluated_args.iter().copied(), t.sort);
802                    model.eval(new_term).unwrap_or(new_term)
803                }
804            }
805
806            // Forall: return symbolic -- we only instantiate at the top level
807            TermKind::Forall { .. } => term,
808
809            // Exists: try to find a witness using default candidates.
810            // If all candidates evaluate to False, return False (proven no witness).
811            // If any evaluates to True, return True (witness found).
812            // If mixed (some symbolic), return symbolic (cannot determine).
813            TermKind::Exists {
814                vars,
815                body: exists_body,
816                ..
817            } => {
818                let vars_vec: SmallVec<[(Spur, SortId); 2]> = vars.clone();
819                let body_id = *exists_body;
820                self.evaluate_exists_inline(&vars_vec, body_id, model, manager, cache)
821            }
822
823            // Array select: evaluate index, then try multiple model lookups.
824            //
825            // Key insight: the model stores values keyed by the *original*
826            // term graph (e.g. select(a, 3)), not by any model-evaluated
827            // variant.  If we first evaluate the array `a` via the model
828            // (obtaining some value V) and then build select(V, 3), the
829            // resulting TermId won't match the model's entry for select(a, 3).
830            // Therefore, we first try looking up `select(original_array,
831            // evaluated_index)` before falling back.
832            TermKind::Select(array, index) => {
833                let eval_index = self.evaluate_under_model_cached(*index, model, manager, cache);
834                // Try 1: select(original_array, evaluated_index) — this matches
835                // the term graph created during MBQI instantiation encoding.
836                let select_with_orig_array = manager.mk_select(*array, eval_index);
837                if let Some(val) = model.eval(select_with_orig_array) {
838                    val
839                } else if let Some(val) = model.eval(term) {
840                    // Try 2: the un-modified original term (before substitution)
841                    val
842                } else {
843                    // Try 3: also evaluate the array in case it resolves
844                    let eval_array =
845                        self.evaluate_under_model_cached(*array, model, manager, cache);
846                    let new_select = manager.mk_select(eval_array, eval_index);
847                    if let Some(val) = model.eval(new_select) {
848                        val
849                    } else {
850                        // Select term not in model — return symbolic.
851                        // This will make `all_evaluations_ground` false,
852                        // causing MBQI to return Unknown (not Satisfied),
853                        // which triggers blind instantiation as a fallback.
854                        new_select
855                    }
856                }
857            }
858
859            // Variables that haven't been substituted -- look up in model or return as-is
860            TermKind::Var(_) => model.eval(term).unwrap_or(term),
861
862            // Anything else: try simplification
863            _ => manager.simplify(term),
864        };
865
866        cache.insert(term, result);
867        result
868    }
869
870    /// Evaluate an Exists quantifier inline by trying default candidates.
871    ///
872    /// Returns:
873    /// - True  if any candidate gives a True body evaluation
874    /// - False if ALL candidates give False body evaluations (provably no witness)
875    /// - symbolic term if any evaluation is symbolic (cannot determine)
876    fn evaluate_exists_inline(
877        &self,
878        vars: &SmallVec<[(Spur, SortId); 2]>,
879        body: TermId,
880        model: &CompletedModel,
881        manager: &mut TermManager,
882        parent_cache: &mut FxHashMap<TermId, TermId>,
883    ) -> TermId {
884        // Build default candidate lists for each bound variable
885        let mut candidate_lists: Vec<Vec<TermId>> = Vec::new();
886        for &(_var_name, sort) in vars {
887            let mut cands = Vec::new();
888            // Use universe if available
889            if let Some(universe) = model.universe(sort) {
890                cands.extend_from_slice(universe);
891            }
892            // Add values from model assignments
893            for (&term, &value) in &model.assignments {
894                if let Some(t) = manager.get(term)
895                    && t.sort == sort
896                    && !cands.contains(&value)
897                {
898                    cands.push(value);
899                }
900            }
901            // Add default candidates for known sorts
902            if sort == manager.sorts.int_sort {
903                for i in -2i64..=5 {
904                    let val = manager.mk_int(BigInt::from(i));
905                    if !cands.contains(&val) {
906                        cands.push(val);
907                    }
908                }
909            } else if sort == manager.sorts.bool_sort {
910                let t = manager.mk_true();
911                let f = manager.mk_false();
912                if !cands.contains(&t) {
913                    cands.push(t);
914                }
915                if !cands.contains(&f) {
916                    cands.push(f);
917                }
918            }
919            cands.truncate(self.max_candidates_per_var);
920            candidate_lists.push(cands);
921        }
922
923        if candidate_lists.is_empty() || candidate_lists.iter().any(|c| c.is_empty()) {
924            // No candidates → cannot determine
925            return body; // symbolic
926        }
927
928        // Enumerate combinations (simplified for single-var case; limit total)
929        let total_combos: usize = candidate_lists
930            .iter()
931            .map(|c| c.len())
932            .product::<usize>()
933            .min(50);
934        let mut found_true = false;
935        let mut found_symbolic = false;
936        let mut all_false = true;
937        let mut combo_count = 0;
938
939        // Use the same enumerate_combinations helper logic
940        let mut indices = vec![0usize; candidate_lists.len()];
941        loop {
942            if combo_count >= total_combos {
943                break;
944            }
945            // Build assignment for this combination
946            let mut subst: FxHashMap<Spur, TermId> = FxHashMap::default();
947            for (i, &(var_name, _sort)) in vars.iter().enumerate() {
948                if let Some(&candidate) = candidate_lists[i].get(indices[i]) {
949                    subst.insert(var_name, candidate);
950                }
951            }
952
953            // Apply substitution and evaluate
954            let mut sub_cache: FxHashMap<TermId, TermId> = FxHashMap::default();
955            let substituted = self.apply_substitution_cached(body, &subst, manager, &mut sub_cache);
956            let evaluated =
957                self.evaluate_under_model_cached(substituted, model, manager, parent_cache);
958
959            if let Some(eval_t) = manager.get(evaluated) {
960                match eval_t.kind {
961                    TermKind::True => {
962                        found_true = true;
963                        break; // witness found, no need to continue
964                    }
965                    TermKind::False => {
966                        // this candidate is False, keep checking
967                    }
968                    _ => {
969                        // symbolic
970                        found_symbolic = true;
971                        all_false = false;
972                    }
973                }
974            } else {
975                found_symbolic = true;
976                all_false = false;
977            }
978
979            combo_count += 1;
980
981            // Increment indices (odometer)
982            let mut carry = true;
983            for (i, idx) in indices.iter_mut().enumerate() {
984                if carry {
985                    *idx += 1;
986                    let limit = candidate_lists.get(i).map_or(1, |c| c.len());
987                    if *idx >= limit {
988                        *idx = 0;
989                    } else {
990                        carry = false;
991                    }
992                }
993            }
994            if carry {
995                break; // all combinations tried
996            }
997        }
998
999        if found_true {
1000            manager.mk_true()
1001        } else if all_false && !found_symbolic && combo_count > 0 {
1002            // All candidates gave False: exists is provably False under this model
1003            manager.mk_false()
1004        } else {
1005            // Some candidates were symbolic or we had a witness check issue
1006            body // Return symbolic
1007        }
1008    }
1009
1010    /// Evaluate equality
1011    fn eval_eq(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1012        if lhs == rhs {
1013            return manager.mk_true();
1014        }
1015
1016        let lhs_t = manager.get(lhs);
1017        let rhs_t = manager.get(rhs);
1018
1019        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1020            match (&l.kind, &r.kind) {
1021                (TermKind::IntConst(a), TermKind::IntConst(b)) => {
1022                    if a == b {
1023                        manager.mk_true()
1024                    } else {
1025                        manager.mk_false()
1026                    }
1027                }
1028                (TermKind::RealConst(a), TermKind::RealConst(b)) => {
1029                    if a == b {
1030                        manager.mk_true()
1031                    } else {
1032                        manager.mk_false()
1033                    }
1034                }
1035                (TermKind::True, TermKind::True) | (TermKind::False, TermKind::False) => {
1036                    manager.mk_true()
1037                }
1038                (TermKind::True, TermKind::False) | (TermKind::False, TermKind::True) => {
1039                    manager.mk_false()
1040                }
1041                _ => manager.mk_eq(lhs, rhs),
1042            }
1043        } else {
1044            manager.mk_eq(lhs, rhs)
1045        }
1046    }
1047
1048    /// Evaluate less-than
1049    fn eval_lt(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1050        let lhs_t = manager.get(lhs);
1051        let rhs_t = manager.get(rhs);
1052
1053        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1054            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind) {
1055                if a < b {
1056                    return manager.mk_true();
1057                } else {
1058                    return manager.mk_false();
1059                }
1060            }
1061            if let (TermKind::RealConst(a), TermKind::RealConst(b)) = (&l.kind, &r.kind) {
1062                if a < b {
1063                    return manager.mk_true();
1064                } else {
1065                    return manager.mk_false();
1066                }
1067            }
1068        }
1069
1070        manager.mk_lt(lhs, rhs)
1071    }
1072
1073    /// Evaluate less-than-or-equal
1074    fn eval_le(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1075        let lhs_t = manager.get(lhs);
1076        let rhs_t = manager.get(rhs);
1077
1078        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1079            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind) {
1080                if a <= b {
1081                    return manager.mk_true();
1082                } else {
1083                    return manager.mk_false();
1084                }
1085            }
1086            if let (TermKind::RealConst(a), TermKind::RealConst(b)) = (&l.kind, &r.kind) {
1087                if a <= b {
1088                    return manager.mk_true();
1089                } else {
1090                    return manager.mk_false();
1091                }
1092            }
1093        }
1094
1095        manager.mk_le(lhs, rhs)
1096    }
1097
1098    /// Evaluate greater-than
1099    fn eval_gt(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1100        self.eval_lt(rhs, lhs, manager)
1101    }
1102
1103    /// Evaluate greater-than-or-equal
1104    fn eval_ge(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1105        self.eval_le(rhs, lhs, manager)
1106    }
1107
1108    /// Evaluate addition
1109    fn eval_add(&self, args: &[TermId], manager: &mut TermManager) -> TermId {
1110        let mut result = BigInt::from(0);
1111        let mut all_ints = true;
1112
1113        for &arg in args {
1114            if let Some(arg_t) = manager.get(arg) {
1115                if let TermKind::IntConst(val) = &arg_t.kind {
1116                    result += val;
1117                } else {
1118                    all_ints = false;
1119                    break;
1120                }
1121            } else {
1122                all_ints = false;
1123                break;
1124            }
1125        }
1126
1127        if all_ints {
1128            manager.mk_int(result)
1129        } else {
1130            manager.mk_add(args.iter().copied())
1131        }
1132    }
1133
1134    /// Evaluate multiplication
1135    fn eval_mul(&self, args: &[TermId], manager: &mut TermManager) -> TermId {
1136        let mut result = BigInt::from(1);
1137        let mut all_ints = true;
1138
1139        for &arg in args {
1140            if let Some(arg_t) = manager.get(arg) {
1141                if let TermKind::IntConst(val) = &arg_t.kind {
1142                    result *= val;
1143                } else {
1144                    all_ints = false;
1145                    break;
1146                }
1147            } else {
1148                all_ints = false;
1149                break;
1150            }
1151        }
1152
1153        if all_ints {
1154            manager.mk_int(result)
1155        } else {
1156            manager.mk_mul(args.iter().copied())
1157        }
1158    }
1159
1160    /// Evaluate subtraction
1161    fn eval_sub(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1162        let lhs_t = manager.get(lhs);
1163        let rhs_t = manager.get(rhs);
1164
1165        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1166            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind) {
1167                return manager.mk_int(a - b);
1168            }
1169        }
1170
1171        manager.mk_sub(lhs, rhs)
1172    }
1173
1174    /// Evaluate integer division
1175    fn eval_div(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1176        let lhs_t = manager.get(lhs);
1177        let rhs_t = manager.get(rhs);
1178
1179        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1180            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind) {
1181                if *b != BigInt::from(0) {
1182                    return manager.mk_int(a / b);
1183                }
1184            }
1185        }
1186
1187        manager.mk_div(lhs, rhs)
1188    }
1189
1190    /// Evaluate modulo
1191    fn eval_modulo(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1192        let lhs_t = manager.get(lhs);
1193        let rhs_t = manager.get(rhs);
1194
1195        if let (Some(l), Some(r)) = (lhs_t, rhs_t) {
1196            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&l.kind, &r.kind) {
1197                if *b != BigInt::from(0) {
1198                    return manager.mk_int(a % b);
1199                }
1200            }
1201        }
1202
1203        manager.mk_mod(lhs, rhs)
1204    }
1205
1206    /// Evaluate negation
1207    fn eval_neg(&self, arg: TermId, manager: &mut TermManager) -> TermId {
1208        if let Some(arg_t) = manager.get(arg) {
1209            if let TermKind::IntConst(val) = &arg_t.kind {
1210                return manager.mk_int(-val);
1211            }
1212        }
1213
1214        manager.mk_neg(arg)
1215    }
1216
1217    /// Check if an evaluated term is a counterexample.
1218    ///
1219    /// Policy:
1220    /// - For ∀x.φ(x): only a CONCRETE False means "definitely a counterexample".
1221    ///   True => definitely not a counterexample (model satisfies this instance).
1222    ///   Symbolic residual => not a counterexample; the instance is unknown.
1223    ///   This avoids generating spurious instantiation lemmas for symbolic
1224    ///   evaluations (e.g. unconstrained select terms), which would create
1225    ///   unnecessary arithmetic constraints and cause false UNSAT.
1226    /// - For ∃x.φ(x): only a concrete True means "definitely a witness".
1227    ///   False or symbolic => not a witness (conservative).
1228    fn is_counterexample(
1229        &self,
1230        evaluated: TermId,
1231        is_universal: bool,
1232        manager: &TermManager,
1233    ) -> bool {
1234        let Some(eval_t) = manager.get(evaluated) else {
1235            // Cannot resolve the term at all -- not a counterexample (unknown).
1236            return false;
1237        };
1238
1239        if is_universal {
1240            // For ∀x.φ(x): only concrete False is a genuine counterexample.
1241            // Symbolic residuals (neither True nor False) mean we could not
1242            // evaluate the body under the current model -- do NOT treat these
1243            // as counterexamples to avoid injecting unnecessary lemmas.
1244            matches!(eval_t.kind, TermKind::False)
1245        } else {
1246            // For ∃x.φ(x): only concrete True counts as a witness.
1247            matches!(eval_t.kind, TermKind::True)
1248        }
1249    }
1250
1251    /// Check whether an evaluated term resolved to a concrete boolean value
1252    /// (True or False) as opposed to a symbolic residual.
1253    fn is_ground_boolean(&self, evaluated: TermId, manager: &TermManager) -> bool {
1254        let Some(eval_t) = manager.get(evaluated) else {
1255            return false;
1256        };
1257        matches!(eval_t.kind, TermKind::True | TermKind::False)
1258    }
1259
1260    /// Set generation bound for candidate selection
1261    pub fn set_generation_bound(&mut self, bound: u32) {
1262        self.generation_bound = bound;
1263    }
1264
1265    /// Inject extra candidates from outside (e.g. Skolem function applications).
1266    ///
1267    /// These are merged into the per-sort candidate cache so they appear in
1268    /// every subsequent `build_candidate_lists` call.
1269    pub fn inject_extra_candidates(&mut self, extras: &FxHashMap<SortId, Vec<TermId>>) {
1270        for (&sort, terms) in extras {
1271            let entry = self.candidate_cache.entry(sort).or_default();
1272            for &t in terms {
1273                if !entry.contains(&t) {
1274                    entry.push(t);
1275                }
1276            }
1277        }
1278    }
1279
1280    /// Clear the candidate cache
1281    pub fn clear_cache(&mut self) {
1282        self.candidate_cache.clear();
1283    }
1284
1285    /// Get statistics
1286    pub fn stats(&self) -> &CexStats {
1287        &self.stats
1288    }
1289}
1290
1291impl Default for CounterExampleGenerator {
1292    fn default() -> Self {
1293        Self::new()
1294    }
1295}
1296
1297/// Refinement strategy for narrowing the search space
1298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1299pub enum RefinementStrategy {
1300    /// No refinement
1301    None,
1302    /// Block found counterexamples
1303    BlockCounterexamples,
1304    /// Learn from conflicts
1305    ConflictLearning,
1306    /// Generalize from counterexamples
1307    Generalization,
1308}
1309
1310/// Statistics for counterexample generation
1311#[derive(Debug, Clone, Default)]
1312pub struct CexStats {
1313    /// Number of search attempts
1314    pub num_searches: usize,
1315    /// Number of counterexamples found
1316    pub num_counterexamples_found: usize,
1317    /// Number of combinations tried
1318    pub num_combinations_tried: usize,
1319    /// Number of timeouts
1320    pub num_timeouts: usize,
1321    /// Total time spent
1322    #[cfg(feature = "std")]
1323    pub total_time: Duration,
1324}
1325
1326impl fmt::Display for CexStats {
1327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1328        writeln!(f, "Counterexample Statistics:")?;
1329        writeln!(f, "  Searches: {}", self.num_searches)?;
1330        writeln!(f, "  CEX found: {}", self.num_counterexamples_found)?;
1331        writeln!(f, "  Combinations tried: {}", self.num_combinations_tried)?;
1332        writeln!(f, "  Timeouts: {}", self.num_timeouts)?;
1333        #[cfg(feature = "std")]
1334        writeln!(f, "  Total time: {:.2}ms", self.total_time.as_millis())?;
1335        Ok(())
1336    }
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342
1343    #[test]
1344    fn test_counterexample_creation() {
1345        let cex = CounterExample::new(TermId::new(1), FxHashMap::default(), vec![], 0);
1346        assert_eq!(cex.quantifier, TermId::new(1));
1347        assert_eq!(cex.quality, 1.0);
1348    }
1349
1350    #[test]
1351    fn test_cex_generator_creation() {
1352        let generator = CounterExampleGenerator::new();
1353        assert_eq!(generator.max_cex_per_quantifier, 5);
1354        assert_eq!(generator.max_candidates_per_var, 10);
1355    }
1356
1357    #[test]
1358    fn test_cex_generator_with_limits() {
1359        let generator = CounterExampleGenerator::with_limits(10, 20, Duration::from_secs(2));
1360        assert_eq!(generator.max_cex_per_quantifier, 10);
1361        assert_eq!(generator.max_candidates_per_var, 20);
1362        assert_eq!(generator.max_search_time, Duration::from_secs(2));
1363    }
1364
1365    #[test]
1366    fn test_enumerate_combinations_empty() {
1367        let generator = CounterExampleGenerator::new();
1368        let combos = generator.enumerate_combinations(&[], 10, 100);
1369        assert_eq!(combos.len(), 1);
1370        assert!(combos[0].is_empty());
1371    }
1372
1373    #[test]
1374    fn test_enumerate_combinations_single() {
1375        let generator = CounterExampleGenerator::new();
1376        let candidates = vec![vec![TermId::new(1), TermId::new(2)]];
1377        let combos = generator.enumerate_combinations(&candidates, 10, 100);
1378        assert_eq!(combos.len(), 2);
1379    }
1380
1381    #[test]
1382    fn test_enumerate_combinations_multiple() {
1383        let generator = CounterExampleGenerator::new();
1384        let candidates = vec![
1385            vec![TermId::new(1), TermId::new(2)],
1386            vec![TermId::new(3), TermId::new(4)],
1387        ];
1388        let combos = generator.enumerate_combinations(&candidates, 10, 100);
1389        assert_eq!(combos.len(), 4); // 2 * 2
1390    }
1391
1392    #[test]
1393    fn test_enumerate_combinations_limit() {
1394        let generator = CounterExampleGenerator::new();
1395        let candidates = vec![
1396            vec![TermId::new(1), TermId::new(2), TermId::new(3)],
1397            vec![TermId::new(4), TermId::new(5), TermId::new(6)],
1398        ];
1399        let combos = generator.enumerate_combinations(&candidates, 10, 5);
1400        assert!(combos.len() <= 5);
1401    }
1402
1403    #[test]
1404    fn test_cex_stats_display() {
1405        let stats = CexStats {
1406            num_searches: 10,
1407            num_counterexamples_found: 5,
1408            num_combinations_tried: 100,
1409            num_timeouts: 1,
1410            total_time: Duration::from_millis(500),
1411        };
1412        let display = format!("{}", stats);
1413        assert!(display.contains("Searches: 10"));
1414        assert!(display.contains("CEX found: 5"));
1415    }
1416
1417    #[test]
1418    fn test_refinement_strategy() {
1419        assert_ne!(
1420            RefinementStrategy::None,
1421            RefinementStrategy::BlockCounterexamples
1422        );
1423    }
1424}