use std::collections::BTreeSet;
use smtlite::{Model, Solution};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BranchRec {
pub site: u64,
pub taken: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum FlipOutcome {
Verified,
ProvenUnflippable,
SolverUnknown,
#[default]
NotPosed,
RunFailed,
Unconfirmed,
AlreadyExplored,
}
#[derive(Clone, Debug, Default)]
pub struct FlipRec {
pub at: u64,
pub input: Vec<(String, u64)>,
pub verified: bool,
pub outcome: FlipOutcome,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum PropertyOutcome {
Confirmed,
Proven,
SolverUnknown,
#[default]
NotPosed,
RunFailed,
Unreproduced,
}
#[derive(Clone, Debug, Default)]
pub struct PropertyRec {
pub label: String,
pub outcome: PropertyOutcome,
pub violation: Option<(u64, u64)>,
pub input: Vec<(String, u64)>,
}
#[derive(Clone, Debug, Default)]
pub struct SinkRec {
pub site: u64,
pub kind: String,
pub args: Vec<(String, u64)>,
}
#[derive(Clone, Debug, Default)]
pub struct RunResult {
pub result: Option<i64>,
pub trapped: Option<String>,
pub log: Vec<BranchRec>,
pub sinks: Vec<SinkRec>,
}
#[derive(Clone, Debug, Default)]
pub struct Report {
pub result: Option<i64>,
pub runs: usize,
pub flips_verified: usize,
pub sinks: Vec<SinkRec>,
pub properties: Vec<PropertyRec>,
pub flips: Vec<FlipRec>,
pub gaps: Vec<String>,
pub exhausted: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Opts {
pub max_runs: usize,
pub max_flips: usize,
}
impl Default for Opts {
fn default() -> Self {
Self {
max_runs: 64,
max_flips: 32,
}
}
}
pub trait Frontend {
type Seed: Clone;
type Walk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String>;
fn walk(&self, seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk>;
fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution;
fn site(&self, walk: &Self::Walk, i: usize) -> u64;
fn model_inputs(&self, model: &Model, walk: &Self::Walk) -> Vec<(String, u64)>;
fn apply_inputs(&self, seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed;
fn walk_gaps(&self, _walk: &Self::Walk) -> Vec<String> {
Vec::new()
}
fn walk_sinks(&self, _walk: &Self::Walk) -> Vec<SinkRec> {
Vec::new()
}
}
struct Pending<S> {
seed: S,
run: RunResult,
}
struct Attempt<S> {
rec: FlipRec,
verified: bool,
ran: bool,
gap: Option<String>,
child: Option<Pending<S>>,
}
#[must_use]
pub fn explore<F: Frontend>(frontend: &F, base: &F::Seed, opts: &Opts) -> Report {
let mut report = Report::default();
let mut seen: BTreeSet<Vec<bool>> = BTreeSet::new();
let first = match frontend.run(base) {
Ok(r) => r,
Err(e) => {
report.gaps.push(format!("run failed: {e}"));
return report;
}
};
report.runs = 1;
report.result = first.result;
let mut frontier = vec![Pending { seed: base.clone(), run: first }];
while let Some(item) = frontier.pop() {
if report.runs >= opts.max_runs || report.flips_verified >= opts.max_flips {
return report;
}
if let Some(t) = &item.run.trapped {
report.gaps.push(format!("run trapped: {t}"));
}
let path: Vec<bool> = item.run.log.iter().map(|b| b.taken).collect();
if !seen.insert(path) {
continue;
}
let Some(mut walk) = frontend.walk(&item.seed, &item.run.log) else {
report
.gaps
.push("walk could not replay the concrete path".to_string());
continue;
};
report.sinks.extend(frontend.walk_sinks(&walk));
report.gaps.extend(frontend.walk_gaps(&walk));
let (mut spent, mut flipped) = (0usize, 0usize);
let mut attempts = Vec::new();
for i in (0..item.run.log.len()).rev() {
if report.runs + spent >= opts.max_runs
|| report.flips_verified + flipped >= opts.max_flips
{
break;
}
let a = attempt(frontend, &item.seed, &item.run.log, &mut walk, i, &seen);
spent += usize::from(a.ran);
flipped += usize::from(a.verified);
attempts.push(a);
}
for a in attempts {
report.runs += usize::from(a.ran);
if let Some(g) = a.gap {
report.gaps.push(g);
}
report.flips.push(a.rec);
if a.verified {
report.flips_verified += 1;
if let Some(child) = a.child {
frontier.push(child);
}
}
}
}
report.exhausted = true;
report
}
fn attempt<F: Frontend>(
frontend: &F,
seed: &F::Seed,
log: &[BranchRec],
walk: &mut F::Walk,
i: usize,
seen: &BTreeSet<Vec<bool>>,
) -> Attempt<F::Seed> {
let at = frontend.site(walk, i);
let solution = frontend.flip(walk, i);
let Solution::Sat(m) = &solution else {
let outcome = match solution {
Solution::Unsat => FlipOutcome::ProvenUnflippable,
Solution::Unknown => FlipOutcome::SolverUnknown,
_ => FlipOutcome::NotPosed,
};
let gap = (outcome == FlipOutcome::SolverUnknown)
.then(|| format!("solver Unknown at {at:#x}"));
return Attempt {
rec: FlipRec { at, input: Vec::new(), verified: false, outcome },
verified: false,
ran: false,
gap,
child: None,
};
};
let inputs = frontend.model_inputs(m, walk);
let child_seed = frontend.apply_inputs(seed, &inputs);
let child_run = match frontend.run(&child_seed) {
Ok(r) => r,
Err(_) => {
return Attempt {
rec: FlipRec {
at,
input: inputs,
verified: false,
outcome: FlipOutcome::RunFailed,
},
verified: false,
ran: true,
gap: None,
child: None,
};
}
};
let prefix_ok = child_run.log.len() > i
&& child_run.log[..i]
.iter()
.zip(&log[..i])
.all(|(c, p)| c.site == p.site && c.taken == p.taken);
let flipped =
prefix_ok && child_run.log[i].site == log[i].site && child_run.log[i].taken != log[i].taken;
let child_path: Vec<bool> = child_run.log.iter().map(|b| b.taken).collect();
let verified = flipped && !seen.contains(&child_path);
let outcome = if verified {
FlipOutcome::Verified
} else if flipped {
FlipOutcome::AlreadyExplored
} else {
FlipOutcome::Unconfirmed
};
Attempt {
rec: FlipRec { at, input: inputs, verified, outcome },
verified,
ran: true,
gap: None,
child: verified.then_some(Pending { seed: child_seed, run: child_run }),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Toy;
#[derive(Clone)]
struct Seed {
x: u8,
}
struct Walk {
solver: smtlite::Solver,
x: smtlite::Bv,
conds: Vec<smtlite::Bv>,
taken: Vec<bool>,
path: Vec<smtlite::Bv>,
}
impl Frontend for Toy {
type Seed = Seed;
type Walk = Walk;
fn run(&self, seed: &Self::Seed) -> Result<RunResult, String> {
let mut log = Vec::new();
let x = seed.x;
log.push(BranchRec { site: 0x100, taken: x < 0x10 });
if x < 0x10 {
log.push(BranchRec { site: 0x200, taken: x == 0x42 });
}
Ok(RunResult { result: Some(i64::from(x)), trapped: None, log, sinks: Vec::new() })
}
fn walk(&self, _seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk> {
let mut solver = smtlite::Solver::new();
let x = solver.var("in!x", 8);
let mut conds = Vec::new();
let mut taken = Vec::new();
let mut path = Vec::new();
for b in log {
let cond = match b.site {
0x100 => x.ult(&smtlite::Bv::val(0x10, 8)),
0x200 => x.eq(&smtlite::Bv::val(0x42, 8)),
_ => return None,
};
path.push(if b.taken { cond.clone() } else { cond.not() });
conds.push(cond);
taken.push(b.taken);
}
Some(Walk { solver, x, conds, taken, path })
}
fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution {
let g_i = if walk.taken[i] {
walk.conds[i].not()
} else {
walk.conds[i].clone()
};
let mut assumptions = walk.path[..i].to_vec();
assumptions.push(g_i);
walk.solver.check_all(&assumptions)
}
fn site(&self, walk: &Self::Walk, i: usize) -> u64 {
let _ = walk;
[0x100, 0x200][i]
}
fn model_inputs(&self, model: &Model, _walk: &Self::Walk) -> Vec<(String, u64)> {
model.get("in!x").map(|v| vec![("in!x".to_string(), v)]).unwrap_or_default()
}
fn apply_inputs(&self, _seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed {
Seed { x: inputs.first().map_or(0, |(_, v)| *v as u8) }
}
}
#[test]
fn the_loop_verifies_a_real_flip_and_proves_the_unreachable_one() {
let r = explore(&Toy, &Seed { x: 0 }, &Opts { max_runs: 64, max_flips: 32 });
assert_eq!(r.runs, 3, "flips={:?}", r.flips);
assert_eq!(r.flips_verified, 1, "flips={:?}", r.flips);
assert_eq!(
r.flips.iter().map(|f| (f.at, f.outcome)).collect::<Vec<_>>(),
vec![
(0x200, FlipOutcome::ProvenUnflippable),
(0x100, FlipOutcome::Verified),
(0x100, FlipOutcome::AlreadyExplored),
]
);
assert!(r.exhausted);
}
}