use std::path::Path;
use std::time::Instant;
use serde::Serialize;
use crate::ontology::liskov::{
check, liskov_corpus, liskov_sha256, liskov_witness_path, pc_checker, LiskovWitness, Pair,
PairClass, LISKOV_WITNESS_DIR,
};
use crate::ontology::sigma::SigmaError;
use crate::ontology::verdict::{Reason, Verdict};
use crate::ontology::witness::FIRED;
use super::finding::LintFinding;
use super::relations_gate::{corpus_documents, typed_graph, TypedGraph};
use super::rules::RuleSeverity;
use super::{GateDetail, GateExtra, GateResult};
pub const GATE: &str = "refines";
#[derive(Debug)]
pub enum RefinesOutcome {
NoSigma,
Malformed(SigmaError),
PositiveControlFailed { which: &'static str, why: String },
WitnessStale { path: String, why: String },
Ran {
result: Box<GateResult>,
findings: Vec<LintFinding>,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct LiskovWitnessReport {
pub path: String,
pub pc_reasoner: String,
pub liskov_sha256: String,
pub reasoner_git_sha: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct RefinesCounters {
pub refines_pairs: usize,
pub liskov_pairs_checked: usize,
pub liskov_pairs_legacy: usize,
pub liskov_prose: usize,
pub liskov_prose_baseline: Option<usize>,
pub prose_clauses: Vec<String>,
pub requires_n: usize,
pub ensures_n: usize,
pub invariants_n: usize,
pub pc_checker: String,
pub violations: usize,
pub witness: Option<LiskovWitnessReport>,
}
fn malformed_findings(corpus: &crate::ontology::liskov::LiskovCorpus) -> Vec<LintFinding> {
corpus
.malformed
.iter()
.map(|(file, why)| {
LintFinding::new(
"PV-ONT-026",
RuleSeverity::Error,
format!("clause is not {{id, statement, formal, formal_status}}: {why}"),
file.clone(),
)
})
.collect()
}
fn classify_pairs(pairs: Vec<Pair>, c: &mut RefinesCounters) -> Vec<Pair> {
let mut checkable = Vec::new();
for p in pairs {
match p.class() {
PairClass::Legacy => c.liskov_pairs_legacy += 1,
PairClass::Prose(names) => {
c.liskov_prose += 1;
c.prose_clauses.extend(names);
}
PairClass::Checkable => checkable.push(p),
}
}
checkable
}
fn baseline_finding(c: &RefinesCounters) -> Option<LintFinding> {
let baseline = c.liskov_prose_baseline?;
if c.liskov_prose <= baseline {
return None;
}
Some(LintFinding::new(
"PV-ONT-027",
RuleSeverity::Error,
format!(
"liskov_prose rose {baseline} -> {}: a refines pair with a prose clause was added ({}). The baseline in contracts/lint-baseline.json is shrink-only",
c.liskov_prose,
c.prose_clauses.join(", ")
),
"contracts/lint-baseline.json",
))
}
fn refines_verdict(findings: &[LintFinding], liskov_prose: usize) -> Verdict {
if !findings.is_empty() {
Verdict::Fail
} else if liskov_prose > 0 {
Verdict::Unknown(Reason::Prose)
} else {
Verdict::Pass
}
}
#[must_use]
pub fn run_refines_gate(contract_dir: &Path) -> RefinesOutcome {
let start = Instant::now();
let pc = match pc_checker() {
Ok(fired) => fired,
Err(why) => {
return RefinesOutcome::PositiveControlFailed {
which: "pc_checker",
why,
}
}
};
let (ids, edges) = match typed_graph(contract_dir) {
TypedGraph::NoSigma => return RefinesOutcome::NoSigma,
TypedGraph::Malformed(e) => return RefinesOutcome::Malformed(e),
TypedGraph::Read { ids, edges } => (ids, edges),
};
let corpus = liskov_corpus(&corpus_documents(contract_dir), &edges);
let mut findings = malformed_findings(&corpus);
let mut c = RefinesCounters {
refines_pairs: corpus.refines_edges,
requires_n: corpus.counts.0,
ensures_n: corpus.counts.1,
invariants_n: corpus.counts.2,
pc_checker: pc.to_string(),
liskov_prose_baseline: baseline_liskov_prose(contract_dir),
..RefinesCounters::default()
};
let checkable = classify_pairs(corpus.pairs, &mut c);
findings.extend(baseline_finding(&c));
if !checkable.is_empty() {
match checked_witness(contract_dir, &checkable) {
Err(outcome) => return outcome,
Ok((report, verdicts)) => {
c.liskov_pairs_checked = checkable.len();
c.witness = Some(report);
findings.extend(verdicts);
}
}
}
c.violations = findings.len();
let verdict = refines_verdict(&findings, c.liskov_prose);
let result = GateResult {
name: GATE.into(),
passed: verdict == Verdict::Pass,
skipped: false,
verdict,
duration_ms: u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
detail: GateDetail::Validate {
contracts: ids.len(),
errors: findings.len(),
warnings: 0,
error_messages: findings.iter().map(|f| f.message.clone()).collect(),
},
extra: Some(GateExtra::Refines(Box::new(c))),
};
RefinesOutcome::Ran {
result: Box::new(result),
findings,
}
}
fn checked_witness(
contract_dir: &Path,
checkable: &[Pair],
) -> Result<(LiskovWitnessReport, Vec<LintFinding>), RefinesOutcome> {
let sha = liskov_sha256(checkable);
let shown = format!("{LISKOV_WITNESS_DIR}/{sha}.json");
let stale = |why: String| RefinesOutcome::WitnessStale {
path: shown.clone(),
why,
};
let text = std::fs::read_to_string(liskov_witness_path(contract_dir, &sha))
.map_err(|e| stale(format!("{shown}: {e}")))?;
let witness: LiskovWitness = serde_json::from_str(&text)
.map_err(|e| stale(format!("{shown} does not parse as a Liskov witness: {e}")))?;
if witness.liskov_sha256 != sha {
return Err(stale(format!(
"{shown} records liskov_sha256 {}, and the pairs are {sha}",
witness.liskov_sha256
)));
}
if witness.pc_reasoner != FIRED {
return Err(RefinesOutcome::PositiveControlFailed {
which: "pc_reasoner",
why: format!("{shown} records pc_reasoner `{}`", witness.pc_reasoner),
});
}
let findings = match check(checkable, &witness) {
Ok(violations) => violations
.iter()
.map(|v| {
LintFinding::new(
"PV-ONT-024",
RuleSeverity::Error,
v.to_string(),
format!("contracts/{}.yaml", v.a),
)
})
.collect(),
Err(e) => vec![LintFinding::new(
"PV-ONT-025",
RuleSeverity::Error,
format!("{shown} does not check against the current refines pairs: {e}. Re-run `make contracts`; never edit a witness by hand"),
format!("contracts/{shown}"),
)],
};
let report = LiskovWitnessReport {
path: shown,
pc_reasoner: witness.pc_reasoner,
liskov_sha256: sha,
reasoner_git_sha: witness.reasoner_git_sha,
};
Ok((report, findings))
}
fn baseline_liskov_prose(contract_dir: &Path) -> Option<usize> {
let raw = std::fs::read_to_string(contract_dir.join("lint-baseline.json")).ok()?;
let doc: serde_json::Value = serde_json::from_str(&raw).ok()?;
doc.get("ont")?
.get("liskov_prose")?
.as_u64()
.and_then(|n| usize::try_from(n).ok())
}
#[must_use]
pub fn decline_reason(outcome: &RefinesOutcome) -> Option<Reason> {
match outcome {
RefinesOutcome::NoSigma => Some(Reason::NoCheckable),
RefinesOutcome::PositiveControlFailed { .. } => Some(Reason::PositiveControlFailed),
RefinesOutcome::WitnessStale { .. } => Some(Reason::WitnessStale),
RefinesOutcome::Malformed(_) | RefinesOutcome::Ran { .. } => None,
}
}
#[must_use]
pub fn why(outcome: &RefinesOutcome) -> String {
match outcome {
RefinesOutcome::NoSigma => "no contracts/ontology.yaml".into(),
RefinesOutcome::Malformed(e) => format!("Σ is malformed: {e}"),
RefinesOutcome::PositiveControlFailed { which, why } => {
format!("positive control {which} did not fire: {why}")
}
RefinesOutcome::WitnessStale { why, .. } => format!(
"the Liskov witness is stale: {why}. Run `make contracts` to regenerate it with pv-sat"
),
RefinesOutcome::Ran { .. } => String::new(),
}
}
#[must_use]
pub fn explain(result: &GateResult, findings: &[LintFinding]) -> Vec<String> {
match result.verdict {
Verdict::Fail => findings.iter().map(|f| f.message.clone()).collect(),
Verdict::Unknown(Reason::Prose) => match &result.extra {
Some(GateExtra::Refines(c)) => vec![format!(
"refines: Unknown{{Prose}} — prose clause(s), not checked: {}",
c.prose_clauses.join(", ")
)],
_ => Vec::new(),
},
_ => Vec::new(),
}
}
#[cfg(test)]
#[path = "refines_gate_tests.rs"]
mod tests;