Skip to main content

oxiz_solver/mbqi/
mod.rs

1//! Model-Based Quantifier Instantiation (MBQI)
2//!
3//! This module implements a comprehensive MBQI system for handling quantified formulas
4//! in SMT solving. MBQI is a powerful technique that uses candidate models to guide
5//! the instantiation of quantified formulas.
6//!
7//! # Algorithm Overview
8//!
9//! Model-Based Quantifier Instantiation works by:
10//!
11//! 1. **Model Extraction**: Get a partial model M from the SAT solver
12//! 2. **Model Completion**: Complete the partial model to handle all function interpretations
13//! 3. **Quantifier Specialization**: For each universal quantifier ∀x.φ(x), specialize φ with M
14//! 4. **Counterexample Search**: Look for assignments that falsify φ under M
15//! 5. **Instantiation**: If counterexample x' found where ¬φ(x') holds, add φ(x') as lemma
16//! 6. **Refinement**: Repeat until no counterexamples exist or resource limits reached
17//!
18//! # Key Features
19//!
20//! - **Finite Model Finding**: Restricts infinite domains to finite universes
21//! - **Model Completion**: Completes partial models using macro solving
22//! - **Projection Functions**: Maps values to representative terms
23//! - **Lazy Instantiation**: Generates instances on-demand
24//! - **Conflict-Driven Learning**: Learns from failed instantiation attempts
25//! - **Symmetry Breaking**: Reduces redundant search in symmetric problems
26//!
27//! # References
28//!
29//! - Ge, Y., & de Moura, L. (2009). "Complete instantiation for quantified formulas
30//!   in satisfiability modulo theories." CAV 2009.
31//! - Reynolds, A., et al. (2013). "Quantifier instantiation techniques for finite
32//!   model finding in SMT." CADE 2013.
33//!
34//! # Module Organization
35//!
36//! - `model_completion`: Algorithms for completing partial models
37//! - `counterexample`: Counter-example generation and refinement
38//! - `instantiation`: Model-based instantiation logic
39//! - `finite_model`: Finite model finder with bounded enumeration
40//! - `lazy_instantiation`: Lazy quantifier instantiation strategies
41//! - `integration`: Integration with the main solver
42//! - `heuristics`: MBQI-specific heuristics and strategies
43//! - `macros`: Macro definitions and utilities
44
45#![allow(missing_docs)]
46#![allow(dead_code)]
47
48#[allow(unused_imports)]
49use crate::prelude::*;
50use core::fmt;
51use oxiz_core::ast::TermId;
52use oxiz_core::interner::Spur;
53use oxiz_core::sort::SortId;
54use smallvec::SmallVec;
55
56pub mod counterexample;
57pub mod finite_model;
58pub mod heuristics;
59pub mod instantiation;
60pub mod integration;
61pub mod lazy_instantiation;
62pub mod macros;
63pub mod model_completion;
64pub mod patterns;
65
66// Re-export key types
67pub use counterexample::{
68    CexGenerationResult, CounterExample, CounterExampleGenerator, RefinementStrategy,
69};
70pub use finite_model::{FiniteModel, FiniteModelFinder, SymmetryBreaker, UniverseSize};
71pub use heuristics::{InstantiationHeuristic, MBQIHeuristics, SelectionStrategy, TriggerSelection};
72pub use instantiation::{
73    InstantiationContext, InstantiationEngine, InstantiationPattern, QuantifierInstantiator,
74};
75pub use integration::{MBQIIntegration, SolverCallback};
76pub use lazy_instantiation::{LazyInstantiator, LazyStrategy, MatchingContext};
77pub use model_completion::{
78    MacroSolver, ModelCompleter, ModelFixer, ProjectionFunction, UninterpretedSortHandler,
79};
80
81/// A quantified formula tracked by MBQI
82#[derive(Debug, Clone)]
83pub struct QuantifiedFormula {
84    /// The original quantified term
85    pub term: TermId,
86    /// Bound variables (name, sort)
87    pub bound_vars: SmallVec<[(Spur, SortId); 4]>,
88    /// The body of the quantifier
89    pub body: TermId,
90    /// Whether this is universal (true) or existential (false)
91    pub is_universal: bool,
92    /// Quantifier nesting depth
93    pub nesting_depth: u32,
94    /// Number of times this quantifier has been instantiated
95    pub instantiation_count: usize,
96    /// Maximum allowed instantiations for this quantifier
97    pub max_instantiations: usize,
98    /// Generation number (for tracking term age)
99    pub generation: u32,
100    /// Weight for prioritization
101    pub weight: f64,
102    /// Patterns for E-matching (if available)
103    pub patterns: Vec<Vec<TermId>>,
104    /// Whether this quantifier has been flattened
105    pub is_flattened: bool,
106    /// Cached specialized body (None if not yet specialized)
107    pub specialized_body: Option<TermId>,
108}
109
110impl QuantifiedFormula {
111    /// Create a new tracked quantified formula
112    pub fn new(
113        term: TermId,
114        bound_vars: SmallVec<[(Spur, SortId); 4]>,
115        body: TermId,
116        is_universal: bool,
117    ) -> Self {
118        Self {
119            term,
120            bound_vars,
121            body,
122            is_universal,
123            nesting_depth: 0,
124            instantiation_count: 0,
125            max_instantiations: 1000,
126            generation: 0,
127            weight: 1.0,
128            patterns: Vec::new(),
129            is_flattened: false,
130            specialized_body: None,
131        }
132    }
133
134    /// Create with custom parameters
135    pub fn with_params(
136        term: TermId,
137        bound_vars: SmallVec<[(Spur, SortId); 4]>,
138        body: TermId,
139        is_universal: bool,
140        max_instantiations: usize,
141        weight: f64,
142    ) -> Self {
143        let mut qf = Self::new(term, bound_vars, body, is_universal);
144        qf.max_instantiations = max_instantiations;
145        qf.weight = weight;
146        qf
147    }
148
149    /// Check if we can instantiate more
150    pub fn can_instantiate(&self) -> bool {
151        self.instantiation_count < self.max_instantiations
152    }
153
154    /// Get the number of bound variables
155    pub fn num_vars(&self) -> usize {
156        self.bound_vars.len()
157    }
158
159    /// Get variable name by index
160    pub fn var_name(&self, idx: usize) -> Option<Spur> {
161        self.bound_vars.get(idx).map(|(name, _)| *name)
162    }
163
164    /// Get variable sort by index
165    pub fn var_sort(&self, idx: usize) -> Option<SortId> {
166        self.bound_vars.get(idx).map(|(_, sort)| *sort)
167    }
168
169    /// Record an instantiation
170    pub fn record_instantiation(&mut self) {
171        self.instantiation_count += 1;
172    }
173
174    /// Calculate priority score (higher = more important)
175    pub fn priority_score(&self) -> f64 {
176        // Combine multiple factors:
177        // - Weight (user-specified importance)
178        // - Inverse of instantiation count (prefer less-instantiated)
179        // - Inverse of nesting depth (prefer simpler quantifiers)
180        let inst_factor = 1.0 / (1.0 + self.instantiation_count as f64);
181        let depth_factor = 1.0 / (1.0 + self.nesting_depth as f64);
182        self.weight * inst_factor * depth_factor
183    }
184}
185
186/// An instantiation of a quantified formula
187#[derive(Debug, Clone)]
188pub struct Instantiation {
189    /// The quantifier that was instantiated
190    pub quantifier: TermId,
191    /// The substitution used (variable name -> term)
192    pub substitution: FxHashMap<Spur, TermId>,
193    /// The resulting ground term (body with substitution applied)
194    pub result: TermId,
195    /// Generation at which this instantiation was created
196    pub generation: u32,
197    /// Reason for instantiation (for proof generation)
198    pub reason: InstantiationReason,
199    /// Cost/weight of this instantiation
200    pub cost: f64,
201}
202
203impl Instantiation {
204    /// Create a new instantiation
205    pub fn new(
206        quantifier: TermId,
207        substitution: FxHashMap<Spur, TermId>,
208        result: TermId,
209        generation: u32,
210    ) -> Self {
211        Self {
212            quantifier,
213            substitution,
214            result,
215            generation,
216            reason: InstantiationReason::ModelBased,
217            cost: 1.0,
218        }
219    }
220
221    /// Create with reason
222    pub fn with_reason(
223        quantifier: TermId,
224        substitution: FxHashMap<Spur, TermId>,
225        result: TermId,
226        generation: u32,
227        reason: InstantiationReason,
228    ) -> Self {
229        Self {
230            quantifier,
231            substitution,
232            result,
233            generation,
234            reason,
235            cost: 1.0,
236        }
237    }
238
239    /// Get the binding as a sorted vector for hashing
240    pub fn binding_key(&self) -> Vec<(Spur, TermId)> {
241        let mut key: Vec<_> = self.substitution.iter().map(|(&k, &v)| (k, v)).collect();
242        key.sort_by_key(|(name, _)| *name);
243        key
244    }
245}
246
247/// Reason for creating an instantiation
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum InstantiationReason {
250    /// Model-based instantiation
251    ModelBased,
252    /// E-matching pattern instantiation
253    EMatching,
254    /// Conflict-driven instantiation
255    Conflict,
256    /// Enumerative instantiation
257    Enumerative,
258    /// User-provided instantiation
259    User,
260    /// Theory-specific instantiation
261    Theory,
262}
263
264impl fmt::Display for InstantiationReason {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match self {
267            Self::ModelBased => write!(f, "model-based"),
268            Self::EMatching => write!(f, "e-matching"),
269            Self::Conflict => write!(f, "conflict"),
270            Self::Enumerative => write!(f, "enumerative"),
271            Self::User => write!(f, "user"),
272            Self::Theory => write!(f, "theory"),
273        }
274    }
275}
276
277/// Result of MBQI check
278#[derive(Debug, Clone)]
279pub enum MBQIResult {
280    /// No quantifiers to process
281    NoQuantifiers,
282    /// All quantifiers satisfied under the model
283    Satisfied,
284    /// Found new instantiations to add
285    NewInstantiations(Vec<Instantiation>),
286    /// Found a conflict (quantifier cannot be satisfied)
287    Conflict {
288        quantifier: TermId,
289        reason: Vec<TermId>,
290    },
291    /// Reached instantiation limit
292    InstantiationLimit,
293    /// Unknown (resource limits or incompleteness)
294    Unknown,
295}
296
297impl MBQIResult {
298    /// Check if the result is satisfiable
299    pub fn is_sat(&self) -> bool {
300        matches!(self, Self::Satisfied)
301    }
302
303    /// Check if the result is unsatisfiable
304    pub fn is_unsat(&self) -> bool {
305        matches!(self, Self::Conflict { .. })
306    }
307
308    /// Check if new instantiations were found
309    pub fn has_instantiations(&self) -> bool {
310        matches!(self, Self::NewInstantiations(_))
311    }
312
313    /// Get the number of new instantiations
314    pub fn num_instantiations(&self) -> usize {
315        match self {
316            Self::NewInstantiations(insts) => insts.len(),
317            _ => 0,
318        }
319    }
320}
321
322/// Statistics about MBQI
323#[derive(Debug, Clone, Default)]
324pub struct MBQIStats {
325    /// Number of tracked quantifiers
326    pub num_quantifiers: usize,
327    /// Total instantiations generated
328    pub total_instantiations: usize,
329    /// Maximum allowed instantiations
330    pub max_instantiations: usize,
331    /// Unique instantiations (after deduplication)
332    pub unique_instantiations: usize,
333    /// Number of MBQI check calls
334    pub num_checks: usize,
335    /// Number of successful model completions
336    pub num_completions: usize,
337    /// Number of counterexamples found
338    pub num_counterexamples: usize,
339    /// Number of conflicts generated
340    pub num_conflicts: usize,
341    /// Total time spent in MBQI (microseconds)
342    pub total_time_us: u64,
343    /// Time spent in model completion (microseconds)
344    pub completion_time_us: u64,
345    /// Time spent in counterexample search (microseconds)
346    pub cex_search_time_us: u64,
347}
348
349impl MBQIStats {
350    /// Create new empty statistics
351    pub fn new() -> Self {
352        Self::default()
353    }
354
355    /// Reset all statistics
356    pub fn reset(&mut self) {
357        *self = Self::default();
358    }
359
360    /// Get average instantiations per check
361    pub fn avg_instantiations_per_check(&self) -> f64 {
362        if self.num_checks == 0 {
363            0.0
364        } else {
365            self.total_instantiations as f64 / self.num_checks as f64
366        }
367    }
368
369    /// Get average time per check (microseconds)
370    pub fn avg_time_per_check_us(&self) -> f64 {
371        if self.num_checks == 0 {
372            0.0
373        } else {
374            self.total_time_us as f64 / self.num_checks as f64
375        }
376    }
377}
378
379impl fmt::Display for MBQIStats {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        writeln!(f, "MBQI Statistics:")?;
382        writeln!(f, "  Quantifiers: {}", self.num_quantifiers)?;
383        writeln!(f, "  Total checks: {}", self.num_checks)?;
384        writeln!(f, "  Total instantiations: {}", self.total_instantiations)?;
385        writeln!(f, "  Unique instantiations: {}", self.unique_instantiations)?;
386        writeln!(
387            f,
388            "  Avg inst/check: {:.2}",
389            self.avg_instantiations_per_check()
390        )?;
391        writeln!(f, "  Counterexamples: {}", self.num_counterexamples)?;
392        writeln!(f, "  Conflicts: {}", self.num_conflicts)?;
393        writeln!(
394            f,
395            "  Total time: {:.2}ms",
396            self.total_time_us as f64 / 1000.0
397        )?;
398        writeln!(
399            f,
400            "  Completion time: {:.2}ms",
401            self.completion_time_us as f64 / 1000.0
402        )?;
403        writeln!(
404            f,
405            "  CEX search time: {:.2}ms",
406            self.cex_search_time_us as f64 / 1000.0
407        )
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use oxiz_core::interner::Key;
415
416    #[test]
417    fn test_quantified_formula_creation() {
418        let qf = QuantifiedFormula::new(TermId::new(1), SmallVec::new(), TermId::new(2), true);
419        assert!(qf.is_universal);
420        assert_eq!(qf.instantiation_count, 0);
421        assert!(qf.can_instantiate());
422    }
423
424    #[test]
425    fn test_quantified_formula_priority() {
426        let mut qf = QuantifiedFormula::with_params(
427            TermId::new(1),
428            SmallVec::new(),
429            TermId::new(2),
430            true,
431            100,
432            2.0,
433        );
434        let initial_priority = qf.priority_score();
435        qf.record_instantiation();
436        let after_priority = qf.priority_score();
437        // Priority should decrease after instantiation
438        assert!(after_priority < initial_priority);
439    }
440
441    #[test]
442    fn test_instantiation_binding_key() {
443        let mut subst = FxHashMap::default();
444        subst.insert(
445            Spur::try_from_usize(1).expect("valid spur"),
446            TermId::new(10),
447        );
448        subst.insert(
449            Spur::try_from_usize(2).expect("valid spur"),
450            TermId::new(20),
451        );
452
453        let inst = Instantiation::new(TermId::new(1), subst, TermId::new(3), 0);
454        let key = inst.binding_key();
455        assert_eq!(key.len(), 2);
456        // Should be sorted
457        assert!(key[0].0 <= key[1].0);
458    }
459
460    #[test]
461    fn test_mbqi_result_predicates() {
462        let sat = MBQIResult::Satisfied;
463        assert!(sat.is_sat());
464        assert!(!sat.is_unsat());
465        assert!(!sat.has_instantiations());
466
467        let conflict = MBQIResult::Conflict {
468            quantifier: TermId::new(1),
469            reason: vec![],
470        };
471        assert!(!conflict.is_sat());
472        assert!(conflict.is_unsat());
473
474        let inst = MBQIResult::NewInstantiations(vec![]);
475        assert!(inst.has_instantiations());
476        assert_eq!(inst.num_instantiations(), 0);
477    }
478
479    #[test]
480    fn test_stats_initialization() {
481        let stats = MBQIStats::new();
482        assert_eq!(stats.num_quantifiers, 0);
483        assert_eq!(stats.total_instantiations, 0);
484        assert_eq!(stats.avg_instantiations_per_check(), 0.0);
485    }
486
487    #[test]
488    fn test_stats_averages() {
489        let mut stats = MBQIStats::new();
490        stats.num_checks = 10;
491        stats.total_instantiations = 50;
492        stats.total_time_us = 1000;
493
494        assert_eq!(stats.avg_instantiations_per_check(), 5.0);
495        assert_eq!(stats.avg_time_per_check_us(), 100.0);
496    }
497
498    #[test]
499    fn test_instantiation_reason_display() {
500        assert_eq!(
501            format!("{}", InstantiationReason::ModelBased),
502            "model-based"
503        );
504        assert_eq!(format!("{}", InstantiationReason::EMatching), "e-matching");
505        assert_eq!(format!("{}", InstantiationReason::Conflict), "conflict");
506    }
507}