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