1use std::fmt::Write as _;
25
26use rucc_rules::Rule;
27
28use crate::verify::{Report, Verdict};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Unverified {
33 pub file: String,
35 pub pattern: String,
37 pub guard: Option<String>,
39 pub widths: Vec<u32>,
41 pub why: String,
43}
44
45#[must_use]
47pub fn listed(file: &str, rules: &[Rule], report: &Report) -> Vec<Unverified> {
48 let mut out = Vec::new();
49 for (rule, verdict) in rules.iter().zip(&report.verdicts) {
50 let Verdict::Bounded { widths, why } = verdict else { continue };
51 out.push(Unverified {
52 file: file.to_owned(),
53 pattern: rule.pattern.to_string(),
54 guard: rule.guard.as_ref().map(ToString::to_string),
55 widths: widths.clone(),
56 why: why.clone(),
57 });
58 }
59 out
60}
61
62#[must_use]
69pub fn render(entries: &[Unverified]) -> String {
70 let mut out = String::from("# Unverified rules\n\n");
71 out.push_str(
72 "Generated by `cargo run -q -p rucc-verify -- crates/rucc-codegen/rules crates/rucc-opt/rules --report docs/UNVERIFIED.md`. Do not edit this file, edit the rules.\n\n",
73 );
74 out.push_str(
75 "Every rule in the rule set carries a bitvector claim and `rucc-verify` discharges it before the rule is allowed in. Almost all of them are settled at the width the compiler runs them at, and there is nothing to say about those. This file is the rest: a rule no solver settles at its own width, proved instead at four and eight bits because somebody wrote down a reason for taking that as enough. `spec/optimizer/41-correctness.md` section 41.8 asks for that set to be a list rather than a number in a build log, because the list is the thing a reviewer can argue with.\n\n",
76 );
77 out.push_str(
78 "A rule the solver refutes is not here, and neither is a rule it gives up on that carries no reason. Neither of those enters the rule set at all, so there is no list for them to be on.\n\n",
79 );
80 out.push_str(&format!("{}\n\n", counted(entries.len())));
81
82 let mut file = "";
83 for entry in entries {
84 if entry.file != file {
85 file = &entry.file;
86 let _ = writeln!(out, "## `{file}`\n");
87 }
88 out.push_str(&bullet(entry));
89 }
90 out
91}
92
93fn counted(count: usize) -> String {
95 match count {
96 0 => "No rule is on this list, which is the state to keep it in.".to_owned(),
97 1 => "One rule is on this list.".to_owned(),
98 many => format!("{many} rules are on this list."),
99 }
100}
101
102fn bullet(entry: &Unverified) -> String {
104 let mut out = format!("- `{}`", entry.pattern);
105 if let Some(guard) = &entry.guard {
106 let _ = write!(out, " when `{guard}`");
107 }
108 let _ = writeln!(out, ", proved at {}: {}", widths(&entry.widths), entry.why);
109 out
110}
111
112fn widths(over: &[u32]) -> String {
114 let mut out = String::new();
115 for (at, width) in over.iter().enumerate() {
116 if at > 0 {
117 out.push_str(if at + 1 == over.len() { " and " } else { ", " });
118 }
119 let _ = write!(out, "{width}");
120 }
121 out.push_str(" bits");
122 out
123}
124
125#[must_use]
133pub fn difference(found: &str, wanted: &str) -> (Vec<String>, Vec<String>) {
134 let entries = |text: &str| -> Vec<String> {
135 text.lines().filter(|line| line.starts_with("- `")).map(ToOwned::to_owned).collect()
136 };
137 let (before, after) = (entries(found), entries(wanted));
138 let added = after.iter().filter(|line| !before.contains(line)).cloned().collect();
139 let removed = before.iter().filter(|line| !after.contains(line)).cloned().collect();
140 (added, removed)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn entry(file: &str, pattern: &str) -> Unverified {
148 Unverified {
149 file: file.to_owned(),
150 pattern: pattern.to_owned(),
151 guard: None,
152 widths: vec![4, 8],
153 why: "the solver does not settle a multiply of two unknowns at this width".to_owned(),
154 }
155 }
156
157 #[test]
158 fn an_empty_list_says_so_rather_than_trailing_off() {
159 let text = render(&[]);
160 assert!(text.contains("No rule is on this list"), "{text}");
161 assert!(!text.contains("- `"), "{text}");
162 assert!(!text.contains("## `"), "{text}");
163 }
164
165 #[test]
166 fn an_entry_carries_the_rule_the_widths_and_the_reason() {
167 let text = render(&[entry("rules/x86-64.rules", "(mul.i64 (value.i64 x) (iconst.i64 k))")]);
168 assert!(text.contains("One rule is on this list."), "{text}");
169 assert!(text.contains("## `rules/x86-64.rules`"), "{text}");
170 assert!(
171 text.contains(
172 "- `(mul.i64 (value.i64 x) (iconst.i64 k))`, proved at 4 and 8 bits: the solver"
173 ),
174 "{text}"
175 );
176 }
177
178 #[test]
179 fn a_guard_is_part_of_what_names_the_rule() {
180 let mut one = entry("rules/x86-64.rules", "(mul.i64 (value.i64 x) (iconst.i64 k))");
181 one.guard = Some("(= k 1)".to_owned());
182 let text = render(&[one]);
183 assert!(text.contains("(iconst.i64 k))` when `(= k 1)`, proved at"), "{text}");
184 }
185
186 #[test]
187 fn each_file_gets_one_heading_and_the_rules_under_it() {
188 let text = render(&[
189 entry("rules/a.rules", "(mul.i8 x y)"),
190 entry("rules/a.rules", "(mul.i16 x y)"),
191 entry("rules/b.rules", "(mul.i32 x y)"),
192 ]);
193 assert_eq!(text.matches("## `rules/a.rules`").count(), 1, "{text}");
194 assert_eq!(text.matches("## `rules/b.rules`").count(), 1, "{text}");
195 assert!(text.contains("3 rules are on this list."), "{text}");
196 }
197
198 #[test]
199 fn one_width_is_not_said_as_a_pair() {
200 assert_eq!(widths(&[4]), "4 bits");
201 assert_eq!(widths(&[4, 8]), "4 and 8 bits");
202 assert_eq!(widths(&[4, 8, 16]), "4, 8 and 16 bits");
203 }
204
205 #[test]
206 fn the_difference_is_the_rules_that_moved_and_not_the_prose_around_them() {
207 let before = render(&[entry("rules/a.rules", "(mul.i8 x y)")]);
208 let after = render(&[entry("rules/a.rules", "(mul.i16 x y)")]);
209 let (added, removed) = difference(&before, &after);
210 assert_eq!(added.len(), 1, "{added:?}");
211 assert_eq!(removed.len(), 1, "{removed:?}");
212 assert!(added[0].contains("(mul.i16 x y)"), "{added:?}");
213 assert!(removed[0].contains("(mul.i8 x y)"), "{removed:?}");
214
215 let (added, removed) = difference(&before, &before);
217 assert!(added.is_empty() && removed.is_empty(), "{added:?} {removed:?}");
218 }
219}