1use std::collections::BTreeMap;
16use std::path::Path;
17use toml::Value as T;
18
19const RATIO: &str = "docs/verification-ratio.toml";
20const ASSUMPTIONS: &str = "docs/assumptions.md";
21const BYPASSES: &str = "docs/bypasses.md";
22const CATALOG: &str = crate::catalog::DIR;
23const VERIFIED: &[&str] = &["crates/policy/src", "crates/schema/src", "crates/txn/src"];
24
25fn rust_files(dir: &Path, out: &mut Vec<String>) {
26 for e in std::fs::read_dir(dir).into_iter().flatten().flatten() {
27 let p = e.path();
28 if p.is_dir() {
29 rust_files(&p, out);
30 } else if p.extension().is_some_and(|x| x == "rs") {
31 out.push(std::fs::read_to_string(&p).unwrap_or_default());
32 }
33 }
34}
35
36fn source(krate: &str) -> Vec<String> {
37 let mut v = vec![];
38 rust_files(&Path::new("crates").join(krate).join("src"), &mut v);
39 v
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
47pub enum Discharge {
48 Verus,
49 Kani,
50}
51
52pub fn discharge_in(files: &[String], name: &str) -> Option<Discharge> {
53 let pat = format!("fn {name}");
54 for f in files {
55 for (i, _) in f.match_indices(&pat) {
56 let after = &f[i + pat.len()..];
57 if !after.chars().next().is_some_and(|c| c == '(' || c == '<') {
58 continue;
59 }
60 let line_start = f[..i].rfind('\n').map_or(0, |n| n + 1);
61 let attrs: Vec<&str> =
63 f[..line_start].lines().rev().map(str::trim_start).take_while(|l| l.starts_with("#[")).collect();
64 if attrs.iter().any(|l| l.starts_with("#[kani::proof")) {
65 return Some(Discharge::Kani);
66 }
67 if !f[..i].contains("verus!") {
68 continue;
69 }
70 let head = &after[..after.find('{').unwrap_or(after.len())];
71 if f[line_start..i].contains("proof ") || head.contains("ensures") {
72 return Some(Discharge::Verus);
73 }
74 }
75 }
76 None
77}
78
79fn discharge(krate: &str, name: &str) -> Option<Discharge> {
80 discharge_in(&source(krate), name)
81}
82
83pub fn structure_errors(doc: &T) -> Vec<String> {
87 let mut errors = vec![];
88 let mut seen = std::collections::BTreeSet::new();
89 for inv in doc.get("invariant").and_then(T::as_array).into_iter().flatten() {
90 let id = inv.get("id").and_then(T::as_str).unwrap_or("?");
91 if !seen.insert(id.to_string()) {
92 errors.push(format!("{id} is accounted twice"));
93 }
94 let mut names = std::collections::BTreeSet::new();
95 for n in inv.get("discharged").and_then(T::as_array).into_iter().flatten().filter_map(T::as_str) {
96 if !names.insert(n) {
97 errors.push(format!("{id}: {n} is listed twice"));
98 }
99 }
100 }
101 if let Some(bp) = doc.get("baseline").and_then(|b| b.get("per_invariant")).and_then(T::as_table) {
102 for id in bp.keys() {
103 if !seen.contains(id) {
104 errors.push(format!("{id} is in the baseline but no longer accounted (a removed invariant fails)"));
105 }
106 }
107 }
108 errors
109}
110
111fn trust_sites() -> usize {
114 let mut files = vec![];
115 for d in VERIFIED {
116 rust_files(Path::new(d), &mut files);
117 }
118 files
119 .iter()
120 .map(|f| {
121 f.matches("#[verifier::external_body]").count()
122 + f.lines().filter(|l| l.contains("assume(") && !l.contains("kani::assume")).count()
123 })
124 .sum()
125}
126
127pub struct Report {
128 pub text: String,
129 pub ok: bool,
130}
131
132fn bypasses() -> (BTreeMap<String, Vec<String>>, Vec<String>) {
134 match std::fs::read_to_string(BYPASSES) {
135 Ok(text) => parse_bypasses(&text),
136 Err(_) => (BTreeMap::new(), vec![format!("{BYPASSES} is missing")]),
137 }
138}
139
140pub fn parse_bypasses(text: &str) -> (BTreeMap<String, Vec<String>>, Vec<String>) {
141 let mut open: BTreeMap<String, Vec<String>> = BTreeMap::new();
142 let mut errors = vec![];
143 let mut seen = std::collections::BTreeSet::new();
144 const TABLES: [&str; 3] = ["Open", "Closed", "Accepted"];
147 let mut table: Option<&str> = None;
148 let mut tables: Vec<&str> = vec![];
149 for line in text.lines() {
150 if let Some(h) = line.strip_prefix("## ") {
151 let h = h.trim();
152 table = TABLES.iter().copied().find(|t| *t == h);
153 match table {
154 Some(t) => tables.push(t),
155 None => errors.push(format!("{BYPASSES}: section {h:?} is not one of Open, Closed, Accepted")),
156 }
157 continue;
158 }
159 let cells: Vec<&str> = line.trim().trim_matches('|').split('|').map(str::trim).collect();
160 let id = cells.first().copied().unwrap_or("");
161 let (kind, width) = match id.split_once('-') {
162 Some(("B", n)) if n.parse::<u32>().is_ok() => ('B', 8),
163 Some(("X", n)) if n.parse::<u32>().is_ok() => ('X', 6),
164 _ => continue,
165 };
166 let want = if kind == 'X' { "Accepted" } else { "Open or Closed" };
167 match (kind, table) {
168 (_, None) => errors.push(format!("{BYPASSES}: {id} is outside the Open, Closed and Accepted tables")),
169 ('X', Some("Accepted")) | ('B', Some("Open" | "Closed")) => {}
170 (_, Some(t)) => errors.push(format!("{BYPASSES}: {id} is in {t}; a {kind}- row belongs in {want}")),
171 }
172 if !seen.insert(id.to_string()) {
173 errors.push(format!("{BYPASSES}: {id} appears twice (ids are never reused)"));
174 }
175 if cells.len() != width {
176 errors.push(format!("{BYPASSES}: {id} has {} cells, want {width}", cells.len()));
177 continue;
178 }
179 let invariants = cells[width - 1];
181 let named = invariants == "all"
182 || invariants == "โ"
183 || invariants
184 .split(',')
185 .map(str::trim)
186 .all(|i| i.strip_prefix('I').is_some_and(|n| n.parse::<u32>().is_ok()));
187 if invariants.is_empty() || !named {
188 errors.push(format!("{BYPASSES}: {id}: invariants {invariants:?} is not a list of ids, `all` or `โ`"));
189 }
190 if kind == 'X' {
191 continue;
192 }
193 let (severity, status) = (cells[3], cells[4]);
194 if !matches!(severity, "High" | "Medium" | "Low") {
195 errors.push(format!("{BYPASSES}: {id}: severity {severity:?} is not High, Medium or Low"));
196 }
197 let closed = status.starts_with("Closed (") && status.ends_with(')');
198 if status.starts_with("Open") {
199 open.entry(severity.to_string()).or_default().push(id.to_string());
200 } else if !closed {
201 errors.push(format!("{BYPASSES}: {id}: status {status:?} is neither Openโฆ nor Closed (<commit>)"));
202 }
203 match table {
204 Some("Open") if closed => errors.push(format!("{BYPASSES}: {id} is Closed but left in Open")),
205 Some("Closed") if status.starts_with("Open") => {
206 errors.push(format!("{BYPASSES}: {id} is Open but sits in Closed"))
207 }
208 _ => {}
209 }
210 }
211 if tables != TABLES {
212 errors.push(format!("{BYPASSES}: tables {tables:?}; want Open, Closed, Accepted, each once, in that order"));
213 }
214 (open, errors)
215}
216
217pub fn run(write_baseline: bool, gate: bool) -> Report {
218 let mut errors: Vec<String> = vec![];
219 let doc: T = match std::fs::read_to_string(RATIO)
220 .map_err(|e| e.to_string())
221 .and_then(|t| toml::from_str(&t).map_err(|e| e.to_string()))
222 {
223 Ok(d) => d,
224 Err(e) => return Report { text: format!("{RATIO}: {e}"), ok: false },
225 };
226 let assumptions = std::fs::read_to_string(ASSUMPTIONS).unwrap_or_default();
227 let ids: Vec<String> = ["V", "P", "G", "U"]
228 .iter()
229 .flat_map(|p| (1..=40).map(move |n| format!("{p}{n}")))
230 .filter(|id| assumptions.contains(&format!("**{id}**")))
231 .collect();
232 let internal = ids.iter().filter(|i| i.starts_with('U')).count();
233 let trust_ids = ids.len() - internal;
234 let trust_count = trust_ids + trust_sites();
235
236 errors.extend(structure_errors(&doc));
237 let mut used: BTreeMap<(String, Discharge), std::collections::BTreeSet<String>> = BTreeMap::new();
238 let mut lines = vec![];
239 let mut per: BTreeMap<String, (usize, usize)> = BTreeMap::new();
240 let (mut d_sum, mut a_sum) = (0usize, 0usize);
241 for inv in doc.get("invariant").and_then(T::as_array).into_iter().flatten() {
242 let id = inv.get("id").and_then(T::as_str).unwrap_or("?").to_string();
243 let names: Vec<&str> =
244 inv.get("discharged").and_then(T::as_array).into_iter().flatten().filter_map(T::as_str).collect();
245 let assumes: Vec<&str> =
246 inv.get("assumes").and_then(T::as_array).into_iter().flatten().filter_map(T::as_str).collect();
247 for n in &names {
248 match n.split_once("::").map(|(k, f)| (k, discharge(k, f))) {
249 Some((k, Some(d))) => {
250 used.entry((k.to_string(), d)).or_default().insert(n.to_string());
251 }
252 _ => errors.push(format!(
253 "{id}: {n} is not a proof fn, a verified fn with ensures, or a Kani harness in the source"
254 )),
255 }
256 }
257 for a in &assumes {
258 if !ids.iter().any(|x| x == a) {
259 errors.push(format!("{id}: assumption {a} is not in {ASSUMPTIONS}"));
260 }
261 }
262 let (d, a) = (names.len(), assumes.len());
263 d_sum += d;
264 a_sum += a;
265 per.insert(id.clone(), (d, a));
266 let phase = inv.get("phase").and_then(T::as_str).unwrap_or("");
267 lines.push(format!(
268 " {id:<4} rests on {d} discharged and {a} assumed ({}){}",
269 if a == 0 { "-".into() } else { format!("{:.2}", d as f64 / a as f64) },
270 if d == 0 { format!("; {phase}: tested, not proved") } else { String::new() }
271 ));
272 }
273
274 let verifier = doc.get("verifier").and_then(T::as_table);
279 if verifier.is_none() {
280 errors.push("no [verifier] counts: obligations are not tied to the verifier's output".into());
281 }
282 for ((k, d), names) in &used {
283 if *d != Discharge::Verus {
284 continue;
285 }
286 let verified = verifier.and_then(|v| v.get(k.as_str())).and_then(T::as_integer).unwrap_or(0) as usize;
287 if names.len() > verified {
288 errors.push(format!(
289 "{k}: {} distinct Verus obligations credited, but its Verus run verifies {verified}",
290 names.len()
291 ));
292 }
293 }
294
295 let english: Vec<String> = crate::catalog::read_text(Path::new(CATALOG))
298 .ok()
299 .and_then(|t| toml::from_str::<T>(&t).ok())
300 .and_then(|d| d.get("fault").and_then(T::as_array).cloned())
301 .unwrap_or_default()
302 .iter()
303 .filter(|e| {
304 e.get("tags").and_then(T::as_array).is_some_and(|t| t.iter().any(|x| x.as_str() == Some("english")))
305 })
306 .flat_map(|e| e.get("spec").and_then(T::as_array).cloned().unwrap_or_default())
307 .filter_map(|x| x.as_str().map(String::from))
308 .collect();
309 let unwritten: Vec<&String> =
310 per.iter().filter(|(id, (d, _))| *d > 0 && !english.contains(id)).map(|(id, _)| id).collect();
311 if !unwritten.is_empty() {
312 errors.push(format!(
313 "proved without an entry written from the English (ยง17a): {}",
314 unwritten.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
315 ));
316 }
317
318 let base = doc.get("baseline");
321 let b = |k: &str| base.and_then(|b| b.get(k)).and_then(T::as_integer).unwrap_or(0) as usize;
322 if !write_baseline && base.is_some() {
323 if trust_count > b("trust_count") {
324 errors.push(format!("the trust count grew: {trust_count} > {}", b("trust_count")));
325 }
326 if internal > b("internal_unproven") {
327 errors.push(format!("the internal-unproven count grew: {internal} > {}", b("internal_unproven")));
328 }
329 let (bd, ba) = (b("discharged"), b("assumed"));
330 if ba > 0 && a_sum > 0 && (d_sum as f64 / a_sum as f64) < (bd as f64 / ba as f64) - 1e-9 {
331 errors.push(format!("the global ratio fell: {d_sum}:{a_sum} < {bd}:{ba}"));
332 }
333 if let Some(bp) = base.and_then(|b| b.get("per_invariant")).and_then(T::as_table) {
334 for (id, (d, a)) in &per {
335 let Some([pd, pa]) = bp.get(id).and_then(T::as_array).map(|v| {
336 [v.first().and_then(T::as_integer).unwrap_or(0), v.get(1).and_then(T::as_integer).unwrap_or(0)]
337 }) else {
338 continue;
339 };
340 let (pd, pa) = (pd as f64, pa as f64);
341 if pa > 0.0 && *a > 0 && (*d as f64 / *a as f64) < pd / pa - 1e-9 {
342 errors.push(format!("{id}'s ratio fell: {d}:{a} < {pd}:{pa}"));
343 }
344 }
345 }
346 }
347
348 let (open, format_errors) = bypasses();
349 errors.extend(format_errors);
350 let high = open.get("High").cloned().unwrap_or_default();
351 if gate && !high.is_empty() {
352 errors.push(format!("open High bypasses block every gate (ยง17b): {}", high.join(", ")));
353 }
354 let mut text = format!(
355 "verification ratio (SPEC ยง17a)\n trust count: {trust_count} ({} platform and proof-validity entries + {} external_body/assume sites)\n internal-unproven count: {internal}\n global d:a: {d_sum}:{a_sum} ({:.2})\n",
356 trust_ids,
357 trust_count - trust_ids,
358 if a_sum == 0 { 0.0 } else { d_sum as f64 / a_sum as f64 }
359 );
360 text.push_str(&lines.join("\n"));
361 text.push_str("\nbypass register (SPEC ยง17b)\n");
362 for sev in ["High", "Medium", "Low"] {
363 let ids = open.get(sev).cloned().unwrap_or_default();
364 text.push_str(&format!(" open {sev}: {} {}\n", ids.len(), ids.join(" ")));
365 }
366 if write_baseline {
367 let per_s: Vec<String> = per.iter().map(|(k, (d, a))| format!("{k} = [{d}, {a}]")).collect();
368 text.push_str(&format!(
369 "\n[baseline]\ntrust_count = {trust_count}\ninternal_unproven = {internal}\ndischarged = {d_sum}\nassumed = {a_sum}\n\n[baseline.per_invariant]\n{}\n",
370 per_s.join("\n")
371 ));
372 }
373 for e in &errors {
374 text.push_str(&format!(" ERROR: {e}\n"));
375 }
376 Report { text, ok: errors.is_empty() }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
387 fn the_accounting_checks_bite() {
388 let doc = |t: &str| toml::from_str::<T>(t).unwrap();
389 let ok = doc("[[invariant]]\nid = \"I1\"\ndischarged = [\"txn::a\"]\n[baseline.per_invariant]\nI1 = [1, 0]\n");
390 assert!(structure_errors(&ok).is_empty());
391 let removed = doc("[[invariant]]\nid = \"I2\"\n[baseline.per_invariant]\nI1 = [1, 0]\n");
392 assert!(!structure_errors(&removed).is_empty(), "a removed invariant");
393 let dup = doc("[[invariant]]\nid = \"I1\"\ndischarged = [\"txn::a\", \"txn::a\"]\n");
394 assert!(!structure_errors(&dup).is_empty(), "a duplicated obligation");
395 let twice = doc("[[invariant]]\nid = \"I1\"\n[[invariant]]\nid = \"I1\"\n");
396 assert!(!structure_errors(&twice).is_empty(), "an invariant twice");
397
398 let src = vec![
399 "verus! {\n pub proof fn lemma_a(x: u8)\n ensures x == x,\n {}\n pub fn decide(x: u8) -> (r: u8)\n ensures r == x,\n { x }\n fn helper(x: u8) -> u8 { x }\n}\n#[kani::proof]\nfn harness_b() {}\n#[test]\nfn lemma_c() {}\n// fn lemma_d()\n".to_string(),
400 ];
401 assert_eq!(discharge_in(&src, "lemma_a"), Some(Discharge::Verus));
402 assert_eq!(discharge_in(&src, "decide"), Some(Discharge::Verus), "verified against its ensures");
403 assert_eq!(discharge_in(&src, "harness_b"), Some(Discharge::Kani));
404 assert_eq!(discharge_in(&src, "helper"), None, "a plain function");
405 assert_eq!(discharge_in(&src, "lemma_c"), None, "a test");
406 assert_eq!(discharge_in(&src, "lemma_d"), None, "a comment");
407 assert_eq!(discharge_in(&src, "lemma_renamed"), None, "a renamed proof function");
408 }
409
410 #[test]
411 fn the_register_parses_and_its_checks_bite() {
412 if let Ok(shipped) = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/bypasses.md")) {
415 let (open, errors) = parse_bypasses(&shipped);
416 assert!(errors.is_empty(), "{errors:?}");
417 assert!(open.values().flatten().count() > 0);
418 }
419 let row = |id: &str, sev: &str, status: &str| format!("| {id} | r | b | {sev} | {status} | m | A | I1, I2 |\n");
420 let x = "| X-1 | r | b | n | E | all |\n";
421 let reg = |o: &str, c: &str, a: &str| format!("## Open\n{o}\n## Closed\n{c}\n## Accepted\n{a}");
423 let bad = [
424 reg(&(row("B-1", "High", "Open") + &row("B-1", "Low", "Open")), "", x),
425 reg(&row("B-2", "Severe", "Open"), "", x),
426 reg(&row("B-3", "Low", "Done"), "", x),
427 reg("| B-4 | r | b | Low | Open |\n", "", x),
428 reg("", "", "| X-1 | r | b | n |\n"),
429 reg("| B-7 | r | b | Low | Open | m | A | |\n", "", x),
430 reg("| B-8 | r | b | Low | Open | m | A | everything |\n", "", x),
431 reg(&row("B-9", "Low", "Closed (abc123)"), "", x),
434 reg("", &row("B-9", "Low", "Open (v4.6)"), x),
435 reg(&row("B-9", "Low", "Open"), "", &(row("B-10", "Low", "Closed (abc123)") + x)),
436 reg(x, "", ""),
437 row("B-9", "Low", "Open") + ®("", "", x),
438 format!("## Open\n## Accepted\n{x}"),
439 format!("## Closed\n## Open\n## Accepted\n{x}"),
440 reg("", "", x) + "## Open\n",
441 reg("", "", x) + "## Withdrawn\n",
442 ];
443 for text in bad {
444 assert!(!parse_bypasses(&text).1.is_empty(), "accepted: {text}");
445 }
446 let (open, errors) =
447 parse_bypasses(®(&row("B-5", "High", "Open (v4.5)"), &row("B-6", "Low", "Closed (abc123)"), x));
448 assert!(errors.is_empty(), "{errors:?}");
449 assert_eq!(open.get("High").map(Vec::len), Some(1));
450 assert_eq!(open.get("Low"), None);
451 }
452}