Skip to main content

rucc_verify/
lib.rs

1//! SMT verification of the rule set.
2//!
3//! Design: `spec/15-testing.md` section 15.5. Outside the layer stack: this runs in CI, not
4//! in the compiler.
5//!
6//! Every rewrite rule and every lowering rule carries a specification, and this crate
7//! discharges it. An unverified rule does not enter the rule set. Rules that the solver
8//! cannot discharge, usually wide bitvector multiplication, get a bounded proof over
9//! restricted widths plus a reviewed justification, and the count of those is a reported
10//! metric that going up is a signal about.
11//!
12//! The approach follows Crocus (ASPLOS 2024), cited in `spec/01-research-2026.md`.
13//!
14//! # What is asked
15//!
16//! A rule says that its pattern and its replacement compute the same thing, and its `spec`
17//! clause says what that thing is in bitvectors. Two obligations follow and both are asked as
18//! one question. The first is the one that matters: what the pattern means and what the
19//! replacement means have to be the same, both read out of the machine model rather than out of
20//! anybody's description of them. The second is the `spec` clause itself, which is written by
21//! hand and so is worth checking rather than trusting, because a rule whose stated claim is not
22//! what its pattern actually means would otherwise be verified against its own mistake.
23//!
24//! The question put to the solver is the negation: is there any assignment to the pattern's
25//! variables that makes either of those false? An `unsat` back means there is not, which is the
26//! rule discharged. A `sat` back is a counterexample and the rule is wrong. Nothing else counts
27//! as a pass, and in particular a solver that gives up is reported as having given up rather
28//! than folded into either answer.
29//!
30//! The guard is an assumption rather than part of the claim, which is what makes a rule that is
31//! only true for some constants provable at all.
32//!
33//! # When the solver gives up
34//!
35//! Some claims no solver settles in the time anybody will wait, and a multiplication of two
36//! unknowns at sixty four bits is the usual one. Such a rule may carry a `bounded` clause
37//! saying why a proof at narrower widths would be enough, and then the shrug is answered by
38//! asking the same question again at [`BOUNDED_WIDTHS`]. Every one of them has to come back
39//! `unsat`, the clause has to be there before any of it happens, and the result is a verdict of
40//! its own rather than a discharge, because a claim proved at four, eight and sixteen bits is
41//! not the claim the compiler relies on.
42//!
43//! The clause is a judgement somebody makes and signs for. A tool that fell back to narrow
44//! widths on its own would turn every rule the solver is slow on into a rule nobody checked,
45//! which is the failure this whole crate exists to prevent, so the fallback is never taken
46//! without a written reason and the number of times it was taken is printed.
47//!
48//! # The gate
49//!
50//! [`admit`] is the rule set's front door and the `rucc-verify` program is what CI runs it
51//! from. A file with anything in it that is not a proof is refused whole rather than having the
52//! failing rules dropped, because a compiler built from the rules that happened to pass is a
53//! compiler nobody described.
54//!
55//! # Widths
56//!
57//! A rule is written at the width its pattern's opcode names, and the terms inside it may name
58//! another. `(add.i32 (value.i64 x) (value.i64 y))` is a thirty two bit add of two sixty four bit
59//! registers, and both numbers are in the question that gets asked: `x` is declared sixty four
60//! bits wide and what the rule computes is thirty two. That is what a rule has to be able to say
61//! before `sext`, `zext` and `trunc` can be lowered at all, and the conversions themselves are
62//! written the way `spec/10-backend.md` writes them, `(sign_extend 32 64 x)` and
63//! `(extract 31 0 x)`, with the widths spelled out rather than inferred.
64//!
65//! When the machine term is wider than the IR term it replaces, which is what lowering a value
66//! into a wider register looks like, the two are asked to agree on the bits the IR term has. What
67//! the rest of the register holds is then left to the rule's `spec` clause, which is the only
68//! place a target's sign extension rule is written down and so the only place it can be checked.
69
70#![doc(html_root_url = "https://docs.rs/rucc-verify/0.2.21")]
71
72mod model;
73mod solver;
74mod verify;
75
76pub use model::{DEFAULT_WIDTH, Model, Widths, rule_width};
77pub use solver::{Answer, Solver};
78pub use verify::{BOUNDED_WIDTHS, Report, Verdict, admit, query, query_at, verify};
79
80/// The milestone in `spec/17-milestones.md` that fills this crate in.
81pub const MILESTONE: &str = "M3";