rucc_verify/verify.rs
1//! Turning a rule into a question, and the answers into a report.
2
3use std::fmt;
4
5use rucc_rules::{Error, Rule, Term, TermKind};
6
7use crate::model::{MEMORY_CONST, Model, Sort, Widths, rule_width};
8use crate::solver::{Answer, Solver};
9
10/// What became of one rule.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Verdict {
13 /// Nothing makes the claim false.
14 Discharged,
15 /// Something does, and this is what the solver printed of it.
16 Refuted(String),
17 /// The claim holds at every width narrower than the rule's own, and the rule carries a
18 /// written reason for taking that as enough. A pass, and a counted one.
19 Bounded {
20 /// The widths it was proved at, narrowest first.
21 widths: Vec<u32>,
22 /// The reason the rule gives, which is what a reviewer signed for.
23 why: String,
24 },
25 /// The solver gave up. Not a pass.
26 Unknown,
27}
28
29impl Verdict {
30 /// Whether a rule with this verdict may enter the rule set.
31 #[must_use]
32 pub fn accepted(&self) -> bool {
33 matches!(self, Verdict::Discharged | Verdict::Bounded { .. })
34 }
35
36 /// Why it may not, as a sentence, or nothing when it may.
37 #[must_use]
38 pub fn refusal(&self) -> Option<String> {
39 match self {
40 Verdict::Discharged | Verdict::Bounded { .. } => None,
41 Verdict::Refuted(model) => {
42 Some(format!("this rule is not true, and here is what makes it false: {model}"))
43 }
44 Verdict::Unknown => Some(
45 "the solver could not settle this rule, and a rule nobody has proved does not \
46 enter the rule set"
47 .to_owned(),
48 ),
49 }
50 }
51}
52
53/// What became of a rule set.
54#[derive(Debug, Default, Clone, PartialEq, Eq)]
55pub struct Report {
56 /// One verdict per rule, in the order the rules were given.
57 pub verdicts: Vec<Verdict>,
58}
59
60impl Report {
61 /// How many rules were discharged at their own width.
62 #[must_use]
63 pub fn discharged(&self) -> usize {
64 self.verdicts.iter().filter(|v| **v == Verdict::Discharged).count()
65 }
66
67 /// How many rules got a bounded proof instead.
68 ///
69 /// `spec/15-testing.md` section 15.5 asks for this number to be reported rather than merely
70 /// known, because it going up is the signal that the rule set is drifting towards claims
71 /// nobody is checking at the width the compiler runs at.
72 #[must_use]
73 pub fn bounded(&self) -> usize {
74 self.verdicts.iter().filter(|v| matches!(v, Verdict::Bounded { .. })).count()
75 }
76
77 /// Whether every rule was discharged at its own width. A bounded proof is not one of these.
78 #[must_use]
79 pub fn all_discharged(&self) -> bool {
80 self.verdicts.iter().all(|v| *v == Verdict::Discharged)
81 }
82
83 /// Whether every rule may enter the rule set, which allows a bounded proof and allows
84 /// nothing else. A solver that gave up is not a pass, because "we could not tell" is not
85 /// "it is correct".
86 #[must_use]
87 pub fn accepted(&self) -> bool {
88 self.verdicts.iter().all(Verdict::accepted)
89 }
90}
91
92impl fmt::Display for Report {
93 /// One line, which is what a build prints and what a person reads in a log.
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 let refused = self.verdicts.len() - self.discharged() - self.bounded();
96 let rules = if self.verdicts.len() == 1 { "rule" } else { "rules" };
97 write!(
98 f,
99 "{} {rules}: {} discharged, {} by bounded proof, {} refused",
100 self.verdicts.len(),
101 self.discharged(),
102 self.bounded(),
103 refused
104 )
105 }
106}
107
108/// The SMT-LIB question one rule asks, at the width the rule works in.
109///
110/// This is separate from asking it so that the query can be read, kept in a test, and handed to
111/// a solver by hand when one is arguing with it.
112///
113/// # Errors
114///
115/// Anything the model cannot write out, which is any term nobody has said the meaning of.
116pub fn query(path: &str, rule: &Rule, model: &Model) -> Result<String, Error> {
117 query_at(path, rule, model, rule_width(&rule.pattern))
118}
119
120/// The same question asked at a width somebody chose.
121///
122/// This is what a bounded proof is made of: the rule's own claim, in narrower bitvectors than
123/// the ones it will run in. Every width in the rule scales by the one ratio, so a rule that
124/// converts between widths still converts between them here.
125///
126/// # Errors
127///
128/// Anything the model cannot write out, which is any term nobody has said the meaning of, and
129/// anything whose widths do not fit together.
130pub fn query_at(path: &str, rule: &Rule, model: &Model, width: u32) -> Result<String, Error> {
131 let widths = Widths::at(&rule.pattern, width);
132
133 // A rule that reaches memory needs the theory of arrays and a constant to stand for the
134 // memory it starts from. A rule that does not gets neither, so every rule written before
135 // effects existed asks exactly the question it asked before.
136 let memory = [&rule.pattern, &rule.replacement, &rule.spec]
137 .iter()
138 .any(|term| model.touches_memory(term));
139 let logic = if memory { "QF_ABV" } else { "QF_BV" };
140 let mut out = format!("(set-logic {logic})\n");
141 if memory {
142 let sort = Sort::Memory.write(&widths);
143 out.push_str(&format!("(declare-const {MEMORY_CONST} {sort})\n"));
144 }
145
146 // Each name at the width the pattern binds it at, which is not one width for the whole rule:
147 // a rule that lowers a thirty two bit add of two sixty four bit registers has both numbers
148 // in it and neither is the other.
149 for (name, at) in widths.names() {
150 out.push_str(&format!("(declare-const {name} (_ BitVec {at}))\n"));
151 }
152
153 if let Some(guard) = &rule.guard {
154 // An assumption, not part of the claim. A rule that only holds for some constants is
155 // only being asked about those constants.
156 out.push_str(&format!("(assert {})\n", model.write(path, guard, &widths)?.0));
157 }
158
159 // Two obligations, asked as one question. The first is the one that matters: what the
160 // pattern means and what the replacement means have to be the same thing, both read out of
161 // the model rather than out of anybody's description of them. The second is the rule's own
162 // `spec` clause, which is written by hand and so is worth checking rather than trusting: a
163 // rule whose stated claim is not what its pattern actually means would otherwise verify
164 // against its own mistake.
165 let (matched, over) = model.write(path, &rule.pattern, &widths)?;
166 let (produced, into) = model.write(path, &rule.replacement, &widths)?;
167 let same = agreement(path, &rule.replacement, &matched, &produced, over, into)?;
168 let substituted = substitute(&rule.spec, &produced);
169 let claim = model.write(path, &substituted, &widths.with(&produced, into))?.0;
170 out.push_str(&format!("(assert (not (and {same} {claim})))\n"));
171 out.push_str("(check-sat)\n(get-model)\n");
172 Ok(out)
173}
174
175/// What it takes for a machine term to compute what the IR term it replaces computes.
176///
177/// The same bitvector, when the two are the same width, which is every rule that does not
178/// convert. When the machine term is wider they have to agree on the bits the IR term has, which
179/// is what lowering a value into a register wider than the value means, and what the rest of the
180/// register holds is left to the rule's own `spec` clause to claim: on a target where a thirty
181/// two bit add sign extends into a sixty four bit register, that clause is the only place the
182/// sign extension is stated and so it is the only place it can be checked.
183///
184/// A machine term narrower than the IR term loses bits, and that is a mistake rather than a
185/// claim about anything.
186fn agreement(
187 path: &str,
188 at: &Term,
189 matched: &str,
190 produced: &str,
191 over: Sort,
192 into: Sort,
193) -> Result<String, Error> {
194 if over == into {
195 return Ok(format!("(= {matched} {produced})"));
196 }
197 let fail = |said: String| Error {
198 path: path.to_owned(),
199 line: at.line,
200 column: at.column,
201 message: said,
202 };
203 let (Sort::Bits(over), Sort::Bits(into)) = (over, into) else {
204 // One of the two is a memory and the other is not, which is a rule replacing something
205 // with an effect by something without one or the other way round. There is no reading
206 // of that which is a mistake in the widths.
207 let said = "this replaces something that computes a value with something that computes a memory, or the other way round"
208 .to_owned();
209 return Err(fail(said));
210 };
211 if into < over {
212 let said = format!(
213 "what this replaces is {over} bits wide and this is {into}, so it cannot compute it"
214 );
215 return Err(fail(said));
216 }
217 Ok(format!("(= {matched} ((_ extract {} 0) {produced}))", over - 1))
218}
219
220/// The widths a bounded proof is taken over, narrowest first.
221///
222/// Two of them rather than one, because a claim that holds at a single width can hold for
223/// reasons that are about that width. Both of them small, because the claims that need a
224/// bounded proof at all are the ones mixing multiplication with division, and one of those is
225/// as far out of reach at sixteen bits as it is at sixty four: the rule the tests use is
226/// answered in hundredths of a second at eight bits and not at all at sixteen. Only widths
227/// narrower than the rule's own are used, so a rule that already works in four bits has nothing
228/// to fall back to.
229pub const BOUNDED_WIDTHS: [u32; 2] = [4, 8];
230
231/// Ask about every rule.
232///
233/// A rule that the solver settles at its own width is discharged and that is the end of it. A
234/// rule it gives up on is asked again at [`BOUNDED_WIDTHS`], but only if the rule carries a
235/// written reason for taking narrow widths as enough, because a bounded proof is a judgement
236/// somebody makes and not a fallback a tool takes on its own.
237///
238/// # Errors
239///
240/// Anything the model cannot write out, and anything that stops the solver from running.
241pub fn verify(
242 path: &str,
243 rules: &[Rule],
244 model: &Model,
245 solver: &Solver,
246) -> Result<Report, Vec<Error>> {
247 let mut report = Report::default();
248 let mut errors = Vec::new();
249
250 for rule in rules {
251 let width = rule_width(&rule.pattern);
252 match ask(path, rule, model, solver, width) {
253 Err(error) => errors.push(error),
254 Ok(Answer::Unsat) => report.verdicts.push(Verdict::Discharged),
255 Ok(Answer::Sat(found)) => report.verdicts.push(Verdict::Refuted(found)),
256 Ok(Answer::Unknown) => match &rule.bounded {
257 None => report.verdicts.push(Verdict::Unknown),
258 Some(why) => match bounded(path, rule, model, solver, width, why) {
259 Ok(verdict) => report.verdicts.push(verdict),
260 Err(error) => errors.push(error),
261 },
262 },
263 }
264 }
265
266 if errors.is_empty() { Ok(report) } else { Err(errors) }
267}
268
269/// Verify a rule set and refuse the whole of it if anything in it cannot enter.
270///
271/// This is the gate `spec/17-milestones.md` asks for. It refuses the file rather than dropping
272/// the rules that failed, because a compiler built from the rules that happened to pass is a
273/// compiler nobody described: what it does with the terms the dropped rules matched is then a
274/// question about the order of the rest.
275///
276/// # Errors
277///
278/// One error per rule that may not enter, at the line the rule starts on, and anything that
279/// stopped the verification from happening at all.
280pub fn admit(
281 path: &str,
282 rules: &[Rule],
283 model: &Model,
284 solver: &Solver,
285) -> Result<Report, Vec<Error>> {
286 let report = verify(path, rules, model, solver)?;
287 let mut errors = Vec::new();
288 for (rule, verdict) in rules.iter().zip(&report.verdicts) {
289 if let Some(said) = verdict.refusal() {
290 errors.push(Error {
291 path: path.to_owned(),
292 line: rule.line,
293 column: rule.column,
294 message: said,
295 });
296 }
297 }
298 if errors.is_empty() { Ok(report) } else { Err(errors) }
299}
300
301/// Put one question to the solver.
302fn ask(
303 path: &str,
304 rule: &Rule,
305 model: &Model,
306 solver: &Solver,
307 width: u32,
308) -> Result<Answer, Error> {
309 let asked = query_at(path, rule, model, width)?;
310 solver.ask(&asked).map_err(|problem| Error {
311 path: path.to_owned(),
312 line: rule.line,
313 column: rule.column,
314 message: format!("the solver could not be run: {problem}"),
315 })
316}
317
318/// Ask the rule again at the narrow widths, once the real one has come back a shrug.
319///
320/// Every width has to come back `unsat`. A counterexample at a narrow width is reported as the
321/// refutation it looks like, named with the width it was found at, because the two things it
322/// can be are a rule that is wrong and a rule whose constants do not fit in four bits, and both
323/// are for a person to look at rather than for this to decide.
324fn bounded(
325 path: &str,
326 rule: &Rule,
327 model: &Model,
328 solver: &Solver,
329 width: u32,
330 why: &str,
331) -> Result<Verdict, Error> {
332 let mut proved = Vec::new();
333 for narrow in BOUNDED_WIDTHS.iter().copied().filter(|narrow| *narrow < width) {
334 match ask(path, rule, model, solver, narrow)? {
335 Answer::Unsat => proved.push(narrow),
336 Answer::Sat(found) => {
337 let said = format!("at {narrow} bits, where the rule works in {width}: {found}");
338 return Ok(Verdict::Refuted(said));
339 }
340 Answer::Unknown => return Ok(Verdict::Unknown),
341 }
342 }
343 if proved.is_empty() {
344 return Ok(Verdict::Unknown);
345 }
346 Ok(Verdict::Bounded { widths: proved, why: why.to_owned() })
347}
348
349/// Put the replacement's meaning where the specification says `(result)`.
350///
351/// This is a substitution on the written form rather than on the term, because what the
352/// replacement means is SMT-LIB text by the time it is known and there is nothing to put back
353/// into a term.
354fn substitute(spec: &Term, produced: &str) -> Term {
355 match &spec.kind {
356 TermKind::App { head, args } if head == "result" && args.is_empty() => {
357 Term { kind: TermKind::Var(produced.to_owned()), line: spec.line, column: spec.column }
358 }
359 TermKind::App { head, args } => Term {
360 kind: TermKind::App {
361 head: head.clone(),
362 args: args.iter().map(|arg| substitute(arg, produced)).collect(),
363 },
364 line: spec.line,
365 column: spec.column,
366 },
367 _ => spec.clone(),
368 }
369}