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