use std::fmt;
const BITVECTOR_MARKER: &str = ";; query spun off because: bitvector";
const BITBLAST_MARKER: &str = "tactic.default_tactic sat";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VerusError {
NotBitVector,
NoBitBlastBlock,
NoCheckSat,
}
impl fmt::Display for VerusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VerusError::NotBitVector => write!(
f,
"not a `by (bit_vector)` query: no `{BITVECTOR_MARKER}` marker \
(a Verus prelude dump such as root.smt2, or an ordinary \
quantified query, is outside the QF_BV fragment)"
),
VerusError::NoBitBlastBlock => write!(
f,
"bitvector query has no `{BITBLAST_MARKER}` block — unexpected Verus log shape"
),
VerusError::NoCheckSat => write!(f, "bitvector block has no `(check-sat)`"),
}
}
}
impl std::error::Error for VerusError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BvObligation {
pub location: Option<String>,
pub script: String,
}
pub fn extract(log: &str) -> Result<BvObligation, VerusError> {
if !log.contains(BITVECTOR_MARKER) {
return Err(VerusError::NotBitVector);
}
let lines: Vec<&str> = log.lines().collect();
let start = lines
.iter()
.rposition(|l| l.contains(BITBLAST_MARKER))
.ok_or(VerusError::NoBitBlastBlock)?;
let end = lines[start..]
.iter()
.position(|l| l.trim() == "(check-sat)")
.map(|i| start + i)
.ok_or(VerusError::NoCheckSat)?;
let location = lines[..start]
.iter()
.rev()
.find(|l| l.starts_with(";; ") && l.contains(".rs:"))
.map(|l| l.trim_start_matches(";; ").trim().to_string());
let mut out = String::from("(set-logic QF_BV)\n");
for line in &lines[start..=end] {
let t = line.trim_start();
if t.starts_with("(set-option") || t.starts_with("(get-info") {
continue;
}
out.push_str(line);
out.push('\n');
}
Ok(BvObligation {
location,
script: out,
})
}
pub fn is_bitvector_query(log: &str) -> bool {
log.contains(BITVECTOR_MARKER)
}
#[cfg(test)]
mod tests {
use super::*;
const REAL_LOG: &str = include_str!("../tests/fixtures/verus_gale_cpu_mask_raw.smt2");
const REAL_MPU_LOG: &str = include_str!("../tests/fixtures/verus_gale_mpu_pow2_raw.smt2");
#[test]
fn extracts_the_bitvector_block_from_a_real_verus_log() {
let o = extract(REAL_LOG).expect("real Verus log must yield an obligation");
assert!(
o.script.starts_with("(set-logic QF_BV)"),
"slice must be a standalone QF_BV script"
);
assert!(
o.script.contains("(check-sat)"),
"slice must end in check-sat"
);
assert!(
o.script.contains("%%location_label%%0"),
"slice must carry the real goal, got: {}",
&o.script[..200.min(o.script.len())]
);
}
#[test]
fn slice_excludes_the_quantified_prelude() {
let o = extract(REAL_LOG).unwrap();
assert!(
REAL_LOG.contains("forall"),
"fixture should contain the prelude's quantifiers"
);
assert!(
!o.script.contains("forall"),
"the slice must not contain quantifiers"
);
assert!(
!o.script.contains("declare-datatypes") && !o.script.contains("declare-sort"),
"the slice must not contain the prelude's sorts/datatypes"
);
}
#[test]
fn slice_drops_solver_configuration() {
let o = extract(REAL_LOG).unwrap();
assert!(!o.script.contains("set-option"));
assert!(!o.script.contains("get-info"));
}
#[test]
fn reports_the_source_location() {
let o = extract(REAL_LOG).unwrap();
let loc = o.location.expect("Verus records a source location");
assert!(
loc.contains(".rs:"),
"expected a file:line location, got {loc}"
);
}
#[test]
fn refuses_a_log_that_is_not_a_bitvector_query() {
let ordinary = "(declare-const x Int)\n(assert (forall ((y Int)) (> y 0)))\n(check-sat)\n";
assert_eq!(extract(ordinary), Err(VerusError::NotBitVector));
assert!(!is_bitvector_query(ordinary));
assert!(is_bitvector_query(REAL_LOG));
}
#[test]
fn marked_but_malformed_logs_error_rather_than_guess() {
let no_block = format!("{BITVECTOR_MARKER}\n(assert true)\n(check-sat)\n");
assert_eq!(extract(&no_block), Err(VerusError::NoBitBlastBlock));
let no_check = format!("{BITVECTOR_MARKER}\n(set-option :{BITBLAST_MARKER})\n(assert x)\n");
assert_eq!(extract(&no_check), Err(VerusError::NoCheckSat));
}
#[test]
fn gale_mpu_biconditional_discharges() {
let o = extract(REAL_MPU_LOG).expect("gale's mpu obligation must lift");
assert!(
!o.script.contains("forall"),
"240 prelude quantifiers must stay out of the slice"
);
let outcome = crate::smtlib::solve_str(&o.script).expect("slice must parse");
match outcome.result.expect("has check-sat") {
crate::CheckResult::Unsat(cert) => {
cert.recheck().expect("certificate must re-check");
}
other => panic!("gale mpu.rs:98 is_power_of_two must be UNSAT, got {other:?}"),
}
}
#[test]
fn lifted_obligation_discharges_with_a_recheckable_certificate() {
let o = extract(REAL_LOG).unwrap();
let outcome = crate::smtlib::solve_str(&o.script).expect("slice must parse");
match outcome.result.expect("script has a check-sat") {
crate::CheckResult::Unsat(cert) => {
cert.recheck().expect("certificate must re-check");
}
other => panic!("gale's cpu_mask obligation must be UNSAT, got {other:?}"),
}
}
}