rucc_codegen/coverage.rs
1//! Which IR opcodes have somewhere to go, and which do not.
2//!
3//! Design: `spec/10-backend.md` section 10.2, under **Coverage**.
4//!
5//! Every opcode has to be lowered by something or be a hole somebody wrote down. Without this the
6//! way a hole is found is that somebody compiles a program containing one and the selector reports
7//! that it cannot lower an instruction, which is a fine diagnostic and a bad discovery mechanism:
8//! it turns a gap in the rule set into a user's problem rather than a failing build.
9//!
10//! # The three answers
11//!
12//! An opcode is lowered by a rule, or somewhere a rule cannot reach, or nowhere.
13//!
14//! The first is the ordinary answer and the one this can check by itself. [`crate::term`] says
15//! every name a rule could be written at, the table says every name one is written at, and an
16//! opcode is covered when each of its names is in both. That is what makes this a check about
17//! widths rather than about opcodes: an `add` with a rule at four widths and no rule at the fifth
18//! is not covered, and would be reported here as the missing name rather than as a covered opcode.
19//!
20//! The second is [`ELSEWHERE`], which is not a gap. `spec/10-backend.md` names five of them and
21//! there are more now, and they are all the same kind of thing: an opcode whose lowering depends on
22//! something no pattern can see. Where a call's arguments go depends on the signature, where a
23//! local lives depends on the frame, an unconditional jump is an edge and edges live on the block,
24//! and a `memcpy` is a run of moves whose length is a constant the pattern would have to count. A
25//! rule matches one term and can say none of that.
26//!
27//! The third is [`GAPS`], which is the number `spec/15-testing.md` section 15.8 says we keep. Each
28//! entry names why it is there and the issue that closes it, so that an opcode nobody has written a
29//! rule for is a decision somebody wrote down rather than a surprise.
30//!
31//! # What makes the lists honest
32//!
33//! An entry that stops being true fails. An opcode on either list that a rule starts covering is a
34//! stale entry and the tests below say so by name, which is the same rule the exclusion lists in
35//! the compatibility harness are kept under: a list nothing checks is a list that only grows.
36//!
37//! The direction this cannot check is an opcode moving from [`GAPS`] to [`ELSEWHERE`] without the
38//! list following it, because where an opcode is lowered by name is a `match` arm and there is
39//! nothing to ask about a `match` arm from here. What that costs is one line of a list going out of
40//! date; what it does not cost is a gap going unnoticed, since the opcode is still on a list and
41//! still counted.
42//!
43//! # The other question
44//!
45//! All of the above is about the rule set as it is written. [`Fired`] is about the rule set as it
46//! is used: which rules a compilation actually reached. A rule nothing reaches is proved and dead
47//! weight, or it is a construct the corpus does not contain and somebody should know which. The
48//! selector marks a rule as it fires it, the driver writes the marks out under
49//! `-Zrule-coverage=FILE`, and the harness in `tamnd/rucc-compat` unions those files over a corpus,
50//! which is what turns coverage of the rule set into a number. `spec/20-execution-testing.md`
51//! section 20.9 is the design and `tamnd/rucc#261` is the work.
52
53use core::fmt;
54use core::fmt::Write as _;
55
56use rucc_ir::Opcode;
57use rucc_target::Arch;
58
59use crate::select::{Table, Test};
60use crate::term;
61
62/// An opcode no rule is written about, and the place that lowers it instead.
63///
64/// Not one of these is a gap. Each is an opcode whose lowering depends on something a pattern
65/// cannot see, so the answer lives where that something is known.
66pub static ELSEWHERE: &[(Opcode, &str)] = &[
67 // The convention. What a call's operands are is whatever the signature made them, and which
68 // register each one arrives in depends on the classification of every argument before it.
69 (Opcode::Call, "`crate::abi`, which builds a call out of the convention"),
70 (Opcode::CallIndirect, "`crate::abi`, the same instruction with the callee in a register"),
71 // The frame, which is not known until the allocator has finished running out of registers.
72 (Opcode::Alloca, "`crate::lower`, as an address into a frame `crate::frame` lays out later"),
73 // A relocation, which is right because of what the linker does rather than because of what
74 // any bitvector equals.
75 (Opcode::GlobalAddr, "`crate::lower`, a `lea` off the instruction pointer with a name on it"),
76 // No instruction at all. The IR keeps the width the same and the machine has one register
77 // file for both, so the value is already where it needs to be.
78 (Opcode::PtrToInt, "`crate::lower`, which renames the value rather than computing anything"),
79 (Opcode::IntToPtr, "`crate::lower`, the same rename the other way round"),
80 // The edges and the two ways of writing down that control does not arrive.
81 (Opcode::Jump, "`crate::layout`, since an edge is on the block and not in the block"),
82 (Opcode::Unreachable, "nothing at all, which is the answer for a place control does not reach"),
83 (Opcode::UnreachableHint, "nothing at all, for the same reason"),
84 // Rewritten into the opcodes above before selection ever sees them.
85 (Opcode::Switch, "`crate::expand`, into the compare and branch chain it is"),
86 (Opcode::FConst, "`crate::expand`, into a constant in memory and a load of it"),
87 (Opcode::FNeg, "`crate::expand`, into the sign bit flip it is"),
88 (Opcode::UIToFP, "`crate::expand`, into a widening and a signed conversion"),
89 (Opcode::FPToUI, "`crate::expand`, into a signed conversion and a narrowing"),
90 (Opcode::Memcpy, "`crate::expand`, into the moves it stands for"),
91 (Opcode::Memset, "`crate::expand`, into the fills it stands for"),
92 (Opcode::Memmove, "`crate::expand`, into a call, since the two regions may overlap"),
93 // The variable argument list, which is four opcodes reading a structure the ABI describes.
94 (Opcode::VaStart, "`crate::varargs`, which writes the register save area the ABI describes"),
95 (Opcode::VaArg, "`crate::varargs`, into the walk over that structure"),
96 (Opcode::VaObject, "`crate::varargs`, the same walk for something that arrived in memory"),
97 (Opcode::VaCopy, "`crate::varargs`, into a copy of the structure"),
98 (Opcode::VaEnd, "`crate::varargs`, which removes it, since there is nothing to undo"),
99];
100
101/// An opcode nothing lowers, why it is here, and the issue that closes it.
102///
103/// This is the count `spec/15-testing.md` section 15.8 asks for. It is not zero yet and the
104/// spec says it should be, which is the honest reading of where the back end is: every one of
105/// these is a feature nobody has written, and all but three of them are opcodes the front end
106/// cannot produce either, so a program that reaches one of these is a program that reaches an
107/// unimplemented builtin first.
108pub static GAPS: &[(Opcode, &str, &str)] = &[
109 (Opcode::Splat, "a vector, and no rule is written about a lane count", "tamnd/rucc#200"),
110 (
111 Opcode::TargetIntrinsic,
112 "the same, since what needs one is a vector builtin",
113 "tamnd/rucc#200",
114 ),
115 (Opcode::BlockAddr, "the address of a label", "tamnd/rucc#353"),
116 (Opcode::IndirectBr, "the branch a computed goto turns into", "tamnd/rucc#353"),
117 (
118 Opcode::FRem,
119 "a call to `fmod`, so a link line question as much as a lowering one",
120 "tamnd/rucc#226",
121 ),
122 (
123 Opcode::Fma,
124 "a call or one instruction, depending on what the machine is told it has",
125 "tamnd/rucc#226",
126 ),
127 (Opcode::AtomicLoad, "an ordering, which the IR cannot say yet", "tamnd/rucc#311"),
128 (Opcode::AtomicStore, "the same", "tamnd/rucc#311"),
129 (Opcode::AtomicRmw, "the same, and a `lock` prefix per operation", "tamnd/rucc#311"),
130 (Opcode::Cmpxchg, "the same, and a result that is a pair", "tamnd/rucc#311"),
131 (
132 Opcode::Fence,
133 "the same, and nothing at all on this machine for most orderings",
134 "tamnd/rucc#311",
135 ),
136 (
137 Opcode::Ctlz,
138 "one instruction on a machine that has it and several on one that does not",
139 "tamnd/rucc#310",
140 ),
141 (Opcode::Cttz, "the same", "tamnd/rucc#310"),
142 (Opcode::Ctpop, "the same", "tamnd/rucc#310"),
143 (Opcode::Bswap, "three instructions and no rule", "tamnd/rucc#307"),
144 (Opcode::Bitreverse, "a node nothing writes and nothing lowers", "tamnd/rucc#363"),
145 (
146 Opcode::SAddOverflow,
147 "a result and a flag together, which no rule can write",
148 "tamnd/rucc#309",
149 ),
150 (Opcode::UAddOverflow, "the same", "tamnd/rucc#309"),
151 (Opcode::SSubOverflow, "the same", "tamnd/rucc#309"),
152 (Opcode::USubOverflow, "the same", "tamnd/rucc#309"),
153 (Opcode::SMulOverflow, "the same", "tamnd/rucc#309"),
154 (Opcode::UMulOverflow, "the same", "tamnd/rucc#309"),
155 (Opcode::Expect, "a branch weight nothing reads yet", "tamnd/rucc#364"),
156 (Opcode::Prefetch, "one instruction, once the hints have somewhere to go", "tamnd/rucc#313"),
157 (Opcode::FrameAddress, "a walk up the frame pointers", "tamnd/rucc#312"),
158 (Opcode::ReturnAddress, "the same walk, one word further along", "tamnd/rucc#312"),
159 (
160 Opcode::StackSave,
161 "a frame that can grow, as a variable length array needs",
162 "tamnd/rucc#291",
163 ),
164 (Opcode::StackRestore, "the same", "tamnd/rucc#291"),
165 (
166 Opcode::SetjmpMarker,
167 "a call that returns twice, which the allocator has to be told about",
168 "tamnd/rucc#223",
169 ),
170 (Opcode::LongjmpMarker, "the same", "tamnd/rucc#223"),
171 (Opcode::TailCall, "a terminator nothing writes and nothing lowers", "tamnd/rucc#365"),
172 (
173 Opcode::InlineAsm,
174 "a template, its constraints, and sixty eight torture programs",
175 "tamnd/rucc#349",
176 ),
177];
178
179/// A width no rule is written at, why, and the issue that closes it.
180///
181/// The other half of coverage, and the half an opcode list cannot say. An opcode is covered when
182/// every name it has is a name a rule is written at, and a width with no name has no names to
183/// check: an `add` of two `__int128`s is not a missing rule for `add`, it is a width the rule
184/// language cannot spell. So the widths are written down here for the same reason the opcodes are
185/// written down above.
186pub static WIDTHS: &[(&str, &str, &str)] = &[
187 (
188 "one bit",
189 "everything but and, or, xor, a constant, and the widening out of one",
190 "tamnd/rucc#352",
191 ),
192 (
193 "a hundred and twenty eight bits",
194 "no register pair, so nothing at that width has a name",
195 "tamnd/rucc#351",
196 ),
197 (
198 "eighty bits",
199 "a long double is on the x87 stack and no rule is about that stack",
200 "tamnd/rucc#326",
201 ),
202 (
203 "a vector of any lane count",
204 "a rule at a width says nothing about how many lanes",
205 "tamnd/rucc#200",
206 ),
207];
208
209/// What a target's rules cover, and what they do not.
210#[derive(Debug)]
211pub struct Report {
212 /// The rule file this is about, so that anything said about it names a file to open.
213 pub source: &'static str,
214 /// How many opcodes the IR has.
215 pub opcodes: usize,
216 /// The opcodes every name of which a rule is written at.
217 pub by_rule: Vec<Opcode>,
218 /// How many names those are, which is one per opcode and width.
219 pub names: usize,
220 /// A name a rule could be written at and none is, which is what a missing rule looks like.
221 pub uncovered: Vec<(Opcode, &'static str)>,
222 /// A name a rule is written at that nothing can ever be called, which is a dead rule.
223 pub unreachable: Vec<&'static str>,
224 /// The opcodes lowered somewhere a rule cannot reach.
225 pub elsewhere: Vec<Opcode>,
226 /// The opcodes nothing lowers.
227 pub gaps: Vec<Opcode>,
228 /// The opcodes on none of the three lists, which is what a new opcode is until somebody says
229 /// where it goes.
230 pub unaccounted: Vec<Opcode>,
231}
232
233impl fmt::Display for Report {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(
236 f,
237 "rucc-codegen: {} lowers {} of the {} IR opcodes by rule at {} names, {} are lowered \
238 where no rule reaches and {} have no lowering yet",
239 self.source,
240 self.by_rule.len(),
241 self.opcodes,
242 self.names,
243 self.elsewhere.len(),
244 self.gaps.len()
245 )
246 }
247}
248
249/// What a table covers.
250///
251/// Nothing is executed and nothing is compiled. The rule set and the naming of instructions are
252/// both data, and the answer is a comparison of two lists.
253#[must_use]
254pub fn report(table: &Table) -> Report {
255 let named = term::heads();
256 let patterns = pattern_heads(table);
257
258 let mut by_rule = Vec::new();
259 let mut uncovered = Vec::new();
260 for &(opcode, name) in &named {
261 if patterns.contains(&name) {
262 by_rule.push(opcode);
263 } else {
264 uncovered.push((opcode, name));
265 }
266 }
267 // An opcode is covered when every name it has is covered, so one missing width takes the
268 // whole opcode off the list however many of its other widths are there.
269 for &(opcode, _) in &uncovered {
270 by_rule.retain(|&covered| covered != opcode);
271 }
272 by_rule.sort_unstable();
273 by_rule.dedup();
274
275 let names = named.len() - uncovered.len();
276 let unreachable: Vec<&'static str> = patterns
277 .iter()
278 .filter(|head| !named.iter().any(|(_, name)| name == *head))
279 .copied()
280 .collect();
281
282 let elsewhere: Vec<Opcode> = ELSEWHERE.iter().map(|&(opcode, _)| opcode).collect();
283 let gaps: Vec<Opcode> = GAPS.iter().map(|&(opcode, ..)| opcode).collect();
284 let unaccounted: Vec<Opcode> = Opcode::all()
285 .filter(|opcode| {
286 !by_rule.contains(opcode) && !elsewhere.contains(opcode) && !gaps.contains(opcode)
287 })
288 .collect();
289
290 Report {
291 source: table.source,
292 opcodes: Opcode::all().count(),
293 by_rule,
294 names,
295 uncovered,
296 unreachable,
297 elsewhere,
298 gaps,
299 unaccounted,
300 }
301}
302
303/// Every name a rule in a table is written about, which is the first test the trie makes.
304///
305/// Node zero is the root of the trie over the patterns and the first thing any walk asks is what
306/// the term in hand is called, so its tests are exactly the set of pattern heads. There is no
307/// wildcard there to worry about: a rule matching any term at all is one nobody has written and
308/// one that would be an error to write, since a lowering has to know what it is lowering.
309fn pattern_heads(table: &Table) -> Vec<&'static str> {
310 let Some(root) = table.nodes.first() else { return Vec::new() };
311 let mut found: Vec<&'static str> = root
312 .tests
313 .iter()
314 .filter_map(|(test, _)| match test {
315 Test::App { head, .. } => Some(*head),
316 Test::Int(_) => None,
317 })
318 .collect();
319 found.sort_unstable();
320 found.dedup();
321 found
322}
323
324/// The rules a target lowers by, or `None` where no back end in this crate covers it.
325///
326/// The same question [`crate::pipeline::Machine::for_target`] answers about the rest of a machine,
327/// and it is here as well because a caller that wants to write down what a run covered has a
328/// target and no machine. An architecture that gets a rule file at M6 gets an arm here at the same
329/// time, and until then it has no rules to report coverage of rather than an empty set of them.
330#[must_use]
331pub fn table(arch: Arch) -> Option<&'static Table> {
332 match arch {
333 Arch::X86_64 => Some(&crate::select::x86_64::TABLE),
334 Arch::Aarch64 | Arch::Riscv64 => None,
335 }
336}
337
338/// Which rules fired, over one function or over a whole compilation.
339///
340/// A bit per rule and nothing else. This is on the path of every instruction selected, so what it
341/// costs is paid by every compilation whether or not anybody asked for the number, and the cheapest
342/// thing that answers the question is a flag per rule set once.
343///
344/// The index of a rule is how this is kept and not how it is written down. An index moves the
345/// moment a rule is added above it, so [`Fired::listing`] names the rule file and the line instead:
346/// a line is a place somebody can open, and a report written by one build can still be read against
347/// a rule file that has grown since.
348#[derive(Debug, Clone, Default, PartialEq, Eq)]
349pub struct Fired {
350 /// One entry per rule, true once that rule has fired. It grows to fit the highest index
351 /// marked rather than being sized from a table, so nothing here has to be told which target
352 /// is being compiled for.
353 seen: Vec<bool>,
354}
355
356impl Fired {
357 /// Nothing has fired yet.
358 #[must_use]
359 pub const fn new() -> Fired {
360 Fired { seen: Vec::new() }
361 }
362
363 /// Records that the rule at this index fired.
364 pub fn mark(&mut self, rule: usize) {
365 if self.seen.len() <= rule {
366 self.seen.resize(rule + 1, false);
367 }
368 self.seen[rule] = true;
369 }
370
371 /// Whether the rule at this index fired.
372 #[must_use]
373 pub fn has(&self, rule: usize) -> bool {
374 self.seen.get(rule).copied().unwrap_or(false)
375 }
376
377 /// How many rules fired.
378 #[must_use]
379 pub fn count(&self) -> usize {
380 self.seen.iter().filter(|fired| **fired).count()
381 }
382
383 /// Takes in everything another one recorded.
384 ///
385 /// One compilation is many functions and one command line is many files, and the question is
386 /// about all of them together. Merging rather than writing a file per function is also what
387 /// keeps the answer the same however the work was scheduled.
388 pub fn merge(&mut self, other: &Fired) {
389 if self.seen.len() < other.seen.len() {
390 self.seen.resize(other.seen.len(), false);
391 }
392 for (mine, theirs) in self.seen.iter_mut().zip(&other.seen) {
393 *mine |= *theirs;
394 }
395 }
396
397 /// What `-Zrule-coverage=FILE` writes.
398 ///
399 /// One line per rule in the table, in the order the rule file writes them, each saying whether
400 /// the rule fired and naming the file and line it is written at. Every rule is listed rather
401 /// than only the ones that fired, so that one of these files says what the whole rule set was
402 /// as well as what this compilation reached: a reader unioning them over a corpus needs both
403 /// and would otherwise have to parse the rule file to get the second.
404 ///
405 /// The first line is a comment holding the count, which is the number a person wants and the
406 /// one thing here that is not worth making them add up.
407 #[must_use]
408 pub fn listing(&self, table: &Table) -> String {
409 let fired = table.rules.iter().enumerate().filter(|(index, _)| self.has(*index)).count();
410 let mut out = format!(
411 "# rucc rule coverage: {fired} of {} rules in {} fired\n",
412 table.rules.len(),
413 table.source
414 );
415 for (index, rule) in table.rules.iter().enumerate() {
416 let word = if self.has(index) { "fired" } else { "unused" };
417 let _ = writeln!(out, "{word} {}:{} {}", table.source, rule.line, rule.pattern);
418 }
419 out
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::select::x86_64::TABLE;
427
428 /// The claim the whole module is for, in the direction that matters: a name an instruction
429 /// can be called by is a name a rule is written at. This is the width check as much as the
430 /// opcode check, since a name is an opcode and a width together.
431 #[test]
432 fn every_name_an_instruction_can_have_is_one_a_rule_is_written_at() {
433 let report = report(&TABLE);
434 assert!(
435 report.uncovered.is_empty(),
436 "nothing in {} lowers these, and each is an opcode at a width the rule language can \
437 spell: {:?}",
438 report.source,
439 report.uncovered
440 );
441 }
442
443 /// And the other direction, which costs nothing to ask and finds a rule that can never fire.
444 /// A pattern head no instruction is ever called by is a rule written against a name that was
445 /// renamed or misspelled, and it would sit there proved and unreachable.
446 #[test]
447 fn every_name_a_rule_is_written_at_is_one_an_instruction_can_have() {
448 let report = report(&TABLE);
449 assert!(
450 report.unreachable.is_empty(),
451 "{} has rules for these and no instruction is ever called one: {:?}",
452 report.source,
453 report.unreachable
454 );
455 }
456
457 /// Every opcode is one of the three things, so a new opcode in the IR fails this until
458 /// somebody says where it goes. That is the whole point: the answer for a new opcode should
459 /// be written down when it is added rather than discovered by a user compiling a program.
460 #[test]
461 fn every_opcode_is_lowered_or_is_a_gap_somebody_wrote_down() {
462 let report = report(&TABLE);
463 assert!(
464 report.unaccounted.is_empty(),
465 "no rule lowers these, `ELSEWHERE` does not say where they are lowered and `GAPS` \
466 does not say why they are not: {:?}",
467 report.unaccounted
468 );
469 assert_eq!(
470 report.by_rule.len() + report.elsewhere.len() + report.gaps.len(),
471 report.opcodes,
472 "the three lists overlap, so an opcode is counted twice"
473 );
474 }
475
476 /// An entry that starts being covered fails, which is the rule every list in this project is
477 /// kept under. An opcode a rule now lowers is one that should be off both lists, and a list
478 /// that keeps claiming otherwise is a list nobody can read.
479 #[test]
480 fn an_entry_a_rule_now_covers_is_a_stale_entry() {
481 let report = report(&TABLE);
482 for &(opcode, where_) in ELSEWHERE {
483 assert!(
484 !report.by_rule.contains(&opcode),
485 "`{}` is lowered by a rule now, so the `ELSEWHERE` entry saying it is lowered by \
486 {where_} is stale",
487 opcode.name()
488 );
489 }
490 for &(opcode, why, issue) in GAPS {
491 assert!(
492 !report.by_rule.contains(&opcode),
493 "`{}` is lowered by a rule now, so the `GAPS` entry saying it is {why} is stale \
494 and {issue} may be closed",
495 opcode.name()
496 );
497 assert!(
498 !report.elsewhere.contains(&opcode),
499 "`{}` is on both lists, so it is both lowered and not lowered",
500 opcode.name()
501 );
502 }
503 }
504
505 /// Every gap names an issue, since a gap with no issue behind it is a gap nobody has decided
506 /// anything about, which is the thing this module exists to stop.
507 #[test]
508 fn every_gap_names_the_issue_that_closes_it() {
509 let issues = GAPS
510 .iter()
511 .map(|&(_, _, issue)| issue)
512 .chain(WIDTHS.iter().map(|&(_, _, issue)| issue));
513 for issue in issues {
514 let number = issue
515 .strip_prefix("tamnd/rucc#")
516 .unwrap_or_else(|| panic!("{issue} is not an issue in this project's tracker"));
517 assert!(number.parse::<u32>().is_ok(), "{issue} does not name an issue number");
518 }
519 }
520
521 /// The count, which `spec/15-testing.md` section 15.8 says we keep about ourselves. CI runs
522 /// this test with the output shown, so the number lands in a log next to the rule proof
523 /// rather than in a file somebody has to go and read.
524 #[test]
525 fn the_count_is_reported() {
526 let report = report(&TABLE);
527 println!("{report}");
528 for &(opcode, why, issue) in GAPS {
529 println!("rucc-codegen: no lowering for `{}`, which is {why}: {issue}", opcode.name());
530 }
531 for &(width, why, issue) in WIDTHS {
532 println!("rucc-codegen: no rule at {width}, which is {why}: {issue}");
533 }
534 assert_eq!(report.gaps.len(), GAPS.len());
535 }
536
537 /// What the root of the trie is, which is the assumption [`pattern_heads`] rests on. If the
538 /// rule compiler ever built the trie some other way this would say so, rather than the
539 /// coverage numbers quietly becoming a report about an empty list.
540 #[test]
541 fn the_root_of_the_trie_is_the_head_of_every_pattern() {
542 let heads = pattern_heads(&TABLE);
543 assert!(!heads.is_empty(), "the table has rules and the root of the trie tests nothing");
544 for rule in TABLE.rules {
545 let head = rule
546 .pattern
547 .strip_prefix('(')
548 .and_then(|rest| rest.split([' ', ')']).next())
549 .expect("a pattern is an application");
550 assert!(
551 heads.contains(&head),
552 "line {}: {} is a pattern whose head the root of the trie does not test",
553 rule.line,
554 rule.pattern
555 );
556 }
557 }
558
559 /// The one target with a rule file, and the two that get one at M6. A machine that can be
560 /// compiled for has rules to report the coverage of, and one that cannot has none rather than
561 /// an empty set of them, which are different answers and would read the same as a number.
562 #[test]
563 fn a_target_with_a_back_end_is_a_target_with_a_rule_set() {
564 let x86 = table(Arch::X86_64).expect("x86-64 is what this crate lowers for");
565 assert_eq!(x86.source, TABLE.source);
566 assert!(!x86.rules.is_empty());
567 assert!(table(Arch::Aarch64).is_none(), "there is no aarch64 rule file yet");
568 assert!(table(Arch::Riscv64).is_none(), "there is no riscv64 rule file yet");
569 }
570
571 /// What a rule is called outside this process. The index is not it: a rule added at the top of
572 /// the file moves every index below it, and a report from last week would then be a report
573 /// about the wrong rules. The file and the line do not move that way and are somewhere to look.
574 #[test]
575 fn a_rule_is_written_down_as_the_place_it_is_written_at() {
576 let mut fired = Fired::new();
577 fired.mark(0);
578 let listing = fired.listing(&TABLE);
579 let first =
580 format!("fired {}:{} {}", TABLE.source, TABLE.rules[0].line, TABLE.rules[0].pattern);
581 assert!(listing.contains(&first), "{listing}");
582 assert!(listing.lines().next().is_some_and(|line| line.starts_with('#')), "{listing}");
583 }
584
585 /// Every rule is listed and not only the ones that fired, which is what lets one of these files
586 /// be read on its own. A reader that only got the rules that fired would have to parse the rule
587 /// file to find out what the rest of them were.
588 #[test]
589 fn one_file_says_what_the_whole_rule_set_is() {
590 let listing = Fired::new().listing(&TABLE);
591 let lines: Vec<&str> = listing.lines().collect();
592 assert_eq!(lines.len(), TABLE.rules.len() + 1, "one line per rule and one for the count");
593 assert_eq!(
594 lines.iter().filter(|line| line.starts_with("unused ")).count(),
595 TABLE.rules.len()
596 );
597 assert!(lines[0].contains(&format!("0 of {} rules", TABLE.rules.len())), "{}", lines[0]);
598 }
599
600 /// A compilation is many functions and a command line is many files, and the question is about
601 /// all of them at once. Merging is also what keeps the answer the same however the work was
602 /// scheduled, which is the rule `spec/03-architecture.md` section 3.7 holds everything to.
603 #[test]
604 fn what_two_runs_reached_is_what_either_of_them_reached() {
605 let mut one = Fired::new();
606 one.mark(3);
607 one.mark(3);
608 assert_eq!(one.count(), 1, "a rule that fires twice is one rule");
609 let mut two = Fired::new();
610 two.mark(0);
611 two.mark(9);
612 one.merge(&two);
613 assert_eq!(one.count(), 3);
614 assert!(one.has(0) && one.has(3) && one.has(9));
615 assert!(!one.has(1));
616
617 // The merge is symmetric, since neither order of two files is the right one.
618 let mut back = Fired::new();
619 back.mark(0);
620 back.mark(9);
621 let mut three = Fired::new();
622 three.mark(3);
623 back.merge(&three);
624 assert_eq!(back, one);
625 }
626}