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