#[allow(unused_imports)]
use crate::prelude::*;
#[derive(Debug, Clone)]
pub struct PropagationStep {
pub literal: i64,
pub description: String,
pub reason_clause: Option<u32>,
pub level: u32,
}
#[derive(Debug, Clone)]
pub struct TheoryConflictInfo {
pub theory: String,
pub reason: String,
pub involved_terms: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ConflictExplanation {
pub clause_id: u32,
pub contributing_assertions: Vec<u32>,
pub propagation_chain: Vec<PropagationStep>,
pub theory_info: Option<TheoryConflictInfo>,
pub minimal_subset: Vec<u32>,
}
impl ConflictExplanation {
pub fn format(&self) -> String {
let mut out = String::new();
out.push_str(&format!("=== Conflict Explanation (clause #{}) ===\n\n", self.clause_id));
out.push_str("Contributing assertions:\n");
if self.contributing_assertions.is_empty() {
out.push_str(" (none identified)\n");
} else {
for &idx in &self.contributing_assertions {
out.push_str(&format!(" - Assertion #{}\n", idx));
}
}
out.push('\n');
out.push_str("Propagation chain:\n");
if self.propagation_chain.is_empty() {
out.push_str(" (empty)\n");
} else {
for (i, step) in self.propagation_chain.iter().enumerate() {
let reason = step
.reason_clause
.map_or_else(|| "decision".to_string(), |c| format!("clause #{}", c));
out.push_str(&format!(
" {}. {} (level={}, reason={})\n",
i + 1,
step.description,
step.level,
reason
));
}
}
out.push('\n');
if let Some(ref ti) = self.theory_info {
out.push_str(&format!("Theory conflict detected by: {}\n", ti.theory));
out.push_str(&format!(" Reason: {}\n", ti.reason));
if !ti.involved_terms.is_empty() {
out.push_str(" Involved terms:\n");
for term in &ti.involved_terms {
out.push_str(&format!(" - {}\n", term));
}
}
out.push('\n');
}
if !self.minimal_subset.is_empty() {
out.push_str("Minimal conflicting subset:\n");
for &idx in &self.minimal_subset {
out.push_str(&format!(" - Assertion #{}\n", idx));
}
}
out
}
}
#[derive(Debug, Clone)]
pub struct UnsatExplanation {
pub all_assertions: Vec<u32>,
pub conflict_chain: Vec<ConflictExplanation>,
pub minimal_unsat_subset: Vec<u32>,
pub summary: String,
}
impl UnsatExplanation {
pub fn format(&self) -> String {
let mut out = String::new();
out.push_str("=== UNSAT Explanation ===\n\n");
out.push_str(&format!("Summary: {}\n\n", self.summary));
out.push_str(&format!(
"Total assertions: {}\n",
self.all_assertions.len()
));
out.push_str(&format!(
"Minimal UNSAT subset size: {}\n\n",
self.minimal_unsat_subset.len()
));
if !self.minimal_unsat_subset.is_empty() {
out.push_str("Minimal UNSAT subset:\n");
for &idx in &self.minimal_unsat_subset {
out.push_str(&format!(" - Assertion #{}\n", idx));
}
out.push('\n');
}
out.push_str(&format!(
"Conflict chain ({} conflicts):\n",
self.conflict_chain.len()
));
for (i, conflict) in self.conflict_chain.iter().enumerate() {
out.push_str(&format!("\n--- Conflict {} ---\n", i + 1));
out.push_str(&conflict.format());
}
out
}
}
#[derive(Debug)]
pub struct ConflictExplainer {
assertion_descriptions: Vec<String>,
recorded_conflicts: Vec<ConflictExplanation>,
propagation_history: Vec<PropagationStep>,
}
impl ConflictExplainer {
pub fn new() -> Self {
Self {
assertion_descriptions: Vec::new(),
recorded_conflicts: Vec::new(),
propagation_history: Vec::new(),
}
}
pub fn register_assertion(&mut self, index: u32, description: impl Into<String>) {
let desc = description.into();
let idx = index as usize;
if idx >= self.assertion_descriptions.len() {
self.assertion_descriptions.resize(idx + 1, String::new());
}
self.assertion_descriptions[idx] = desc;
}
pub fn record_propagation(&mut self, step: PropagationStep) {
self.propagation_history.push(step);
}
pub fn clear_propagation_history(&mut self) {
self.propagation_history.clear();
}
pub fn record_conflict(
&mut self,
clause_id: u32,
contributing_assertions: Vec<u32>,
theory_info: Option<TheoryConflictInfo>,
) {
let prop_chain = self.propagation_history.clone();
let minimal = self.compute_minimal_subset(&contributing_assertions);
let explanation = ConflictExplanation {
clause_id,
contributing_assertions,
propagation_chain: prop_chain,
theory_info,
minimal_subset: minimal,
};
self.recorded_conflicts.push(explanation);
}
pub fn explain_conflict(&self, clause_id: u32) -> Option<ConflictExplanation> {
self.recorded_conflicts
.iter()
.find(|c| c.clause_id == clause_id)
.cloned()
}
pub fn explain_unsat(&self) -> UnsatExplanation {
let mut all_assertions: Vec<u32> = Vec::new();
for conflict in &self.recorded_conflicts {
for &idx in &conflict.contributing_assertions {
if !all_assertions.contains(&idx) {
all_assertions.push(idx);
}
}
}
all_assertions.sort_unstable();
let mut minimal_set: Vec<u32> = Vec::new();
for conflict in &self.recorded_conflicts {
for &idx in &conflict.minimal_subset {
if !minimal_set.contains(&idx) {
minimal_set.push(idx);
}
}
}
minimal_set.sort_unstable();
if minimal_set.is_empty() {
minimal_set = all_assertions.clone();
}
let summary = if self.recorded_conflicts.is_empty() {
"UNSAT detected but no conflicts were recorded.".to_string()
} else if self.recorded_conflicts.len() == 1 {
let conflict = &self.recorded_conflicts[0];
if let Some(ref ti) = conflict.theory_info {
format!(
"UNSAT due to {} theory conflict: {}",
ti.theory, ti.reason
)
} else {
format!(
"UNSAT due to conflict in clause #{}",
conflict.clause_id
)
}
} else {
format!(
"UNSAT after {} conflicts involving {} assertions",
self.recorded_conflicts.len(),
all_assertions.len()
)
};
UnsatExplanation {
all_assertions,
conflict_chain: self.recorded_conflicts.clone(),
minimal_unsat_subset: minimal_set,
summary,
}
}
pub fn num_conflicts(&self) -> usize {
self.recorded_conflicts.len()
}
pub fn clear(&mut self) {
self.assertion_descriptions.clear();
self.recorded_conflicts.clear();
self.propagation_history.clear();
}
fn compute_minimal_subset(&self, assertions: &[u32]) -> Vec<u32> {
if assertions.len() <= 1 {
return assertions.to_vec();
}
let referenced: FxHashSet<u32> = {
let mut set = FxHashSet::default();
for step in &self.propagation_history {
if let Some(clause) = step.reason_clause {
set.insert(clause);
}
}
set
};
let mut minimal: Vec<u32> = assertions
.iter()
.copied()
.filter(|idx| referenced.contains(idx) || assertions.len() <= 2)
.collect();
if minimal.is_empty() {
minimal = assertions.to_vec();
}
minimal.sort_unstable();
minimal
}
}
impl Default for ConflictExplainer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conflict_explainer_new() {
let explainer = ConflictExplainer::new();
assert_eq!(explainer.num_conflicts(), 0);
}
#[test]
fn test_record_and_explain_conflict() {
let mut explainer = ConflictExplainer::new();
explainer.register_assertion(0, "x > 0");
explainer.register_assertion(1, "x < 0");
explainer.record_propagation(PropagationStep {
literal: 1,
description: "x1 = true (from x > 0)".to_string(),
reason_clause: Some(0),
level: 0,
});
explainer.record_propagation(PropagationStep {
literal: -2,
description: "x2 = false (from x < 0)".to_string(),
reason_clause: Some(1),
level: 0,
});
explainer.record_conflict(
5,
vec![0, 1],
Some(TheoryConflictInfo {
theory: "LRA".to_string(),
reason: "x > 0 AND x < 0 is contradictory".to_string(),
involved_terms: vec!["x".to_string()],
}),
);
assert_eq!(explainer.num_conflicts(), 1);
let explanation = explainer.explain_conflict(5);
assert!(explanation.is_some());
let expl = explanation.expect("should have explanation");
assert_eq!(expl.clause_id, 5);
assert_eq!(expl.contributing_assertions, vec![0, 1]);
assert!(expl.theory_info.is_some());
assert_eq!(expl.propagation_chain.len(), 2);
}
#[test]
fn test_explain_unsat() {
let mut explainer = ConflictExplainer::new();
explainer.register_assertion(0, "p");
explainer.register_assertion(1, "NOT p");
explainer.record_conflict(1, vec![0, 1], None);
let unsat = explainer.explain_unsat();
assert_eq!(unsat.all_assertions, vec![0, 1]);
assert!(!unsat.summary.is_empty());
assert_eq!(unsat.conflict_chain.len(), 1);
}
#[test]
fn test_format_conflict_explanation() {
let explanation = ConflictExplanation {
clause_id: 3,
contributing_assertions: vec![0, 1, 2],
propagation_chain: vec![
PropagationStep {
literal: 1,
description: "x = true".to_string(),
reason_clause: None,
level: 0,
},
PropagationStep {
literal: -2,
description: "y = false".to_string(),
reason_clause: Some(1),
level: 0,
},
],
theory_info: Some(TheoryConflictInfo {
theory: "EUF".to_string(),
reason: "congruence conflict".to_string(),
involved_terms: vec!["f(a)".to_string(), "f(b)".to_string()],
}),
minimal_subset: vec![0, 1],
};
let text = explanation.format();
assert!(text.contains("Conflict Explanation (clause #3)"));
assert!(text.contains("Assertion #0"));
assert!(text.contains("x = true"));
assert!(text.contains("EUF"));
assert!(text.contains("congruence conflict"));
assert!(text.contains("f(a)"));
}
#[test]
fn test_format_unsat_explanation() {
let mut explainer = ConflictExplainer::new();
explainer.record_conflict(
1,
vec![0, 1],
Some(TheoryConflictInfo {
theory: "LRA".to_string(),
reason: "linear arithmetic conflict".to_string(),
involved_terms: vec![],
}),
);
let unsat = explainer.explain_unsat();
let text = unsat.format();
assert!(text.contains("UNSAT Explanation"));
assert!(text.contains("LRA"));
assert!(text.contains("linear arithmetic conflict"));
}
#[test]
fn test_explainer_clear() {
let mut explainer = ConflictExplainer::new();
explainer.register_assertion(0, "test");
explainer.record_conflict(1, vec![0], None);
assert_eq!(explainer.num_conflicts(), 1);
explainer.clear();
assert_eq!(explainer.num_conflicts(), 0);
}
}