use std::fmt;
use rucc_rules::{Error, Rule, Term, TermKind};
use crate::model::{Model, Widths, rule_width};
use crate::solver::{Answer, Solver};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Discharged,
Refuted(String),
Bounded {
widths: Vec<u32>,
why: String,
},
Unknown,
}
impl Verdict {
#[must_use]
pub fn accepted(&self) -> bool {
matches!(self, Verdict::Discharged | Verdict::Bounded { .. })
}
#[must_use]
pub fn refusal(&self) -> Option<String> {
match self {
Verdict::Discharged | Verdict::Bounded { .. } => None,
Verdict::Refuted(model) => {
Some(format!("this rule is not true, and here is what makes it false: {model}"))
}
Verdict::Unknown => Some(
"the solver could not settle this rule, and a rule nobody has proved does not \
enter the rule set"
.to_owned(),
),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Report {
pub verdicts: Vec<Verdict>,
}
impl Report {
#[must_use]
pub fn discharged(&self) -> usize {
self.verdicts.iter().filter(|v| **v == Verdict::Discharged).count()
}
#[must_use]
pub fn bounded(&self) -> usize {
self.verdicts.iter().filter(|v| matches!(v, Verdict::Bounded { .. })).count()
}
#[must_use]
pub fn all_discharged(&self) -> bool {
self.verdicts.iter().all(|v| *v == Verdict::Discharged)
}
#[must_use]
pub fn accepted(&self) -> bool {
self.verdicts.iter().all(Verdict::accepted)
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let refused = self.verdicts.len() - self.discharged() - self.bounded();
let rules = if self.verdicts.len() == 1 { "rule" } else { "rules" };
write!(
f,
"{} {rules}: {} discharged, {} by bounded proof, {} refused",
self.verdicts.len(),
self.discharged(),
self.bounded(),
refused
)
}
}
pub fn query(path: &str, rule: &Rule, model: &Model) -> Result<String, Error> {
query_at(path, rule, model, rule_width(&rule.pattern))
}
pub fn query_at(path: &str, rule: &Rule, model: &Model, width: u32) -> Result<String, Error> {
let widths = Widths::at(&rule.pattern, width);
let mut out = String::from("(set-logic QF_BV)\n");
for (name, at) in widths.names() {
out.push_str(&format!("(declare-const {name} (_ BitVec {at}))\n"));
}
if let Some(guard) = &rule.guard {
out.push_str(&format!("(assert {})\n", model.write(path, guard, &widths)?.0));
}
let (matched, over) = model.write(path, &rule.pattern, &widths)?;
let (produced, into) = model.write(path, &rule.replacement, &widths)?;
let same = agreement(path, &rule.replacement, &matched, &produced, over, into)?;
let substituted = substitute(&rule.spec, &produced);
let claim = model.write(path, &substituted, &widths.with(&produced, into))?.0;
out.push_str(&format!("(assert (not (and {same} {claim})))\n"));
out.push_str("(check-sat)\n(get-model)\n");
Ok(out)
}
fn agreement(
path: &str,
at: &Term,
matched: &str,
produced: &str,
over: u32,
into: u32,
) -> Result<String, Error> {
if over == into {
return Ok(format!("(= {matched} {produced})"));
}
if into < over {
let said = format!(
"what this replaces is {over} bits wide and this is {into}, so it cannot compute it"
);
return Err(Error {
path: path.to_owned(),
line: at.line,
column: at.column,
message: said,
});
}
Ok(format!("(= {matched} ((_ extract {} 0) {produced}))", over - 1))
}
pub const BOUNDED_WIDTHS: [u32; 2] = [4, 8];
pub fn verify(
path: &str,
rules: &[Rule],
model: &Model,
solver: &Solver,
) -> Result<Report, Vec<Error>> {
let mut report = Report::default();
let mut errors = Vec::new();
for rule in rules {
let width = rule_width(&rule.pattern);
match ask(path, rule, model, solver, width) {
Err(error) => errors.push(error),
Ok(Answer::Unsat) => report.verdicts.push(Verdict::Discharged),
Ok(Answer::Sat(found)) => report.verdicts.push(Verdict::Refuted(found)),
Ok(Answer::Unknown) => match &rule.bounded {
None => report.verdicts.push(Verdict::Unknown),
Some(why) => match bounded(path, rule, model, solver, width, why) {
Ok(verdict) => report.verdicts.push(verdict),
Err(error) => errors.push(error),
},
},
}
}
if errors.is_empty() { Ok(report) } else { Err(errors) }
}
pub fn admit(
path: &str,
rules: &[Rule],
model: &Model,
solver: &Solver,
) -> Result<Report, Vec<Error>> {
let report = verify(path, rules, model, solver)?;
let mut errors = Vec::new();
for (rule, verdict) in rules.iter().zip(&report.verdicts) {
if let Some(said) = verdict.refusal() {
errors.push(Error {
path: path.to_owned(),
line: rule.line,
column: rule.column,
message: said,
});
}
}
if errors.is_empty() { Ok(report) } else { Err(errors) }
}
fn ask(
path: &str,
rule: &Rule,
model: &Model,
solver: &Solver,
width: u32,
) -> Result<Answer, Error> {
let asked = query_at(path, rule, model, width)?;
solver.ask(&asked).map_err(|problem| Error {
path: path.to_owned(),
line: rule.line,
column: rule.column,
message: format!("the solver could not be run: {problem}"),
})
}
fn bounded(
path: &str,
rule: &Rule,
model: &Model,
solver: &Solver,
width: u32,
why: &str,
) -> Result<Verdict, Error> {
let mut proved = Vec::new();
for narrow in BOUNDED_WIDTHS.iter().copied().filter(|narrow| *narrow < width) {
match ask(path, rule, model, solver, narrow)? {
Answer::Unsat => proved.push(narrow),
Answer::Sat(found) => {
let said = format!("at {narrow} bits, where the rule works in {width}: {found}");
return Ok(Verdict::Refuted(said));
}
Answer::Unknown => return Ok(Verdict::Unknown),
}
}
if proved.is_empty() {
return Ok(Verdict::Unknown);
}
Ok(Verdict::Bounded { widths: proved, why: why.to_owned() })
}
fn substitute(spec: &Term, produced: &str) -> Term {
match &spec.kind {
TermKind::App { head, args } if head == "result" && args.is_empty() => {
Term { kind: TermKind::Var(produced.to_owned()), line: spec.line, column: spec.column }
}
TermKind::App { head, args } => Term {
kind: TermKind::App {
head: head.clone(),
args: args.iter().map(|arg| substitute(arg, produced)).collect(),
},
line: spec.line,
column: spec.column,
},
_ => spec.clone(),
}
}