use rucc_rules::{Rule, parse};
use rucc_verify::{Model, Report, Solver, Verdict, Widths, admit, query, query_at, verify};
const MODEL: &str = "\
(semantics (add.i64 left right) (bvadd left right))
(semantics (mul.i64 left right) (bvmul left right))
(semantics (shl.i64 value amount) (bvshl value amount))
(semantics (value v) v)
(semantics (iconst c) c)
(semantics (amode_base_index_scale base index scale) (bvadd base (bvmul index scale)))
(semantics (x64.lea address) address)
(semantics (x64.shl value amount) (bvshl value amount))
(semantics (x64.add left right) (bvadd left right))
(semantics (udiv.i64 left right) (bvudiv left right))
(semantics (x64.udiv left right) (bvudiv left right))
(semantics (add.i32 left right) (bvadd (extract 31 0 left) (extract 31 0 right)))
(semantics (value.i64 v) v)
(semantics (value.i32 v) v)
(semantics (rv.addw a b) (sign_extend 32 64 (bvadd (extract 31 0 a) (extract 31 0 b))))
(semantics (rv.addwu a b) (zero_extend 32 64 (bvadd (extract 31 0 a) (extract 31 0 b))))";
const LEA: &str = "\
(rule (lower (add.i64 (value x) (mul.i64 (value y) (iconst 4))))
(x64.lea (amode_base_index_scale x y 4))
(spec (= (bvadd x (bvmul y 4)) (result))))";
fn rules(text: &str) -> Vec<Rule> {
match parse("t.rules", text) {
Ok(rules) => rules,
Err(errors) => panic!("{}", errors[0]),
}
}
fn model() -> Model {
match Model::read("t.model", MODEL) {
Ok(model) => model,
Err(errors) => panic!("{}", errors[0]),
}
}
fn solver() -> Option<Solver> {
let found = Solver::find();
assert!(
!(found.is_none() && std::env::var_os("RUCC_REQUIRE_SOLVER").is_some()),
"a solver was required and none was found on PATH"
);
found
}
#[test]
fn the_question_a_rule_asks_is_the_negation_of_its_claim() {
let asked = query("t.rules", &rules(LEA)[0], &model()).expect("the model covers this rule");
let expected = "\
(set-logic QF_BV)
(declare-const x (_ BitVec 64))
(declare-const y (_ BitVec 64))
(assert (not (and (= (bvadd x (bvmul y (_ bv4 64))) (bvadd x (bvmul y (_ bv4 64)))) (= (bvadd x (bvmul y (_ bv4 64))) (bvadd x (bvmul y (_ bv4 64)))))))
(check-sat)
(get-model)
";
assert_eq!(asked, expected);
}
#[test]
fn a_guard_is_asserted_rather_than_claimed() {
let text = "\
(rule (lower (shl.i64 (value x) (iconst k)))
(if (and (>= k 0) (< k 64)))
(x64.shl x k)
(spec (= (bvshl x k) (result))))";
let asked = query("t.rules", &rules(text)[0], &model()).expect("the model covers this rule");
assert!(asked.contains("(assert (and (bvsge k (_ bv0 64)) (bvslt k (_ bv64 64))))"), "{asked}");
assert!(asked.contains("(= (bvshl x k) (bvshl x k))"), "{asked}");
}
#[test]
fn the_rules_the_design_document_writes_out_are_discharged() {
let Some(solver) = solver() else {
return;
};
let text = format!(
"{LEA}\n(rule (lower (add.i64 (value x) (value y))) (x64.add x y) (spec (= (bvadd x y) (result))))"
);
let report = verify("t.rules", &rules(&text), &model(), &solver).expect("nothing to report");
assert!(report.all_discharged(), "{report:?}");
assert_eq!(report.discharged(), 2);
}
#[test]
fn a_rule_that_is_wrong_is_refuted_and_the_counterexample_is_kept() {
let Some(solver) = solver() else {
return;
};
let text = "\
(rule (lower (add.i64 (value x) (mul.i64 (value y) (iconst 4))))
(x64.lea (amode_base_index_scale x y 8))
(spec (= (bvadd x (bvmul y 4)) (result))))";
let report = verify("t.rules", &rules(text), &model(), &solver).expect("nothing to report");
assert!(!report.all_discharged());
let Verdict::Refuted(counterexample) = &report.verdicts[0] else {
panic!("that rule is wrong: {:?}", report.verdicts[0]);
};
assert!(counterexample.contains("define-fun"), "{counterexample}");
}
#[test]
fn a_rule_that_needs_its_guard_fails_without_it() {
let Some(solver) = solver() else {
return;
};
let text = "\
(rule (lower (shl.i64 (value x) (iconst k)))
(x64.shl x (bvadd k 1))
(spec (= (bvshl x k) (result))))";
let report = verify("t.rules", &rules(text), &model(), &solver).expect("nothing to report");
assert!(matches!(report.verdicts[0], Verdict::Refuted(_)), "{report:?}");
}
#[test]
fn a_rule_whose_stated_claim_is_not_what_its_pattern_means_is_refuted() {
let Some(solver) = solver() else {
return;
};
let text = "\
(rule (lower (add.i64 (value x) (value y)))
(x64.add x y)
(spec (= (bvsub x y) (result))))";
let report = verify("t.rules", &rules(text), &model(), &solver).expect("nothing to report");
assert!(matches!(report.verdicts[0], Verdict::Refuted(_)), "{report:?}");
}
#[test]
fn a_term_nobody_has_said_the_meaning_of_is_refused() {
let text = "\
(rule (lower (add.i64 (value x) (value y)))
(x64.frobnicate x y)
(spec (= (bvadd x y) (result))))";
let failed = query("t.rules", &rules(text)[0], &model()).expect_err("nothing defines that");
assert_eq!(
failed.to_string(),
"t.rules:2:7: nothing in the model says what `x64.frobnicate` means"
);
}
#[test]
fn a_head_the_solver_already_knows_cannot_be_given_a_second_meaning() {
let failed = Model::read("t.model", "(semantics (bvadd a b) (bvsub a b))")
.expect_err("that is not something to redefine");
assert_eq!(
failed[0].to_string(),
"t.model:1:12: `bvadd` is something the solver already knows"
);
}
#[test]
fn a_model_that_is_not_a_model_says_so() {
let failed = Model::read("t.model", "(x64.lea a)").expect_err("that is not a semantics form");
assert_eq!(failed[0].to_string(), "t.model:1:1: expected a `(semantics ...)` form");
}
const HARD: &str = "\
(rule (lower (udiv.i64 (value x) (value y)))
(x64.udiv x y)
(spec (= (bvmul (result) y) (bvsub x (bvurem x y))))
(bounded \"division against multiplication is out of reach at sixty four bits\"))";
fn quick_solver() -> Option<Solver> {
solver().map(|solver| solver.within(2))
}
#[test]
fn the_same_question_can_be_asked_at_a_narrower_width() {
let asked = query_at("t.rules", &rules(LEA)[0], &model(), 8).expect("the model covers this");
assert!(asked.contains("(declare-const x (_ BitVec 8))"), "{asked}");
assert!(asked.contains("(_ bv4 8)"), "{asked}");
assert!(!asked.contains("64"), "{asked}");
}
#[test]
fn a_rule_the_solver_gives_up_on_gets_a_bounded_proof_if_it_asked_for_one() {
let Some(solver) = quick_solver() else {
return;
};
let report = verify("t.rules", &rules(HARD), &model(), &solver).expect("nothing to report");
let Verdict::Bounded { widths, why } = &report.verdicts[0] else {
panic!("that rule is the one bounded proofs exist for: {:?}", report.verdicts[0]);
};
assert_eq!(widths, &[4, 8]);
assert_eq!(why, "division against multiplication is out of reach at sixty four bits");
assert_eq!(report.bounded(), 1);
assert_eq!(report.discharged(), 0);
assert!(!report.all_discharged());
assert!(report.accepted());
}
#[test]
fn a_rule_that_did_not_ask_for_a_bounded_proof_is_left_unknown() {
let Some(solver) = quick_solver() else {
return;
};
let text = HARD.replace(
"\n (bounded \"division against multiplication is out of reach at sixty four bits\")",
"",
);
let report = verify("t.rules", &rules(&text), &model(), &solver).expect("nothing to report");
assert_eq!(report.verdicts, vec![Verdict::Unknown]);
assert_eq!(report.bounded(), 0);
assert!(!report.accepted());
}
#[test]
fn a_rule_that_is_not_proved_keeps_the_whole_file_out() {
let Some(solver) = quick_solver() else {
return;
};
let wrong = "\
(rule (lower (add.i64 (value x) (value y)))
(x64.add x y)
(spec (= (bvsub x y) (result))))";
let text = format!("{LEA}\n{wrong}");
let errors = admit("t.rules", &rules(&text), &model(), &solver).expect_err("one is wrong");
assert_eq!(errors.len(), 1);
assert!(errors[0].to_string().starts_with("t.rules:4:1: this rule is not true"), "{errors:?}");
}
#[test]
fn a_file_of_rules_that_are_all_proved_is_admitted() {
let Some(solver) = quick_solver() else {
return;
};
let text = format!("{LEA}\n{HARD}");
let report = admit("t.rules", &rules(&text), &model(), &solver).expect("both are proved");
assert_eq!(report.discharged(), 1);
assert_eq!(report.bounded(), 1);
assert_eq!(report.to_string(), "2 rules: 1 discharged, 1 by bounded proof, 0 refused");
}
const ADDW: &str = "\
(rule (lower (add.i32 (value.i64 x) (value.i64 y)))
(rv.addw x y)
(spec (= (sign_extend 32 64 (bvadd (extract 31 0 x) (extract 31 0 y))) (result))))";
#[test]
fn a_rule_that_changes_width_is_discharged() {
let Some(solver) = solver() else {
return;
};
let report = verify("t.rules", &rules(ADDW), &model(), &solver).expect("nothing to report");
assert!(report.all_discharged(), "{report:?}");
}
#[test]
fn a_name_is_as_wide_as_the_pattern_binds_it() {
let asked = query("t.rules", &rules(ADDW)[0], &model()).expect("the model covers this rule");
assert!(asked.contains("(declare-const x (_ BitVec 64))"), "{asked}");
assert!(asked.contains("((_ sign_extend 32)"), "{asked}");
assert!(
asked.contains("(= (bvadd ((_ extract 31 0) x) ((_ extract 31 0) y)) ((_ extract 31 0)")
);
}
#[test]
fn what_the_wider_register_holds_is_claimed_by_the_specification() {
let Some(solver) = solver() else {
return;
};
let text = ADDW.replace("rv.addw", "rv.addwu");
let report = verify("t.rules", &rules(&text), &model(), &solver).expect("nothing to report");
assert!(matches!(report.verdicts[0], Verdict::Refuted(_)), "{report:?}");
}
#[test]
fn a_narrower_question_keeps_the_widths_apart() {
let asked = query_at("t.rules", &rules(ADDW)[0], &model(), 8).expect("the model covers this");
assert!(asked.contains("(declare-const x (_ BitVec 16))"), "{asked}");
assert!(asked.contains("((_ sign_extend 8)"), "{asked}");
assert!(asked.contains("((_ extract 7 0) x)"), "{asked}");
}
#[test]
fn a_replacement_narrower_than_what_it_replaces_is_refused() {
let text = "\
(rule (lower (add.i64 (value x) (value y)))
(x64.add (extract 31 0 x) (extract 31 0 y))
(spec (= (bvadd x y) (result))))";
let failed = query("t.rules", &rules(text)[0], &model()).expect_err("that loses bits");
assert_eq!(
failed.to_string(),
"t.rules:2:7: what this replaces is 64 bits wide and this is 32, so it cannot compute it"
);
}
#[test]
fn an_opcode_that_means_something_of_another_width_is_refused() {
let text = "\
(semantics (add.i32 left right) (bvadd left right))
(semantics (value.i64 v) v)";
let model = Model::read("t.model", text).expect("that is a model");
let rule = &rules(
"(rule (lower (add.i32 (value.i64 x) (value.i64 y))) (x64.add x y) (spec (= x (result))))",
)[0];
let widths = Widths::of(&rule.pattern);
let failed = model.write("t.rules", &rule.pattern, &widths).expect_err("that is 64 bits wide");
assert_eq!(
failed.to_string(),
"t.rules:1:14: `add.i32` is written for 32 bits and means something 64 bits wide"
);
}
#[test]
fn adding_two_things_of_different_widths_is_refused() {
let rule = &rules(
"(rule (lower (add.i64 (value x) (value.i32 y))) (x64.add x y) (spec (= x (result))))",
)[0];
let model = model();
let widths = Widths::of(&rule.pattern);
let failed =
model.write("t.rules", &rule.pattern, &widths).expect_err("those are different widths");
assert!(
failed.to_string().ends_with(
"`bvadd` is given something 64 bits wide and something 32 bits wide, and those are \
not the same kind of thing"
),
"{failed}"
);
}
#[test]
fn a_report_says_what_became_of_every_rule() {
let report = Report {
verdicts: vec![
Verdict::Discharged,
Verdict::Bounded { widths: vec![4, 8], why: "wide multiplication".to_owned() },
Verdict::Unknown,
Verdict::Refuted("(define-fun x () (_ BitVec 64) #x0)".to_owned()),
],
};
assert_eq!(report.to_string(), "4 rules: 1 discharged, 1 by bounded proof, 2 refused");
assert!(!report.accepted());
assert!(!report.all_discharged());
}