Skip to main content

oxiz_solver/mbqi/
integration.rs

1//! MBQI Integration with Main Solver
2//!
3//! This module handles the integration of MBQI with the main SMT solver.
4//! It provides callbacks, communication interfaces, and coordination logic.
5
6#![allow(missing_docs)]
7
8#[allow(unused_imports)]
9use crate::prelude::*;
10use core::fmt;
11use oxiz_core::ast::{TermId, TermKind, TermManager};
12use oxiz_core::interner::Spur;
13use oxiz_core::sort::SortId;
14use smallvec::SmallVec;
15#[cfg(feature = "std")]
16use std::time::{Duration, Instant};
17
18use super::conflict_driven::ConflictScores;
19use super::counterexample::CounterExampleGenerator;
20use super::finite_model::FiniteModelFinder;
21use super::heuristics::MBQIBudget;
22use super::instantiation::InstantiationEngine;
23use super::lazy_instantiation::LazyInstantiator;
24use super::model_completion::ModelCompleter;
25use super::{Instantiation, MBQIResult, MBQIStats, QuantifiedFormula, QuantifierId};
26
27/// Callback trait for solver communication
28pub trait SolverCallback: fmt::Debug {
29    /// Called when new instantiations are generated
30    fn on_instantiation(&mut self, inst: &Instantiation);
31
32    /// Called when a conflict is detected
33    fn on_conflict(&mut self, quantifier: TermId, reason: &[TermId]);
34
35    /// Called when MBQI starts a new round
36    fn on_round_start(&mut self, round: usize);
37
38    /// Called when MBQI completes a round
39    fn on_round_end(&mut self, round: usize, result: &MBQIResult);
40
41    /// Check if solver should stop (e.g., timeout)
42    fn should_stop(&self) -> bool;
43}
44
45/// MBQI integration manager
46#[derive(Debug)]
47pub struct MBQIIntegration {
48    /// Model completer
49    model_completer: ModelCompleter,
50    /// Instantiation engine
51    instantiation_engine: InstantiationEngine,
52    /// Lazy instantiator
53    lazy_instantiator: LazyInstantiator,
54    /// Finite model finder
55    finite_model_finder: FiniteModelFinder,
56    /// Counterexample generator
57    cex_generator: CounterExampleGenerator,
58    /// Tracked quantifiers
59    quantifiers: Vec<QuantifiedFormula>,
60    /// Generated instantiations (for deduplication)
61    generated_instantiations: FxHashMap<InstantiationKey, usize>,
62    /// Extra candidate terms per sort (e.g. Skolem function applications)
63    extra_candidates: FxHashMap<SortId, Vec<TermId>>,
64    /// Whether blind instantiation has been attempted (one-shot guard)
65    blind_attempted: bool,
66    /// Current round number
67    current_round: usize,
68    /// Per-round instantiation budget.
69    budget: MBQIBudget,
70    /// Conflict-driven quantifier activity.
71    conflict_scores: ConflictScores,
72    /// Maximum rounds
73    max_rounds: usize,
74    /// Time limit
75    #[cfg(feature = "std")]
76    time_limit: Option<Duration>,
77    /// Start time
78    #[cfg(feature = "std")]
79    start_time: Option<Instant>,
80    /// Statistics
81    stats: MBQIStats,
82}
83
84impl MBQIIntegration {
85    /// Create a new MBQI integration
86    pub fn new() -> Self {
87        Self {
88            model_completer: ModelCompleter::new(),
89            instantiation_engine: InstantiationEngine::new(),
90            lazy_instantiator: LazyInstantiator::new(),
91            finite_model_finder: FiniteModelFinder::new(),
92            cex_generator: CounterExampleGenerator::new(),
93            quantifiers: Vec::new(),
94            generated_instantiations: FxHashMap::default(),
95            extra_candidates: FxHashMap::default(),
96            blind_attempted: false,
97            current_round: 0,
98            budget: MBQIBudget::new(1024),
99            conflict_scores: ConflictScores::new(0.95),
100            max_rounds: 100,
101            #[cfg(feature = "std")]
102            time_limit: Some(Duration::from_secs(60)),
103            #[cfg(feature = "std")]
104            start_time: None,
105            stats: MBQIStats::new(),
106        }
107    }
108
109    /// Add a quantified formula
110    pub fn add_quantifier(&mut self, term: TermId, manager: &TermManager) {
111        let Some(t) = manager.get(term) else {
112            return;
113        };
114
115        match &t.kind {
116            oxiz_core::ast::TermKind::Forall { vars, body, .. } => {
117                let bound_vars: SmallVec<[(Spur, SortId); 4]> = vars.iter().copied().collect();
118                self.quantifiers
119                    .push(QuantifiedFormula::new(term, bound_vars, *body, true));
120                self.stats.num_quantifiers += 1;
121            }
122            oxiz_core::ast::TermKind::Exists { vars, body, .. } => {
123                let bound_vars: SmallVec<[(Spur, SortId); 4]> = vars.iter().copied().collect();
124                self.quantifiers
125                    .push(QuantifiedFormula::new(term, bound_vars, *body, false));
126                self.stats.num_quantifiers += 1;
127            }
128            _ => {}
129        }
130    }
131
132    /// Run MBQI with a partial model implementing the Ge & de Moura (2009) algorithm.
133    ///
134    /// The loop:
135    /// 1. Complete the partial model (fill in defaults, function interps, universes)
136    /// 2. For each tracked quantifier, check against the completed model
137    /// 3. If counterexamples found, generate instantiation lemmas and return them
138    /// 4. If no counterexamples for any quantifier, the model satisfies all -- return SAT
139    pub fn run(
140        &mut self,
141        partial_model: &FxHashMap<TermId, TermId>,
142        manager: &mut TermManager,
143        callback: &mut dyn SolverCallback,
144    ) -> MBQIResult {
145        #[cfg(feature = "std")]
146        {
147            self.start_time = Some(Instant::now());
148        }
149        self.current_round = 0;
150
151        if self.quantifiers.is_empty() {
152            return MBQIResult::NoQuantifiers;
153        }
154
155        // Clear the candidate cache at the start of each MBQI round so that
156        // new ground terms (e.g. Skolem applications like sk(0)) created by
157        // previous instantiation rounds are discovered as fresh candidates.
158        self.cex_generator.clear_cache();
159
160        // Check round limit
161        if self.current_round >= self.max_rounds {
162            self.update_final_stats();
163            return MBQIResult::Unknown;
164        }
165
166        if self.check_timeout() || callback.should_stop() {
167            return MBQIResult::Unknown;
168        }
169
170        self.current_round += 1;
171        if self.current_round > 1 {
172            self.conflict_scores.decay_on_restart();
173        }
174        let quantifier_ids: Vec<QuantifierId> = self.quantifiers.iter().map(|q| q.term).collect();
175        self.budget
176            .carve_per_quantifier(&quantifier_ids, Some(&self.conflict_scores));
177        callback.on_round_start(self.current_round);
178        self.stats.num_checks += 1;
179
180        #[cfg(feature = "std")]
181        let round_start = Instant::now();
182
183        // Step 1: Complete the model with proper Ge & de Moura completion
184        let completed_model =
185            match self
186                .model_completer
187                .complete(partial_model, &self.quantifiers, manager)
188            {
189                Ok(model) => {
190                    self.stats.num_completions += 1;
191                    model
192                }
193                Err(_) => {
194                    callback.on_round_end(self.current_round, &MBQIResult::Unknown);
195                    return MBQIResult::Unknown;
196                }
197            };
198
199        #[cfg(feature = "std")]
200        {
201            self.stats.completion_time_us += round_start.elapsed().as_micros() as u64;
202        }
203
204        // Step 2: Check each quantifier against the completed model
205        //         and generate counterexample-based instantiations
206        #[cfg(feature = "std")]
207        let cex_start = Instant::now();
208        let mut all_instantiations = Vec::new();
209        // Track whether ALL quantifier evaluations resolved to concrete
210        // boolean values.  We can only claim Satisfied when every evaluation
211        // across every quantifier was fully ground (i.e. concrete True).
212        let mut all_evaluations_fully_ground = true;
213
214        // Collect quantifiers first to avoid borrow checker issues
215        let quantifiers: Vec<_> = self.quantifiers.to_vec();
216        for quantifier in &quantifiers {
217            if !quantifier.can_instantiate() {
218                continue;
219            }
220
221            // Inject extra candidates (e.g. Skolem terms) into the
222            // counterexample generator before searching.
223            self.cex_generator
224                .inject_extra_candidates(&self.extra_candidates);
225
226            // Use the counterexample generator directly to find
227            // assignments that falsify the quantifier body
228            let cex_result = self
229                .cex_generator
230                .generate(quantifier, &completed_model, manager);
231
232            if !cex_result.all_evaluations_ground {
233                all_evaluations_fully_ground = false;
234            }
235
236            self.stats.num_counterexamples += cex_result.counterexamples.len();
237
238            for cex in &cex_result.counterexamples {
239                if !self.budget.consume(quantifier.term, 1) {
240                    break;
241                }
242                // Build the instantiation lemma: body[x1/v1, ..., xn/vn]
243                let ground_body =
244                    self.apply_substitution(quantifier.body, &cex.assignment, manager);
245
246                let inst = cex.to_instantiation(ground_body);
247
248                if !self.is_duplicate(&inst) {
249                    self.record_instantiation(&inst);
250                    callback.on_instantiation(&inst);
251                    all_instantiations.push(inst);
252                }
253            }
254
255            // Also try instantiation engine strategies (pattern-based, enumerative),
256            // but ONLY for universal quantifiers.
257            //
258            // For existential quantifiers: the engine generates body[i/v] lemmas saying
259            // "body must be true here" without verifying that body IS true for that
260            // candidate. Adding False instantiation lemmas for existentials directly
261            // contradicts the asserted constraints and produces spurious UNSAT.
262            // If no witness was found by the counterexample generator, return Unknown.
263            if cex_result.counterexamples.is_empty() && quantifier.is_universal {
264                let engine_insts =
265                    self.instantiation_engine
266                        .instantiate(quantifier, &completed_model, manager);
267
268                for inst in engine_insts {
269                    if !self.budget.consume(quantifier.term, 1) {
270                        break;
271                    }
272                    if !self.is_duplicate(&inst) {
273                        self.record_instantiation(&inst);
274                        callback.on_instantiation(&inst);
275                        all_instantiations.push(inst);
276                    }
277                }
278            } else if cex_result.counterexamples.is_empty() && !quantifier.is_universal {
279                // For existentials with no witness found, mark as not-all-ground so
280                // we return Unknown rather than Satisfied (we couldn't verify).
281                all_evaluations_fully_ground = false;
282            }
283        }
284
285        #[cfg(feature = "std")]
286        {
287            self.stats.cex_search_time_us += cex_start.elapsed().as_micros() as u64;
288        }
289
290        // Step 3: Check result
291        if all_instantiations.is_empty() {
292            if all_evaluations_fully_ground {
293                // Every quantifier body evaluated to concrete True under every
294                // candidate assignment.  The completed model genuinely satisfies
295                // all quantifiers.
296                let result = MBQIResult::Satisfied;
297                callback.on_round_end(self.current_round, &result);
298                self.update_final_stats();
299                return result;
300            }
301            // Some evaluations produced symbolic residuals -- we cannot
302            // conclusively say the model satisfies all quantifiers.
303            // Generate enumerative instantiations to seed the solver with
304            // ground terms (e.g. select(a,0), select(a,1) etc.) so that
305            // subsequent rounds have model values for these terms.
306            for quantifier in &quantifiers {
307                if !quantifier.is_universal || !quantifier.can_instantiate() {
308                    continue;
309                }
310                let engine_insts =
311                    self.instantiation_engine
312                        .instantiate(quantifier, &completed_model, manager);
313                for mut inst in engine_insts {
314                    if !self.budget.consume(quantifier.term, 1) {
315                        break;
316                    }
317                    // Simplify the result body so guards like (0 >= 0 /\ 0 <= 3)
318                    // collapse to True, and Implies(True, consequent) collapses to
319                    // just the consequent.  This produces clean lemmas that the
320                    // SAT solver and theory solvers can reason about directly.
321                    inst.result = self.deep_simplify(inst.result, manager);
322                    // Skip tautologies
323                    if manager
324                        .get(inst.result)
325                        .is_some_and(|t| matches!(t.kind, TermKind::True))
326                    {
327                        continue;
328                    }
329                    if !self.is_duplicate(&inst) {
330                        self.record_instantiation(&inst);
331                        callback.on_instantiation(&inst);
332                        all_instantiations.push(inst);
333                    }
334                }
335            }
336
337            if !all_instantiations.is_empty() {
338                let result = MBQIResult::NewInstantiations(all_instantiations);
339                callback.on_round_end(self.current_round, &result);
340                self.update_final_stats();
341                return result;
342            }
343
344            // Conservatively return Unknown instead of the incorrect Satisfied.
345            let result = MBQIResult::Unknown;
346            callback.on_round_end(self.current_round, &result);
347            self.update_final_stats();
348            return result;
349        }
350
351        // Step 4: Check instantiation limit
352        if self.stats.max_instantiations > 0
353            && self.stats.total_instantiations >= self.stats.max_instantiations
354        {
355            let result = MBQIResult::InstantiationLimit;
356            callback.on_round_end(self.current_round, &result);
357            self.update_final_stats();
358            return result;
359        }
360
361        // Return the new instantiations to the solver.
362        // The solver will add them as lemmas and re-check SAT.
363        // On the next call to MBQI, we'll re-complete the model.
364        let result = MBQIResult::NewInstantiations(all_instantiations);
365        callback.on_round_end(self.current_round, &result);
366        self.update_final_stats();
367        result
368    }
369
370    /// Apply substitution to a term (used for building instantiation lemmas)
371    fn apply_substitution(
372        &self,
373        term: TermId,
374        subst: &FxHashMap<Spur, TermId>,
375        manager: &mut TermManager,
376    ) -> TermId {
377        let mut cache = FxHashMap::default();
378        self.apply_substitution_cached(term, subst, manager, &mut cache)
379    }
380
381    fn apply_substitution_cached(
382        &self,
383        term: TermId,
384        subst: &FxHashMap<Spur, TermId>,
385        manager: &mut TermManager,
386        cache: &mut FxHashMap<TermId, TermId>,
387    ) -> TermId {
388        if let Some(&cached) = cache.get(&term) {
389            return cached;
390        }
391
392        let Some(t) = manager.get(term).cloned() else {
393            return term;
394        };
395
396        let result = match &t.kind {
397            TermKind::Var(name) => subst.get(name).copied().unwrap_or(term),
398            TermKind::Not(arg) => {
399                let new_arg = self.apply_substitution_cached(*arg, subst, manager, cache);
400                manager.mk_not(new_arg)
401            }
402            TermKind::And(args) => {
403                let new_args: Vec<_> = args
404                    .iter()
405                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
406                    .collect();
407                manager.mk_and(new_args)
408            }
409            TermKind::Or(args) => {
410                let new_args: Vec<_> = args
411                    .iter()
412                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
413                    .collect();
414                manager.mk_or(new_args)
415            }
416            TermKind::Implies(lhs, rhs) => {
417                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
418                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
419                manager.mk_implies(new_lhs, new_rhs)
420            }
421            TermKind::Eq(lhs, rhs) => {
422                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
423                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
424                manager.mk_eq(new_lhs, new_rhs)
425            }
426            TermKind::Lt(lhs, rhs) => {
427                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
428                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
429                manager.mk_lt(new_lhs, new_rhs)
430            }
431            TermKind::Le(lhs, rhs) => {
432                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
433                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
434                manager.mk_le(new_lhs, new_rhs)
435            }
436            TermKind::Gt(lhs, rhs) => {
437                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
438                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
439                manager.mk_gt(new_lhs, new_rhs)
440            }
441            TermKind::Ge(lhs, rhs) => {
442                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
443                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
444                manager.mk_ge(new_lhs, new_rhs)
445            }
446            TermKind::Add(args) => {
447                let new_args: SmallVec<[TermId; 4]> = args
448                    .iter()
449                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
450                    .collect();
451                manager.mk_add(new_args)
452            }
453            TermKind::Sub(lhs, rhs) => {
454                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
455                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
456                manager.mk_sub(new_lhs, new_rhs)
457            }
458            TermKind::Mul(args) => {
459                let new_args: SmallVec<[TermId; 4]> = args
460                    .iter()
461                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
462                    .collect();
463                manager.mk_mul(new_args)
464            }
465            TermKind::Div(lhs, rhs) => {
466                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
467                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
468                manager.mk_div(new_lhs, new_rhs)
469            }
470            TermKind::Mod(lhs, rhs) => {
471                let new_lhs = self.apply_substitution_cached(*lhs, subst, manager, cache);
472                let new_rhs = self.apply_substitution_cached(*rhs, subst, manager, cache);
473                manager.mk_mod(new_lhs, new_rhs)
474            }
475            TermKind::Neg(arg) => {
476                let new_arg = self.apply_substitution_cached(*arg, subst, manager, cache);
477                manager.mk_neg(new_arg)
478            }
479            TermKind::Ite(cond, then_br, else_br) => {
480                let new_cond = self.apply_substitution_cached(*cond, subst, manager, cache);
481                let new_then = self.apply_substitution_cached(*then_br, subst, manager, cache);
482                let new_else = self.apply_substitution_cached(*else_br, subst, manager, cache);
483                manager.mk_ite(new_cond, new_then, new_else)
484            }
485            TermKind::Apply { func, args } => {
486                let func_name = manager.resolve_str(*func).to_string();
487                let new_args: SmallVec<[TermId; 4]> = args
488                    .iter()
489                    .map(|&a| self.apply_substitution_cached(a, subst, manager, cache))
490                    .collect();
491                manager.mk_apply(&func_name, new_args, t.sort)
492            }
493            // Array select: recurse into both array and index so that bound
494            // variables appearing in the index (e.g., `select(a, i)`) are
495            // properly substituted.
496            TermKind::Select(array, index) => {
497                let new_array = self.apply_substitution_cached(*array, subst, manager, cache);
498                let new_index = self.apply_substitution_cached(*index, subst, manager, cache);
499                manager.mk_select(new_array, new_index)
500            }
501            // Array store: substitute in all three sub-terms.
502            TermKind::Store(array, index, value) => {
503                let new_array = self.apply_substitution_cached(*array, subst, manager, cache);
504                let new_index = self.apply_substitution_cached(*index, subst, manager, cache);
505                let new_value = self.apply_substitution_cached(*value, subst, manager, cache);
506                manager.mk_store(new_array, new_index, new_value)
507            }
508            // Constants and other terms don't need substitution
509            _ => term,
510        };
511
512        cache.insert(term, result);
513        result
514    }
515
516    /// Clear the deduplication cache so that fresh instantiations (e.g.
517    /// blind or finite domain) with corrected substitution results are
518    /// not filtered out as duplicates of earlier results.
519    pub fn clear_dedup_cache(&mut self) {
520        self.generated_instantiations.clear();
521    }
522
523    /// Check if an instantiation is a duplicate
524    fn is_duplicate(&self, inst: &Instantiation) -> bool {
525        let key = InstantiationKey::from(inst);
526        self.generated_instantiations.contains_key(&key)
527    }
528
529    /// Record an instantiation
530    fn record_instantiation(&mut self, inst: &Instantiation) {
531        let key = InstantiationKey::from(inst);
532        let count = self.generated_instantiations.entry(key).or_insert(0);
533        *count += 1;
534        self.stats.total_instantiations += 1;
535        self.stats.unique_instantiations = self.generated_instantiations.len();
536    }
537
538    /// Check for timeout
539    fn check_timeout(&self) -> bool {
540        #[cfg(feature = "std")]
541        {
542            if let (Some(limit), Some(start)) = (self.time_limit, self.start_time) {
543                return start.elapsed() >= limit;
544            }
545        }
546        false
547    }
548
549    /// Update final statistics
550    fn update_final_stats(&mut self) {
551        #[cfg(feature = "std")]
552        if let Some(start) = self.start_time {
553            self.stats.total_time_us = start.elapsed().as_micros() as u64;
554        }
555    }
556
557    /// Clear all state
558    pub fn clear(&mut self) {
559        self.quantifiers.clear();
560        self.generated_instantiations.clear();
561        self.extra_candidates.clear();
562        self.blind_attempted = false;
563        self.current_round = 0;
564        self.budget = MBQIBudget::new(self.budget.global_budget);
565        self.conflict_scores = ConflictScores::new(self.conflict_scores.decay_factor);
566        #[cfg(feature = "std")]
567        {
568            self.start_time = None;
569        }
570        self.instantiation_engine.clear_caches();
571        self.lazy_instantiator.clear();
572    }
573
574    /// Collect ground terms from trigger patterns
575    pub fn collect_ground_terms(&mut self, _term: TermId, _manager: &TermManager) {
576        // Stub implementation - would collect ground terms for E-matching
577        // This is a placeholder for future E-matching integration
578    }
579
580    /// Check quantifiers with a given model
581    pub fn check_with_model(
582        &mut self,
583        model: &FxHashMap<TermId, TermId>,
584        manager: &mut TermManager,
585    ) -> MBQIResult {
586        // Use a no-op callback for this convenience method
587        #[derive(Debug)]
588        struct NoOpCallback;
589        impl SolverCallback for NoOpCallback {
590            fn on_instantiation(&mut self, _: &Instantiation) {}
591            fn on_round_start(&mut self, _: usize) {}
592            fn on_round_end(&mut self, _: usize, _: &MBQIResult) {}
593            fn on_conflict(&mut self, _: TermId, _: &[TermId]) {}
594            fn should_stop(&self) -> bool {
595                false
596            }
597        }
598        let mut callback = NoOpCallback;
599        self.run(model, manager, &mut callback)
600    }
601
602    /// Get statistics
603    pub fn stats(&self) -> &MBQIStats {
604        &self.stats
605    }
606
607    /// Set maximum rounds
608    pub fn set_max_rounds(&mut self, max: usize) {
609        self.max_rounds = max;
610    }
611
612    /// Set time limit
613    #[cfg(feature = "std")]
614    pub fn set_time_limit(&mut self, limit: Duration) {
615        self.time_limit = Some(limit);
616    }
617
618    /// Add a candidate term for model-based instantiation.
619    ///
620    /// The term is stored per-sort and will be injected into candidate lists
621    /// when the counterexample generator builds domain enumerations.
622    pub fn add_candidate(&mut self, term: TermId, sort: SortId) {
623        self.extra_candidates.entry(sort).or_default().push(term);
624    }
625
626    /// Whether blind instantiation has been attempted
627    pub fn blind_tried(&self) -> bool {
628        self.blind_attempted
629    }
630
631    /// Mark blind instantiation as attempted
632    pub fn mark_blind_tried(&mut self) {
633        self.blind_attempted = true;
634    }
635
636    /// Check if MBQI is enabled
637    pub fn is_enabled(&self) -> bool {
638        // MBQI is always enabled when the struct exists
639        true
640    }
641
642    /// Register a ground constant as an instantiation candidate.
643    ///
644    /// Called from the context layer whenever a `declare-const` is processed.
645    /// The constant is forwarded to `add_candidate` so that trigger-free
646    /// quantifiers can be instantiated with it.
647    pub fn register_declared_const(&mut self, term: TermId, sort: SortId) {
648        self.add_candidate(term, sort);
649    }
650
651    /// Attempt to produce trivial instantiations for trigger-free quantifiers.
652    ///
653    /// Returns the list of resulting [`Instantiation`]s.  Returns an empty vec
654    /// when no quantifiers are registered, no candidates exist, or all
655    /// quantifiers have trigger patterns that are handled by E-matching.
656    ///
657    /// The full ground-term enumeration strategy is implemented in the main
658    /// MBQI engine; this method is an escape valve for the `Unknown` case.
659    pub fn try_trivial_instantiation(&mut self, _manager: &mut TermManager) -> Vec<Instantiation> {
660        Vec::new()
661    }
662
663    /// Generate "blind" instantiations for all universal quantifiers.
664    ///
665    /// Unlike the normal MBQI flow (which checks counterexamples against the
666    /// model), this method instantiates every universal quantifier with every
667    /// Generate instantiations by detecting finite integer domains in
668    /// quantifier guards and enumerating all values.  For a guard like
669    /// `(>= i 0) && (<= i 3)`, this generates instances for i=0,1,2,3.
670    /// Unlike `generate_blind_instantiations` which uses a fixed range,
671    /// this extracts the exact bounds from the formula.
672    pub fn generate_finite_domain_instantiations(
673        &mut self,
674        manager: &mut TermManager,
675    ) -> Vec<Instantiation> {
676        use num_bigint::BigInt;
677        let mut all_insts = Vec::new();
678        let quantifiers: Vec<_> = self.quantifiers.to_vec();
679
680        for quantifier in &quantifiers {
681            if !quantifier.is_universal || !quantifier.can_instantiate() {
682                continue;
683            }
684
685            // Try to extract integer bounds for each variable from the body
686            let bounds = self.extract_variable_bounds(quantifier, manager);
687            if bounds.is_empty() {
688                continue;
689            }
690
691            // Build candidate lists from the extracted bounds
692            let mut candidate_lists: Vec<Vec<TermId>> = Vec::new();
693            for &(var_name, sort) in &quantifier.bound_vars {
694                if let Some(&(lo, hi)) = bounds.get(&var_name) {
695                    if hi - lo <= 20 && sort == manager.sorts.int_sort {
696                        let cands: Vec<TermId> =
697                            (lo..=hi).map(|v| manager.mk_int(BigInt::from(v))).collect();
698                        candidate_lists.push(cands);
699                    } else {
700                        // Too large or non-int: add defaults
701                        let mut cands = Vec::new();
702                        if sort == manager.sorts.int_sort {
703                            for i in -2i64..=5 {
704                                cands.push(manager.mk_int(BigInt::from(i)));
705                            }
706                        }
707                        candidate_lists.push(cands);
708                    }
709                } else {
710                    let mut cands = Vec::new();
711                    if sort == manager.sorts.int_sort {
712                        for i in -2i64..=5 {
713                            cands.push(manager.mk_int(BigInt::from(i)));
714                        }
715                    }
716                    candidate_lists.push(cands);
717                }
718            }
719
720            if candidate_lists.is_empty() || candidate_lists.iter().any(|c| c.is_empty()) {
721                continue;
722            }
723            let combos = self.enumerate_combinations_blind(&candidate_lists, 2000);
724            for combo in combos {
725                let mut subst = FxHashMap::default();
726                for (i, &val) in combo.iter().enumerate() {
727                    if let Some(var_name) = quantifier.var_name(i) {
728                        subst.insert(var_name, val);
729                    }
730                }
731                let ground_body = self.apply_substitution(quantifier.body, &subst, manager);
732                let inst = Instantiation::new(
733                    quantifier.term,
734                    subst,
735                    ground_body,
736                    self.current_round as u32,
737                );
738                if !self.is_duplicate(&inst) {
739                    self.record_instantiation(&inst);
740                    all_insts.push(inst);
741                }
742            }
743        }
744        all_insts
745    }
746
747    /// Extract integer bounds for quantifier variables from the body.
748    /// Looks for patterns like `(=> (and (>= i 0) (<= i 3) ...) body)`
749    /// Extract integer bounds for quantifier variables from the body.
750    /// Looks for patterns like `(=> (and (>= i 0) (<= i 3) ...) body)`
751    fn extract_variable_bounds(
752        &self,
753        quantifier: &QuantifiedFormula,
754        manager: &TermManager,
755    ) -> FxHashMap<Spur, (i64, i64)> {
756        use num_traits::ToPrimitive;
757        let mut bounds: FxHashMap<Spur, (i64, i64)> = FxHashMap::default();
758        let Some(t) = manager.get(quantifier.body) else {
759            return bounds;
760        };
761
762        // Look for Implies(guard, consequent) pattern
763        let guard = match &t.kind {
764            TermKind::Implies(guard, _) => *guard,
765            _ => return bounds,
766        };
767
768        let Some(gt) = manager.get(guard) else {
769            return bounds;
770        };
771
772        let args = match &gt.kind {
773            TermKind::And(args) => args.clone(),
774            _ => return bounds,
775        };
776
777        // Collect per-variable bounds from Ge/Le
778        let mut lowers: FxHashMap<Spur, i64> = FxHashMap::default();
779        let mut uppers: FxHashMap<Spur, i64> = FxHashMap::default();
780
781        for &a in args.iter() {
782            let Some(at) = manager.get(a) else { continue };
783            match &at.kind {
784                TermKind::Ge(lhs, rhs) => {
785                    if let (Some(lt), Some(rt)) = (manager.get(*lhs), manager.get(*rhs)) {
786                        if let (TermKind::Var(name), TermKind::IntConst(n)) = (&lt.kind, &rt.kind) {
787                            if let Some(v) = n.to_i64() {
788                                lowers
789                                    .entry(*name)
790                                    .and_modify(|e| *e = (*e).max(v))
791                                    .or_insert(v);
792                            }
793                        }
794                    }
795                }
796                TermKind::Le(lhs, rhs) => {
797                    if let (Some(lt), Some(rt)) = (manager.get(*lhs), manager.get(*rhs)) {
798                        if let (TermKind::Var(name), TermKind::IntConst(n)) = (&lt.kind, &rt.kind) {
799                            if let Some(v) = n.to_i64() {
800                                uppers
801                                    .entry(*name)
802                                    .and_modify(|e| *e = (*e).min(v))
803                                    .or_insert(v);
804                            }
805                        }
806                    }
807                }
808                _ => {}
809            }
810        }
811
812        for (&name, &lo) in &lowers {
813            if let Some(&hi) = uppers.get(&name) {
814                if hi >= lo {
815                    bounds.insert(name, (lo, hi));
816                }
817            }
818        }
819
820        bounds
821    }
822
823    /// combination of candidate values and returns the ground lemmas.  The
824    /// caller adds them directly to the SAT solver so that theory solvers can
825    /// detect contradictions (e.g. pigeonhole, Skolem contradictions).
826    pub fn generate_blind_instantiations(
827        &mut self,
828        manager: &mut TermManager,
829    ) -> Vec<Instantiation> {
830        use num_bigint::BigInt;
831
832        let mut all_insts = Vec::new();
833        let quantifiers: Vec<_> = self.quantifiers.to_vec();
834
835        for quantifier in &quantifiers {
836            if !quantifier.is_universal || !quantifier.can_instantiate() {
837                continue;
838            }
839
840            // Build candidate lists for each bound variable
841            let mut candidate_lists: Vec<Vec<TermId>> = Vec::new();
842            for &(_var_name, sort) in &quantifier.bound_vars {
843                let mut cands = Vec::new();
844
845                // Include extra candidates (Skolem terms, etc.)
846                if let Some(extras) = self.extra_candidates.get(&sort) {
847                    for &t in extras {
848                        if !cands.contains(&t) {
849                            cands.push(t);
850                        }
851                    }
852                }
853
854                // Add default integer candidates
855                if sort == manager.sorts.int_sort {
856                    for i in -2i64..=5 {
857                        let val = manager.mk_int(BigInt::from(i));
858                        if !cands.contains(&val) {
859                            cands.push(val);
860                        }
861                    }
862                } else if sort == manager.sorts.bool_sort {
863                    let t_val = manager.mk_true();
864                    let f_val = manager.mk_false();
865                    if !cands.contains(&t_val) {
866                        cands.push(t_val);
867                    }
868                    if !cands.contains(&f_val) {
869                        cands.push(f_val);
870                    }
871                }
872
873                // Limit per variable
874                cands.truncate(16);
875                candidate_lists.push(cands);
876            }
877
878            if candidate_lists.is_empty() {
879                continue;
880            }
881
882            // Enumerate combinations
883            let combos = self.enumerate_combinations_blind(&candidate_lists, 500);
884            for combo in combos {
885                // Build substitution
886                let mut subst = FxHashMap::default();
887                for (i, &val) in combo.iter().enumerate() {
888                    if let Some(var_name) = quantifier.var_name(i) {
889                        subst.insert(var_name, val);
890                    }
891                }
892
893                let ground_body = self.apply_substitution(quantifier.body, &subst, manager);
894                // Simplify arithmetic comparisons of constants (e.g. 0 >= 0 → True)
895                // and boolean simplifications so the SAT solver sees clean lemmas.
896                let simplified = self.deep_simplify(ground_body, manager);
897
898                // Skip tautologies (body simplifies to True — no information)
899                if manager
900                    .get(simplified)
901                    .is_some_and(|t| matches!(t.kind, TermKind::True))
902                {
903                    continue;
904                }
905
906                // Skip lemmas that still have an Implies at the top level
907                // after simplification. These have non-ground guards (free
908                // variables from declared constants) and can cause spurious
909                // UNSAT when the theory solver doesn't handle them correctly.
910                // Lemmas with fully resolved guards collapse to just the
911                // consequent (no Implies wrapper) and are safe to add.
912                if manager
913                    .get(simplified)
914                    .is_some_and(|t| matches!(t.kind, TermKind::Implies(_, _)))
915                {
916                    continue;
917                }
918
919                let inst = Instantiation::new(quantifier.term, subst, simplified, 0);
920
921                if !self.is_duplicate(&inst) {
922                    self.record_instantiation(&inst);
923                    all_insts.push(inst);
924                }
925            }
926        }
927
928        all_insts
929    }
930
931    /// Deep-simplify a ground term: reduce constant comparisons, propagate
932    /// boolean values through And/Or/Implies/Not, etc.
933    pub fn deep_simplify(&self, term: TermId, manager: &mut TermManager) -> TermId {
934        let mut cache = FxHashMap::default();
935        self.deep_simplify_cached(term, manager, &mut cache)
936    }
937
938    fn deep_simplify_cached(
939        &self,
940        term: TermId,
941        manager: &mut TermManager,
942        cache: &mut FxHashMap<TermId, TermId>,
943    ) -> TermId {
944        if let Some(&c) = cache.get(&term) {
945            return c;
946        }
947        let Some(t) = manager.get(term).cloned() else {
948            return term;
949        };
950        let result = match &t.kind {
951            TermKind::True
952            | TermKind::False
953            | TermKind::IntConst(_)
954            | TermKind::RealConst(_)
955            | TermKind::BitVecConst { .. }
956            | TermKind::StringLit(_)
957            | TermKind::Var(_) => term,
958
959            TermKind::Not(a) => {
960                let sa = self.deep_simplify_cached(*a, manager, cache);
961                match manager.get(sa).map(|t2| &t2.kind) {
962                    Some(TermKind::True) => manager.mk_false(),
963                    Some(TermKind::False) => manager.mk_true(),
964                    _ => manager.mk_not(sa),
965                }
966            }
967            TermKind::And(args) => {
968                let mut simplified = Vec::new();
969                for &a in args.iter() {
970                    let sa = self.deep_simplify_cached(a, manager, cache);
971                    match manager.get(sa).map(|t2| &t2.kind) {
972                        Some(TermKind::False) => {
973                            return {
974                                let r = manager.mk_false();
975                                cache.insert(term, r);
976                                r
977                            };
978                        }
979                        Some(TermKind::True) => { /* skip */ }
980                        _ => simplified.push(sa),
981                    }
982                }
983                if simplified.is_empty() {
984                    manager.mk_true()
985                } else if simplified.len() == 1 {
986                    simplified[0]
987                } else {
988                    manager.mk_and(simplified)
989                }
990            }
991            TermKind::Or(args) => {
992                let mut simplified = Vec::new();
993                for &a in args.iter() {
994                    let sa = self.deep_simplify_cached(a, manager, cache);
995                    match manager.get(sa).map(|t2| &t2.kind) {
996                        Some(TermKind::True) => {
997                            return {
998                                let r = manager.mk_true();
999                                cache.insert(term, r);
1000                                r
1001                            };
1002                        }
1003                        Some(TermKind::False) => { /* skip */ }
1004                        _ => simplified.push(sa),
1005                    }
1006                }
1007                if simplified.is_empty() {
1008                    manager.mk_false()
1009                } else if simplified.len() == 1 {
1010                    simplified[0]
1011                } else {
1012                    manager.mk_or(simplified)
1013                }
1014            }
1015            TermKind::Implies(lhs, rhs) => {
1016                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1017                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1018                match manager.get(sl).map(|t2| &t2.kind) {
1019                    Some(TermKind::False) => manager.mk_true(),
1020                    Some(TermKind::True) => sr,
1021                    _ => match manager.get(sr).map(|t2| &t2.kind) {
1022                        Some(TermKind::True) => manager.mk_true(),
1023                        _ => manager.mk_implies(sl, sr),
1024                    },
1025                }
1026            }
1027            TermKind::Eq(lhs, rhs) => {
1028                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1029                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1030                self.simplify_eq(sl, sr, manager)
1031            }
1032            TermKind::Le(lhs, rhs) => {
1033                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1034                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1035                self.simplify_le(sl, sr, manager)
1036            }
1037            TermKind::Lt(lhs, rhs) => {
1038                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1039                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1040                self.simplify_lt(sl, sr, manager)
1041            }
1042            TermKind::Ge(lhs, rhs) => {
1043                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1044                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1045                self.simplify_le(sr, sl, manager) // a >= b ≡ b <= a
1046            }
1047            TermKind::Gt(lhs, rhs) => {
1048                let sl = self.deep_simplify_cached(*lhs, manager, cache);
1049                let sr = self.deep_simplify_cached(*rhs, manager, cache);
1050                self.simplify_lt(sr, sl, manager) // a > b ≡ b < a
1051            }
1052            TermKind::Select(arr, idx) => {
1053                let sa = self.deep_simplify_cached(*arr, manager, cache);
1054                let si = self.deep_simplify_cached(*idx, manager, cache);
1055                manager.mk_select(sa, si)
1056            }
1057            TermKind::Apply { func, args } => {
1058                let fname = manager.resolve_str(*func).to_string();
1059                let new_args: SmallVec<[TermId; 4]> = args
1060                    .iter()
1061                    .map(|&a| self.deep_simplify_cached(a, manager, cache))
1062                    .collect();
1063                manager.mk_apply(&fname, new_args, t.sort)
1064            }
1065            _ => term,
1066        };
1067        cache.insert(term, result);
1068        result
1069    }
1070
1071    /// Simplify `lhs = rhs` when both are integer constants
1072    fn simplify_eq(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1073        if lhs == rhs {
1074            return manager.mk_true();
1075        }
1076        let l = manager.get(lhs).cloned();
1077        let r = manager.get(rhs).cloned();
1078        if let (Some(lt), Some(rt)) = (l, r) {
1079            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&lt.kind, &rt.kind) {
1080                return if a == b {
1081                    manager.mk_true()
1082                } else {
1083                    manager.mk_false()
1084                };
1085            }
1086        }
1087        manager.mk_eq(lhs, rhs)
1088    }
1089
1090    /// Simplify `lhs <= rhs` when both are integer constants
1091    fn simplify_le(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1092        let l = manager.get(lhs).cloned();
1093        let r = manager.get(rhs).cloned();
1094        if let (Some(lt), Some(rt)) = (l, r) {
1095            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&lt.kind, &rt.kind) {
1096                return if a <= b {
1097                    manager.mk_true()
1098                } else {
1099                    manager.mk_false()
1100                };
1101            }
1102        }
1103        manager.mk_le(lhs, rhs)
1104    }
1105
1106    /// Simplify `lhs < rhs` when both are integer constants
1107    fn simplify_lt(&self, lhs: TermId, rhs: TermId, manager: &mut TermManager) -> TermId {
1108        let l = manager.get(lhs).cloned();
1109        let r = manager.get(rhs).cloned();
1110        if let (Some(lt), Some(rt)) = (l, r) {
1111            if let (TermKind::IntConst(a), TermKind::IntConst(b)) = (&lt.kind, &rt.kind) {
1112                return if a < b {
1113                    manager.mk_true()
1114                } else {
1115                    manager.mk_false()
1116                };
1117            }
1118        }
1119        manager.mk_lt(lhs, rhs)
1120    }
1121
1122    /// Enumerate all combinations up to a maximum total count.
1123    fn enumerate_combinations_blind(
1124        &self,
1125        candidates: &[Vec<TermId>],
1126        max_total: usize,
1127    ) -> Vec<Vec<TermId>> {
1128        if candidates.is_empty() {
1129            return vec![vec![]];
1130        }
1131
1132        let mut results = Vec::new();
1133        let mut indices = vec![0usize; candidates.len()];
1134
1135        loop {
1136            let combo: Vec<TermId> = indices
1137                .iter()
1138                .enumerate()
1139                .filter_map(|(i, &idx)| candidates.get(i).and_then(|c| c.get(idx).copied()))
1140                .collect();
1141
1142            if combo.len() == candidates.len() {
1143                results.push(combo);
1144            }
1145
1146            if results.len() >= max_total {
1147                break;
1148            }
1149
1150            // Increment indices (odometer style)
1151            let mut carry = true;
1152            for (i, idx) in indices.iter_mut().enumerate() {
1153                if carry {
1154                    *idx += 1;
1155                    let limit = candidates.get(i).map_or(1, |c| c.len());
1156                    if *idx >= limit {
1157                        *idx = 0;
1158                    } else {
1159                        carry = false;
1160                    }
1161                }
1162            }
1163
1164            if carry {
1165                break;
1166            }
1167        }
1168
1169        results
1170    }
1171}
1172
1173impl Default for MBQIIntegration {
1174    fn default() -> Self {
1175        Self::new()
1176    }
1177}
1178
1179/// Key for instantiation deduplication
1180#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1181struct InstantiationKey {
1182    quantifier: TermId,
1183    binding: Vec<(oxiz_core::interner::Spur, TermId)>,
1184}
1185
1186impl From<&Instantiation> for InstantiationKey {
1187    fn from(inst: &Instantiation) -> Self {
1188        let mut binding: Vec<_> = inst.substitution.iter().map(|(&k, &v)| (k, v)).collect();
1189        binding.sort_by_key(|(k, _)| *k);
1190        Self {
1191            quantifier: inst.quantifier,
1192            binding,
1193        }
1194    }
1195}
1196
1197/// Default callback implementation (no-op)
1198#[derive(Debug)]
1199pub struct DefaultCallback {
1200    stop_requested: bool,
1201}
1202
1203impl DefaultCallback {
1204    pub fn new() -> Self {
1205        Self {
1206            stop_requested: false,
1207        }
1208    }
1209
1210    pub fn request_stop(&mut self) {
1211        self.stop_requested = true;
1212    }
1213}
1214
1215impl Default for DefaultCallback {
1216    fn default() -> Self {
1217        Self::new()
1218    }
1219}
1220
1221impl SolverCallback for DefaultCallback {
1222    fn on_instantiation(&mut self, _inst: &Instantiation) {}
1223    fn on_conflict(&mut self, _quantifier: TermId, _reason: &[TermId]) {}
1224    fn on_round_start(&mut self, _round: usize) {}
1225    fn on_round_end(&mut self, _round: usize, _result: &MBQIResult) {}
1226    fn should_stop(&self) -> bool {
1227        self.stop_requested
1228    }
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233    use super::*;
1234
1235    #[test]
1236    fn test_mbqi_integration_creation() {
1237        let integration = MBQIIntegration::new();
1238        assert_eq!(integration.quantifiers.len(), 0);
1239        assert_eq!(integration.current_round, 0);
1240    }
1241
1242    #[test]
1243    fn test_default_callback() {
1244        let mut callback = DefaultCallback::new();
1245        assert!(!callback.should_stop());
1246        callback.request_stop();
1247        assert!(callback.should_stop());
1248    }
1249
1250    #[test]
1251    fn test_integration_clear() {
1252        let mut integration = MBQIIntegration::new();
1253        integration.current_round = 5;
1254        integration.clear();
1255        assert_eq!(integration.current_round, 0);
1256        assert_eq!(integration.quantifiers.len(), 0);
1257    }
1258
1259    #[test]
1260    fn test_set_max_rounds() {
1261        let mut integration = MBQIIntegration::new();
1262        integration.set_max_rounds(50);
1263        assert_eq!(integration.max_rounds, 50);
1264    }
1265
1266    #[test]
1267    fn test_set_time_limit() {
1268        let mut integration = MBQIIntegration::new();
1269        let limit = Duration::from_secs(30);
1270        integration.set_time_limit(limit);
1271        assert_eq!(integration.time_limit, Some(limit));
1272    }
1273}