use serde::{Deserialize, Serialize};
use crate::proof_status::ProofLevel;
use crate::schema::Contract;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum L2Status {
Tested,
Untested,
Unknown,
}
impl L2Status {
#[must_use]
pub fn is_tested(self) -> bool {
matches!(self, Self::Tested)
}
#[must_use]
pub fn mark(self) -> &'static str {
match self {
Self::Tested => "\u{2713}",
Self::Untested => "\u{2717}",
Self::Unknown => "?",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObligationStatus {
pub property: String,
pub obligation_type: String,
pub l2: L2Status,
pub l3_kani: bool,
pub l4_lean: bool,
pub max_level: ProofLevel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractObligationMatrix {
pub stem: String,
pub obligations: Vec<ObligationStatus>,
}
struct LinkTally {
cited: Vec<bool>,
resolved: usize,
}
fn obligation_index(contract: &Contract, token: &str) -> Option<usize> {
contract.proof_obligations.iter().position(|ob| {
ob.id.as_deref().is_some_and(|id| id.trim() == token) || ob.property.trim() == token
})
}
fn cites_existing_test(contract: &Contract, token: &str) -> bool {
if let Some(inner) = token
.strip_prefix("falsification_tests[")
.and_then(|rest| rest.strip_suffix(']'))
{
return inner
.trim()
.parse::<usize>()
.is_ok_and(|i| i < contract.falsification_tests.len());
}
contract
.falsification_tests
.iter()
.any(|ft| ft.id.trim() == token)
}
fn tally_links(contract: &Contract) -> LinkTally {
let mut tally = LinkTally {
cited: vec![false; contract.proof_obligations.len()],
resolved: 0,
};
for ft in &contract.falsification_tests {
let Some(citation) = ft.obligation.as_ref() else {
continue;
};
for token in citation.targets() {
if let Some(idx) = obligation_index(contract, token) {
tally.cited[idx] = true;
tally.resolved += 1;
}
}
}
for (idx, ob) in contract.proof_obligations.iter().enumerate() {
let Some(citation) = ob.discharged_by.as_ref() else {
continue;
};
for token in citation.targets() {
if cites_existing_test(contract, token) {
tally.cited[idx] = true;
tally.resolved += 1;
}
}
}
tally
}
fn l2_verdict(contract: &Contract, tally: &LinkTally, idx: usize) -> L2Status {
if contract.falsification_tests.is_empty() {
return L2Status::Untested;
}
if tally.resolved == 0 {
return L2Status::Unknown;
}
if tally.cited.get(idx).copied().unwrap_or(false) {
L2Status::Tested
} else {
L2Status::Untested
}
}
pub fn obligation_matrix(contracts: &[(String, &Contract)]) -> Vec<ContractObligationMatrix> {
contracts
.iter()
.map(|(stem, contract)| {
let tally = tally_links(contract);
let obligations = contract
.proof_obligations
.iter()
.enumerate()
.map(|(idx, ob)| {
let prop_lower = ob.property.to_lowercase();
let l2 = l2_verdict(contract, &tally, idx);
let l3_kani = contract.kani_harnesses.iter().any(|kh| {
if let Some(ref kh_prop) = kh.property {
let kh_lower = kh_prop.to_lowercase();
property_words_match(&prop_lower, &kh_lower)
} else {
false
}
});
let l4_lean = ob
.lean
.as_ref()
.is_some_and(|lp| lp.status == crate::schema::LeanStatus::Proved);
let max_level = if l4_lean {
ProofLevel::L4
} else if l3_kani {
ProofLevel::L3
} else if l2.is_tested() {
ProofLevel::L2
} else {
ProofLevel::L1
};
ObligationStatus {
property: ob.property.clone(),
obligation_type: ob.obligation_type.to_string(),
l2,
l3_kani,
l4_lean,
max_level,
}
})
.collect();
ContractObligationMatrix {
stem: stem.clone(),
obligations,
}
})
.collect()
}
pub fn format_obligation_table(matrices: &[ContractObligationMatrix]) -> String {
let mut out = String::new();
out.push_str("\nObligation Status Matrix\n");
out.push_str("========================\n");
for matrix in matrices {
if matrix.obligations.is_empty() {
continue;
}
let max_prop_width = matrix
.obligations
.iter()
.map(|o| o.property.len())
.max()
.unwrap_or(20)
.min(40);
out.push_str(&format!("Contract: {}\n", matrix.stem));
out.push_str(&format!(
" {:<width$} | L2 Test | L3 Kani | L4 Lean | Status\n",
"Obligation",
width = max_prop_width
));
out.push_str(&format!(" {}\n", "-".repeat(max_prop_width + 39)));
for ob in &matrix.obligations {
let check = "\u{2713}";
let cross = "\u{2717}";
let l2 = ob.l2.mark();
let l3 = if ob.l3_kani { check } else { cross };
let l4 = if ob.l4_lean { check } else { cross };
let prop_display = truncate(&ob.property, max_prop_width);
out.push_str(&format!(
" {:<width$} | {} | {} | {} | {}\n",
prop_display,
l2,
l3,
l4,
ob.max_level,
width = max_prop_width
));
}
out.push('\n');
}
out
}
pub fn truncate(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn property_words_match(a: &str, b: &str) -> bool {
let stop_words: &[&str] = &[
"the", "for", "and", "all", "are", "with", "from", "that", "this", "has", "not", "any",
"each", "into",
];
let words_a: Vec<&str> = a
.split(|c: char| !c.is_alphanumeric())
.filter(|w| w.len() >= 3 && !stop_words.contains(w))
.collect();
words_a.iter().any(|wa| {
b.split(|c: char| !c.is_alphanumeric())
.filter(|w| w.len() >= 3 && !stop_words.contains(w))
.any(|wb| wa == &wb)
})
}