Skip to main content

oxiz_solver/conflict/
theory_explainer.rs

1//! Theory Conflict Explanation.
2//!
3//! This module generates detailed explanations for theory conflicts, enabling
4//! CDCL(T) to learn effective conflict clauses from theory solver failures.
5//!
6//! ## Explanation Types
7//!
8//! 1. **Equality Explanations**: Why two terms must/cannot be equal
9//! 2. **Bound Explanations**: Why a variable must satisfy certain bounds
10//! 3. **Disequality Explanations**: Why distinct terms cannot be equal
11//! 4. **Arithmetic Explanations**: Linear combinations proving inconsistency
12//!
13//! ## Explanation Quality
14//!
15//! Good explanations are:
16//! - **Minimal**: Use fewest literals possible
17//! - **Relevant**: Only include necessary constraints
18//! - **General**: Learn broadly applicable conflicts
19//! - **Precise**: Accurately capture the inconsistency
20//!
21//! ## References
22//!
23//! - Nieuwenhuis et al.: "Solving SAT and SAT Modulo Theories" (JACM 2006)
24//! - de Moura & Bjørner: "Z3: An Efficient SMT Solver" (TACAS 2008)
25//! - Z3's `smt/theory_explanation.cpp`
26
27use oxiz_sat::Lit;
28use rustc_hash::FxHashSet;
29
30/// Type of theory explanation.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ExplanationType {
33    /// Equality conflict (a = b and a ≠ b).
34    Equality,
35    /// Bound conflict (x > 5 and x < 3).
36    Bounds,
37    /// Disequality conflict (distinct(a,b,c) but a = b).
38    Disequality,
39    /// Arithmetic conflict (linear combination proves UNSAT).
40    Arithmetic,
41    /// Array conflict (read-over-write axiom violation).
42    Array,
43    /// BitVector conflict (overflow, underflow).
44    BitVector,
45}
46
47/// An explanation for a theory conflict.
48#[derive(Debug, Clone)]
49pub struct TheoryExplanation {
50    /// Type of explanation.
51    pub explanation_type: ExplanationType,
52    /// Literals involved in conflict (as conflict clause).
53    pub literals: Vec<Lit>,
54    /// Human-readable explanation (for debugging).
55    pub description: String,
56    /// Proof trace (optional, for proof generation).
57    pub proof_trace: Option<Vec<ProofStep>>,
58}
59
60/// A step in a theory conflict proof.
61#[derive(Debug, Clone)]
62pub struct ProofStep {
63    /// Description of this proof step.
64    pub description: String,
65    /// Literals used in this step.
66    pub premises: Vec<Lit>,
67    /// Conclusion of this step.
68    pub conclusion: String,
69}
70
71/// Configuration for theory explanation generation.
72#[derive(Debug, Clone)]
73pub struct ExplainerConfig {
74    /// Enable proof trace generation.
75    pub generate_proofs: bool,
76    /// Minimize explanations.
77    pub minimize: bool,
78    /// Include human-readable descriptions.
79    pub include_descriptions: bool,
80    /// Maximum explanation size (literals).
81    pub max_size: usize,
82}
83
84impl Default for ExplainerConfig {
85    fn default() -> Self {
86        Self {
87            generate_proofs: false,
88            minimize: true,
89            include_descriptions: false,
90            max_size: 100,
91        }
92    }
93}
94
95/// Statistics for explanation generation.
96#[derive(Debug, Clone, Default)]
97pub struct ExplainerStats {
98    /// Explanations generated.
99    pub explanations_generated: u64,
100    /// Total literals in explanations.
101    pub total_literals: u64,
102    /// Average explanation size.
103    pub avg_size: f64,
104    /// Explanations minimized.
105    pub minimized: u64,
106    /// Time (microseconds).
107    pub time_us: u64,
108}
109
110/// Theory conflict explainer.
111pub struct TheoryExplainer {
112    config: ExplainerConfig,
113    stats: ExplainerStats,
114}
115
116impl TheoryExplainer {
117    /// Create new theory explainer.
118    pub fn new() -> Self {
119        Self::with_config(ExplainerConfig::default())
120    }
121
122    /// Create with configuration.
123    pub fn with_config(config: ExplainerConfig) -> Self {
124        Self {
125            config,
126            stats: ExplainerStats::default(),
127        }
128    }
129
130    /// Get statistics.
131    pub fn stats(&self) -> &ExplainerStats {
132        &self.stats
133    }
134
135    /// Reset statistics.
136    pub fn reset_stats(&mut self) {
137        self.stats = ExplainerStats::default();
138    }
139
140    /// Generate explanation for an equality conflict.
141    ///
142    /// Explains why two terms that must be equal cannot be equal,
143    /// or vice versa.
144    pub fn explain_equality_conflict(
145        &mut self,
146        lits: Vec<Lit>,
147        description: Option<String>,
148    ) -> TheoryExplanation {
149        let start = std::time::Instant::now();
150
151        let minimized_lits = if self.config.minimize {
152            self.minimize_explanation(&lits)
153        } else {
154            lits.clone()
155        };
156
157        let proof_trace = if self.config.generate_proofs {
158            Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Equality))
159        } else {
160            None
161        };
162
163        let desc = if self.config.include_descriptions {
164            description.unwrap_or_else(|| {
165                format!("Equality conflict with {} literals", minimized_lits.len())
166            })
167        } else {
168            String::new()
169        };
170
171        self.stats.explanations_generated += 1;
172        self.stats.total_literals += minimized_lits.len() as u64;
173        self.update_avg_size();
174        self.stats.time_us += start.elapsed().as_micros() as u64;
175
176        TheoryExplanation {
177            explanation_type: ExplanationType::Equality,
178            literals: minimized_lits,
179            description: desc,
180            proof_trace,
181        }
182    }
183
184    /// Generate explanation for a bounds conflict.
185    ///
186    /// Explains why variable bounds are inconsistent (e.g., x > 10 ∧ x < 5).
187    pub fn explain_bounds_conflict(
188        &mut self,
189        lits: Vec<Lit>,
190        description: Option<String>,
191    ) -> TheoryExplanation {
192        let start = std::time::Instant::now();
193
194        let minimized_lits = if self.config.minimize {
195            self.minimize_explanation(&lits)
196        } else {
197            lits.clone()
198        };
199
200        let proof_trace = if self.config.generate_proofs {
201            Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Bounds))
202        } else {
203            None
204        };
205
206        let desc = if self.config.include_descriptions {
207            description.unwrap_or_else(|| {
208                format!("Bounds conflict with {} constraints", minimized_lits.len())
209            })
210        } else {
211            String::new()
212        };
213
214        self.stats.explanations_generated += 1;
215        self.stats.total_literals += minimized_lits.len() as u64;
216        self.update_avg_size();
217        self.stats.time_us += start.elapsed().as_micros() as u64;
218
219        TheoryExplanation {
220            explanation_type: ExplanationType::Bounds,
221            literals: minimized_lits,
222            description: desc,
223            proof_trace,
224        }
225    }
226
227    /// Generate explanation for an arithmetic conflict.
228    ///
229    /// Produces a Farkas coefficient explanation showing linear combination
230    /// that proves inconsistency.
231    pub fn explain_arithmetic_conflict(
232        &mut self,
233        lits: Vec<Lit>,
234        _farkas_coefficients: Option<Vec<i64>>,
235        description: Option<String>,
236    ) -> TheoryExplanation {
237        let start = std::time::Instant::now();
238
239        // In full implementation, use Farkas coefficients to generate
240        // minimal explanation via linear combination
241
242        let minimized_lits = if self.config.minimize {
243            self.minimize_explanation(&lits)
244        } else {
245            lits.clone()
246        };
247
248        let proof_trace = if self.config.generate_proofs {
249            Some(self.generate_proof_trace(&minimized_lits, ExplanationType::Arithmetic))
250        } else {
251            None
252        };
253
254        let desc = if self.config.include_descriptions {
255            description.unwrap_or_else(|| "Arithmetic conflict via linear combination".to_string())
256        } else {
257            String::new()
258        };
259
260        self.stats.explanations_generated += 1;
261        self.stats.total_literals += minimized_lits.len() as u64;
262        self.update_avg_size();
263        self.stats.time_us += start.elapsed().as_micros() as u64;
264
265        TheoryExplanation {
266            explanation_type: ExplanationType::Arithmetic,
267            literals: minimized_lits,
268            description: desc,
269            proof_trace,
270        }
271    }
272
273    /// Minimize an explanation by removing redundant literals.
274    fn minimize_explanation(&mut self, lits: &[Lit]) -> Vec<Lit> {
275        // Simplified minimization - full version would use:
276        // - Relevancy analysis
277        // - Core extraction
278        // - Subsumption checking
279
280        let mut minimized = Vec::new();
281        let _lit_set: FxHashSet<Lit> = lits.iter().copied().collect();
282
283        for &lit in lits {
284            // Check if removing this literal still gives a conflict
285            // For now, keep all literals (placeholder)
286            minimized.push(lit);
287        }
288
289        if minimized.len() < lits.len() {
290            self.stats.minimized += 1;
291        }
292
293        // Enforce max size
294        if minimized.len() > self.config.max_size {
295            minimized.truncate(self.config.max_size);
296        }
297
298        minimized
299    }
300
301    /// Generate proof trace for explanation.
302    fn generate_proof_trace(&self, lits: &[Lit], exp_type: ExplanationType) -> Vec<ProofStep> {
303        let mut trace = Vec::new();
304
305        // Generate initial premises
306        trace.push(ProofStep {
307            description: format!("Theory conflict detected: {:?}", exp_type),
308            premises: lits.to_vec(),
309            conclusion: "Contradiction".to_string(),
310        });
311
312        // Full implementation would include detailed proof steps
313
314        trace
315    }
316
317    /// Update average explanation size.
318    fn update_avg_size(&mut self) {
319        if self.stats.explanations_generated > 0 {
320            self.stats.avg_size =
321                self.stats.total_literals as f64 / self.stats.explanations_generated as f64;
322        }
323    }
324}
325
326impl Default for TheoryExplainer {
327    fn default() -> Self {
328        Self::new()
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use oxiz_sat::Var;
336
337    fn lit(var: u32, positive: bool) -> Lit {
338        let v = Var::new(var);
339        if positive { Lit::pos(v) } else { Lit::neg(v) }
340    }
341
342    #[test]
343    fn test_explainer_creation() {
344        let explainer = TheoryExplainer::new();
345        assert_eq!(explainer.stats().explanations_generated, 0);
346    }
347
348    #[test]
349    fn test_equality_explanation() {
350        let mut explainer = TheoryExplainer::new();
351        let lits = vec![lit(0, true), lit(1, false), lit(2, true)];
352
353        let explanation = explainer.explain_equality_conflict(lits.clone(), None);
354
355        assert_eq!(explanation.explanation_type, ExplanationType::Equality);
356        assert!(!explanation.literals.is_empty());
357        assert_eq!(explainer.stats().explanations_generated, 1);
358    }
359
360    #[test]
361    fn test_bounds_explanation() {
362        let mut explainer = TheoryExplainer::new();
363        let lits = vec![lit(0, true), lit(1, true)];
364
365        let explanation =
366            explainer.explain_bounds_conflict(lits, Some("x > 10 and x < 5".to_string()));
367
368        assert_eq!(explanation.explanation_type, ExplanationType::Bounds);
369        assert_eq!(explainer.stats().explanations_generated, 1);
370    }
371
372    #[test]
373    fn test_arithmetic_explanation() {
374        let mut explainer = TheoryExplainer::new();
375        let lits = vec![lit(0, false), lit(1, false), lit(2, false)];
376
377        let explanation = explainer.explain_arithmetic_conflict(lits, None, None);
378
379        assert_eq!(explanation.explanation_type, ExplanationType::Arithmetic);
380        assert_eq!(explainer.stats().explanations_generated, 1);
381    }
382
383    #[test]
384    fn test_minimization() {
385        let config = ExplainerConfig {
386            minimize: true,
387            ..Default::default()
388        };
389        let mut explainer = TheoryExplainer::with_config(config);
390
391        let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
392
393        let explanation = explainer.explain_equality_conflict(lits, None);
394
395        // Should attempt minimization
396        assert!(explanation.literals.len() <= 4);
397    }
398
399    #[test]
400    fn test_proof_generation() {
401        let config = ExplainerConfig {
402            generate_proofs: true,
403            ..Default::default()
404        };
405        let mut explainer = TheoryExplainer::with_config(config);
406
407        let lits = vec![lit(0, true), lit(1, false)];
408
409        let explanation = explainer.explain_equality_conflict(lits, None);
410
411        assert!(explanation.proof_trace.is_some());
412        let trace = explanation.proof_trace.unwrap();
413        assert!(!trace.is_empty());
414    }
415
416    #[test]
417    fn test_max_size_enforcement() {
418        let config = ExplainerConfig {
419            max_size: 2,
420            ..Default::default()
421        };
422        let mut explainer = TheoryExplainer::with_config(config);
423
424        let lits = vec![lit(0, true), lit(1, false), lit(2, true), lit(3, false)];
425
426        let explanation = explainer.explain_bounds_conflict(lits, None);
427
428        assert!(explanation.literals.len() <= 2);
429    }
430}