Skip to main content

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 a lowering, which is not a gap. `spec/10-backend.md` names five of them and there
21//! 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. Which opcodes those are is
26//! [`crate::capability::lowering`] and is not written down here, because it was written down here
27//! and in the lowering group both and the two could disagree.
28//!
29//! The third is [`GAPS`], which is the number `spec/15-testing.md` section 15.8 says we keep. Each
30//! entry names why it is there and the issue that closes it, so that an opcode nobody has written a
31//! rule for is a decision somebody wrote down rather than a surprise.
32//!
33//! [`WIDTHS`] and [`NAMES`] are the same third answer said about something smaller than an opcode.
34//! A width on [`WIDTHS`] has no names at all, so no opcode is missing a rule at it, and a name on
35//! [`NAMES`] is one width of an opcode that lowers at its other widths. Both carry the issue that
36//! closes them for the same reason [`GAPS`] does.
37//!
38//! # What makes the lists honest
39//!
40//! An entry that stops being true fails. An opcode on either list that a rule starts covering is a
41//! stale entry and the tests below say so by name, which is the same rule the exclusion lists in
42//! the compatibility harness are kept under: a list nothing checks is a list that only grows.
43//!
44//! The direction this cannot check is an opcode moving from [`GAPS`] to a hand written lowering
45//! without [`crate::capability::HAND`] following it, because where an opcode is lowered by name is
46//! a `match` arm and there is nothing to ask about a `match` arm from here. What that costs is one
47//! line of a list going out of date; what it does not cost is a gap going unnoticed, since the
48//! opcode is still on a list and still counted.
49//!
50//! # The other question
51//!
52//! All of the above is about the rule set as it is written. [`Fired`] is about the rule set as it
53//! is used: which rules a compilation actually reached. A rule nothing reaches is proved and dead
54//! weight, or it is a construct the corpus does not contain and somebody should know which. The
55//! selector marks a rule as it fires it, the driver writes the marks out under
56//! `-Zrule-coverage=FILE`, and the harness in `tamnd/rucc-compat` unions those files over a corpus,
57//! which is what turns coverage of the rule set into a number. `spec/20-execution-testing.md`
58//! section 20.9 is the design and `tamnd/rucc#261` is the work.
59
60use core::fmt;
61use core::fmt::Write as _;
62
63use rucc_ir::Opcode;
64use rucc_target::Arch;
65
66use crate::capability::{self, pattern_heads};
67use crate::select::Table;
68use crate::term;
69
70/// An opcode nothing lowers, why it is here, and the issue that closes it.
71///
72/// This is the count `spec/15-testing.md` section 15.8 asks for. It is not zero yet and the
73/// spec says it should be, which is the honest reading of where the back end is: every one of
74/// these is a feature nobody has written, and all of them but one are opcodes the front end
75/// cannot produce either, so a program that reaches one of these is a program that reaches an
76/// unimplemented builtin first. The one is the remainder of two floats, which a program writes
77/// with an operator and which is a call to the maths library rather than an instruction.
78pub static GAPS: &[(Opcode, &str, &str)] = &[
79    (Opcode::Splat, "a vector, and no rule is written about a lane count", "tamnd/rucc#200"),
80    (
81        Opcode::TargetIntrinsic,
82        "the same, since what needs one is a vector builtin",
83        "tamnd/rucc#200",
84    ),
85    (
86        Opcode::FRem,
87        "a call to `fmod`, so a link line question as much as a lowering one",
88        "tamnd/rucc#226",
89    ),
90    (
91        Opcode::Fma,
92        "a call or one instruction, depending on what the machine is told it has",
93        "tamnd/rucc#226",
94    ),
95    (Opcode::Bitreverse, "a node nothing writes and nothing lowers", "tamnd/rucc#363"),
96    (
97        Opcode::SetjmpMarker,
98        "a call that returns twice, which the allocator has to be told about",
99        "tamnd/rucc#223",
100    ),
101    (Opcode::LongjmpMarker, "the same", "tamnd/rucc#223"),
102    (Opcode::TailCall, "a terminator nothing writes and nothing lowers", "tamnd/rucc#365"),
103    // Memory safety. These are a gap in a different sense from the rest: nothing emits one yet
104    // either, since the passes that would are milestones S5 and after, so there is no program the
105    // back end can be handed that reaches one. The ones the safety pass lowers are on `HAND`,
106    // and the five that make a capability all left this list without anything emitting them, which
107    // is the whole of tamnd/rucc#1085's lowering half: each has a lowering waiting for the pass that
108    // will write one, because a capability had to be a value the back end could hold before any of
109    // them could be written down at all. The two region markers left the same way and for a
110    // different reason, which is that what they cost is a count rather than a lowering.
111    // What is left is the plane writes, which the runtime does for itself today because the only
112    // ranges anything asks about are the ones its own allocator handed out. A stack object needs
113    // these, since nothing in the runtime sees a frame being set up or torn down.
114    (Opcode::MetaBegin, "a write over a range of the lifetime plane", "tamnd/rucc#856"),
115    (
116        Opcode::MetaEnd,
117        "the same write, with the version bumped past every capability",
118        "tamnd/rucc#856",
119    ),
120    (
121        Opcode::MetaTransfer,
122        "the same, and the state a range is in while a device owns it, which is S2's",
123        "tamnd/rucc#856",
124    ),
125];
126
127/// A width no rule is written at, why, and the issue that closes it.
128///
129/// The other half of coverage, and the half an opcode list cannot say. An opcode is covered when
130/// every name it has is a name a rule is written at, and a width with no name has no names to
131/// check: an `add` of two `__int128`s is not a missing rule for `add`, it is a width the rule
132/// language cannot spell. So the widths are written down here for the same reason the opcodes are
133/// written down above.
134pub static WIDTHS: &[(&str, &str, &str)] = &[
135    (
136        "one bit",
137        "everything but and, or, xor, a constant, and the widening out of one",
138        "tamnd/rucc#352",
139    ),
140    (
141        "a hundred and twenty eight bits",
142        "split into two halves before selection, except a division",
143        "tamnd/rucc#351",
144    ),
145    (
146        "eighty bits",
147        "a long double is on the x87 stack and no rule is about that stack",
148        "tamnd/rucc#326",
149    ),
150    (
151        "a hundred and twenty eight bits of float",
152        "turned into a call before selection, except a conditional move and the conversions \
153         against an integer that wide",
154        "tamnd/rucc#1064",
155    ),
156    (
157        "a vector of any lane count",
158        "a rule at a width says nothing about how many lanes",
159        "tamnd/rucc#200",
160    ),
161];
162
163/// A name a rule could be written at and deliberately is not, why, and the issue that puts it
164/// back.
165///
166/// The third list, and the one that is about a name rather than about an opcode or a width. An
167/// opcode on [`GAPS`] has no lowering at any width and a width on [`WIDTHS`] has no names at all,
168/// and neither of those can say that `add` is lowered at four widths and left alone at two.
169///
170/// This list used to be all of the narrow arithmetic. C promotes the operands of an arithmetic
171/// operator to `int` before the operator is applied, so `char a, b; a + b` is an `int` addition of
172/// two sign extended chars and there is no C program that asks the back end to add two bytes.
173/// Rules were written at those names anyway, ahead of the pass that would reach them, and they sat
174/// proved and never selected: `tamnd/rucc#261` measured that and `tamnd/rucc#368` took them out.
175/// Most of them are back, because the width narrowing pass in `tamnd/rucc#375` is that caller and
176/// it writes a byte add out of the truncation the assignment back to a `char` already was.
177///
178/// What is left is what the pass will not narrow. A divide is not narrowed because the most
179/// negative byte over minus one is a defined hundred and twenty eight at four bytes and is the
180/// overflow that raises at one, so it wants a range analysis saying that pair cannot happen.
181///
182/// Not every narrow name was ever here, because promotion is not the only way a narrow operation
183/// is born. Reading a bitfield is a shift and a mask by constants at the width of the storage
184/// unit, writing one is a mask, a shift and an `or` of two values, and a truth test on a narrow
185/// scalar is an `icmp_ne` at that scalar's width. Those fire, so those always had rules.
186pub static NAMES: &[(&str, &str, &str)] = &[
187    ("sdiv.i8", "a narrow divide, which wants a range analysis before it can be narrowed", NARROW),
188    ("sdiv.i16", "the same", NARROW),
189    ("udiv.i8", "the same", NARROW),
190    ("udiv.i16", "the same", NARROW),
191    ("srem.i8", "the same", NARROW),
192    ("srem.i16", "the same", NARROW),
193    ("urem.i8", "the same", NARROW),
194    ("urem.i16", "the same", NARROW),
195];
196
197/// The issue every entry of [`NAMES`] waits on, since they all wait on the same one.
198const NARROW: &str = "tamnd/rucc#375";
199
200/// What a target's rules cover, and what they do not.
201#[derive(Debug)]
202pub struct Report {
203    /// The rule file this is about, so that anything said about it names a file to open.
204    pub source: &'static str,
205    /// How many opcodes the IR has.
206    pub opcodes: usize,
207    /// The opcodes every name of which a rule is written at.
208    pub by_rule: Vec<Opcode>,
209    /// How many names those are, which is one per opcode and width.
210    pub names: usize,
211    /// A name a rule could be written at and none is, which is what a missing rule looks like.
212    pub uncovered: Vec<(Opcode, &'static str)>,
213    /// A name on [`NAMES`], which is a missing rule somebody decided to be missing.
214    pub deferred: Vec<(Opcode, &'static str)>,
215    /// A name a rule is written at that nothing can ever be called, which is a dead rule.
216    pub unreachable: Vec<&'static str>,
217    /// The opcodes lowered somewhere a rule cannot reach.
218    pub elsewhere: Vec<Opcode>,
219    /// The opcodes nothing lowers.
220    pub gaps: Vec<Opcode>,
221    /// The opcodes on none of the three lists, which is what a new opcode is until somebody says
222    /// where it goes.
223    pub unaccounted: Vec<Opcode>,
224}
225
226impl fmt::Display for Report {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        write!(
229            f,
230            "rucc-codegen: {} lowers {} of the {} IR opcodes by rule at {} names, {} are lowered \
231             where no rule reaches, {} have no lowering yet and {} names are left for later",
232            self.source,
233            self.by_rule.len(),
234            self.opcodes,
235            self.names,
236            self.elsewhere.len(),
237            self.gaps.len(),
238            self.deferred.len()
239        )
240    }
241}
242
243/// What a table covers.
244///
245/// Nothing is executed and nothing is compiled. The rule set and the naming of instructions are
246/// both data, and the answer is a comparison of two lists.
247#[must_use]
248pub fn report(table: &Table) -> Report {
249    let named = term::heads();
250    let patterns = pattern_heads(table);
251
252    let mut by_rule = Vec::new();
253    let mut uncovered = Vec::new();
254    let mut deferred = Vec::new();
255    for &(opcode, name) in &named {
256        if patterns.contains(&name) {
257            by_rule.push(opcode);
258        } else if NAMES.iter().any(|&(deliberate, ..)| deliberate == name) {
259            deferred.push((opcode, name));
260        } else {
261            uncovered.push((opcode, name));
262        }
263    }
264    // An opcode is covered when every name it has is covered, so one missing width takes the
265    // whole opcode off the list however many of its other widths are there. A name on `NAMES` does
266    // not take it off, because the opcode is lowered and the entry says which widths were left for
267    // later and why: that is a narrower claim than the opcode having nowhere to go, and putting it
268    // on `GAPS` instead would say the wrong thing about an `add` that lowers perfectly well at
269    // four widths.
270    for &(opcode, _) in &uncovered {
271        by_rule.retain(|&covered| covered != opcode);
272    }
273    by_rule.sort_unstable();
274    by_rule.dedup();
275
276    let names = named.len() - uncovered.len() - deferred.len();
277    let unreachable: Vec<&'static str> = patterns
278        .iter()
279        .filter(|head| !named.iter().any(|(_, name)| name == *head))
280        .copied()
281        .collect();
282
283    let elsewhere: Vec<Opcode> =
284        Opcode::all().filter(|&opcode| capability::lowering(opcode).is_some()).collect();
285    let gaps: Vec<Opcode> = GAPS.iter().map(|&(opcode, ..)| opcode).collect();
286    let unaccounted: Vec<Opcode> = Opcode::all()
287        .filter(|opcode| {
288            !by_rule.contains(opcode)
289                && !elsewhere.contains(opcode)
290                && !gaps.contains(opcode)
291                && !capability::LIBCALLS.iter().any(|&(at, ..)| at == *opcode)
292        })
293        .collect();
294
295    Report {
296        source: table.source,
297        opcodes: Opcode::all().count(),
298        by_rule,
299        names,
300        uncovered,
301        deferred,
302        unreachable,
303        elsewhere,
304        gaps,
305        unaccounted,
306    }
307}
308
309/// The rules a target lowers by, or `None` where no back end in this crate covers it.
310///
311/// The same question [`crate::pipeline::Machine::for_target`] answers about the rest of a machine,
312/// and it is here as well because a caller that wants to write down what a run covered has a
313/// target and no machine. An architecture that gets a rule file at M6 gets an arm here at the same
314/// time, and until then it has no rules to report coverage of rather than an empty set of them.
315#[must_use]
316pub fn table(arch: Arch) -> Option<&'static Table> {
317    match arch {
318        Arch::X86_64 => Some(&crate::select::x86_64::TABLE),
319        Arch::Aarch64 | Arch::Riscv64 => None,
320    }
321}
322
323/// Which rules fired, over one function or over a whole compilation.
324///
325/// A bit per rule and nothing else. This is on the path of every instruction selected, so what it
326/// costs is paid by every compilation whether or not anybody asked for the number, and the cheapest
327/// thing that answers the question is a flag per rule set once.
328///
329/// The index of a rule is how this is kept and not how it is written down. An index moves the
330/// moment a rule is added above it, so [`Fired::listing`] names the rule file and the line instead:
331/// a line is a place somebody can open, and a report written by one build can still be read against
332/// a rule file that has grown since.
333#[derive(Debug, Clone, Default, PartialEq, Eq)]
334pub struct Fired {
335    /// One entry per rule, true once that rule has fired. It grows to fit the highest index
336    /// marked rather than being sized from a table, so nothing here has to be told which target
337    /// is being compiled for.
338    seen: Vec<bool>,
339}
340
341impl Fired {
342    /// Nothing has fired yet.
343    #[must_use]
344    pub const fn new() -> Fired {
345        Fired { seen: Vec::new() }
346    }
347
348    /// Records that the rule at this index fired.
349    pub fn mark(&mut self, rule: usize) {
350        if self.seen.len() <= rule {
351            self.seen.resize(rule + 1, false);
352        }
353        self.seen[rule] = true;
354    }
355
356    /// Whether the rule at this index fired.
357    #[must_use]
358    pub fn has(&self, rule: usize) -> bool {
359        self.seen.get(rule).copied().unwrap_or(false)
360    }
361
362    /// How many rules fired.
363    #[must_use]
364    pub fn count(&self) -> usize {
365        self.seen.iter().filter(|fired| **fired).count()
366    }
367
368    /// Takes in everything another one recorded.
369    ///
370    /// One compilation is many functions and one command line is many files, and the question is
371    /// about all of them together. Merging rather than writing a file per function is also what
372    /// keeps the answer the same however the work was scheduled.
373    pub fn merge(&mut self, other: &Fired) {
374        if self.seen.len() < other.seen.len() {
375            self.seen.resize(other.seen.len(), false);
376        }
377        for (mine, theirs) in self.seen.iter_mut().zip(&other.seen) {
378            *mine |= *theirs;
379        }
380    }
381
382    /// What `-Zrule-coverage=FILE` writes.
383    ///
384    /// One line per rule in the table, in the order the rule file writes them, each saying whether
385    /// the rule fired and naming the file and line it is written at. Every rule is listed rather
386    /// than only the ones that fired, so that one of these files says what the whole rule set was
387    /// as well as what this compilation reached: a reader unioning them over a corpus needs both
388    /// and would otherwise have to parse the rule file to get the second.
389    ///
390    /// The first line is a comment holding the count, which is the number a person wants and the
391    /// one thing here that is not worth making them add up.
392    #[must_use]
393    pub fn listing(&self, table: &Table) -> String {
394        let fired = table.rules.iter().enumerate().filter(|(index, _)| self.has(*index)).count();
395        let mut out = format!(
396            "# rucc rule coverage: {fired} of {} rules in {} fired\n",
397            table.rules.len(),
398            table.source
399        );
400        for (index, rule) in table.rules.iter().enumerate() {
401            let word = if self.has(index) { "fired" } else { "unused" };
402            let _ = writeln!(out, "{word} {}:{} {}", table.source, rule.line, rule.pattern);
403        }
404        out
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use crate::select::x86_64::TABLE;
412
413    /// The claim the whole module is for, in the direction that matters: a name an instruction
414    /// can be called by is a name a rule is written at. This is the width check as much as the
415    /// opcode check, since a name is an opcode and a width together.
416    #[test]
417    fn every_name_an_instruction_can_have_is_one_a_rule_is_written_at() {
418        let report = report(&TABLE);
419        assert!(
420            report.uncovered.is_empty(),
421            "nothing in {} lowers these, and each is an opcode at a width the rule language can \
422             spell: {:?}",
423            report.source,
424            report.uncovered
425        );
426    }
427
428    /// And the other direction, which costs nothing to ask and finds a rule that can never fire.
429    /// A pattern head no instruction is ever called by is a rule written against a name that was
430    /// renamed or misspelled, and it would sit there proved and unreachable.
431    #[test]
432    fn every_name_a_rule_is_written_at_is_one_an_instruction_can_have() {
433        let report = report(&TABLE);
434        assert!(
435            report.unreachable.is_empty(),
436            "{} has rules for these and no instruction is ever called one: {:?}",
437            report.source,
438            report.unreachable
439        );
440    }
441
442    /// Every opcode is one of the three things, so a new opcode in the IR fails this until
443    /// somebody says where it goes. That is the whole point: the answer for a new opcode should
444    /// be written down when it is added rather than discovered by a user compiling a program.
445    #[test]
446    fn every_opcode_is_lowered_or_is_a_gap_somebody_wrote_down() {
447        let report = report(&TABLE);
448        assert!(
449            report.unaccounted.is_empty(),
450            "no rule lowers these, nothing rewrites them before selection, no runtime function \
451             stands for them and `GAPS` does not say why: {:?}",
452            report.unaccounted
453        );
454    }
455
456    /// An entry that starts being covered fails, which is the rule every list in this project is
457    /// kept under. An opcode a rule now lowers is one that should be off both lists, and a list
458    /// that keeps claiming otherwise is a list nobody can read.
459    #[test]
460    fn an_entry_a_rule_now_covers_is_a_stale_entry() {
461        let report = report(&TABLE);
462        for &(opcode, where_) in capability::HAND {
463            assert!(
464                !report.by_rule.contains(&opcode),
465                "`{}` is lowered by a rule now, so the `HAND` entry saying it is lowered by \
466                 {where_} is stale",
467                opcode.name()
468            );
469        }
470        for &(opcode, why, issue) in GAPS {
471            assert!(
472                !report.by_rule.contains(&opcode),
473                "`{}` is lowered by a rule now, so the `GAPS` entry saying it is {why} is stale \
474                 and {issue} may be closed",
475                opcode.name()
476            );
477            assert!(
478                !report.elsewhere.contains(&opcode),
479                "`{}` is on both lists, so it is both lowered and not lowered",
480                opcode.name()
481            );
482        }
483    }
484
485    /// The same staleness rule one list down. A name a rule is written at is a name that is not
486    /// left for later, and an entry claiming otherwise is one that should have gone when the rule
487    /// arrived. The other direction is checked too: a name no instruction can ever have is a
488    /// misspelling, and it would sit here excusing nothing.
489    #[test]
490    fn a_name_a_rule_is_written_at_is_not_a_name_left_for_later() {
491        let heads = pattern_heads(&TABLE);
492        let named = term::heads();
493        for &(name, why, issue) in NAMES {
494            assert!(
495                !heads.contains(&name),
496                "`{name}` is lowered by a rule now, so the `NAMES` entry saying it is {why} is \
497                 stale and {issue} may be closer than it says"
498            );
499            assert!(
500                named.iter().any(|&(_, head)| head == name),
501                "`{name}` is not a name any instruction can have, so the `NAMES` entry excuses \
502                 nothing"
503            );
504        }
505        let report = report(&TABLE);
506        assert_eq!(report.deferred.len(), NAMES.len(), "{:?}", report.deferred);
507    }
508
509    /// Every gap names an issue, since a gap with no issue behind it is a gap nobody has decided
510    /// anything about, which is the thing this module exists to stop.
511    #[test]
512    fn every_gap_names_the_issue_that_closes_it() {
513        let issues = GAPS
514            .iter()
515            .map(|&(_, _, issue)| issue)
516            .chain(WIDTHS.iter().map(|&(_, _, issue)| issue))
517            .chain(NAMES.iter().map(|&(_, _, issue)| issue));
518        for issue in issues {
519            let number = issue
520                .strip_prefix("tamnd/rucc#")
521                .unwrap_or_else(|| panic!("{issue} is not an issue in this project's tracker"));
522            assert!(number.parse::<u32>().is_ok(), "{issue} does not name an issue number");
523        }
524    }
525
526    /// The count, which `spec/15-testing.md` section 15.8 says we keep about ourselves. CI runs
527    /// this test with the output shown, so the number lands in a log next to the rule proof
528    /// rather than in a file somebody has to go and read.
529    #[test]
530    fn the_count_is_reported() {
531        let report = report(&TABLE);
532        println!("{report}");
533        for &(opcode, why, issue) in GAPS {
534            println!("rucc-codegen: no lowering for `{}`, which is {why}: {issue}", opcode.name());
535        }
536        for &(width, why, issue) in WIDTHS {
537            println!("rucc-codegen: no rule at {width}, which is {why}: {issue}");
538        }
539        for &(name, why, issue) in NAMES {
540            println!("rucc-codegen: no rule at `{name}`, which is {why}: {issue}");
541        }
542        assert_eq!(report.gaps.len(), GAPS.len());
543    }
544
545    /// What the root of the trie is, which is the assumption [`pattern_heads`] rests on. If the
546    /// rule compiler ever built the trie some other way this would say so, rather than the
547    /// coverage numbers quietly becoming a report about an empty list.
548    #[test]
549    fn the_root_of_the_trie_is_the_head_of_every_pattern() {
550        let heads = pattern_heads(&TABLE);
551        assert!(!heads.is_empty(), "the table has rules and the root of the trie tests nothing");
552        for rule in TABLE.rules {
553            let head = rule
554                .pattern
555                .strip_prefix('(')
556                .and_then(|rest| rest.split([' ', ')']).next())
557                .expect("a pattern is an application");
558            assert!(
559                heads.contains(&head),
560                "line {}: {} is a pattern whose head the root of the trie does not test",
561                rule.line,
562                rule.pattern
563            );
564        }
565    }
566
567    /// The one target with a rule file, and the two that get one at M6. A machine that can be
568    /// compiled for has rules to report the coverage of, and one that cannot has none rather than
569    /// an empty set of them, which are different answers and would read the same as a number.
570    #[test]
571    fn a_target_with_a_back_end_is_a_target_with_a_rule_set() {
572        let x86 = table(Arch::X86_64).expect("x86-64 is what this crate lowers for");
573        assert_eq!(x86.source, TABLE.source);
574        assert!(!x86.rules.is_empty());
575        assert!(table(Arch::Aarch64).is_none(), "there is no aarch64 rule file yet");
576        assert!(table(Arch::Riscv64).is_none(), "there is no riscv64 rule file yet");
577    }
578
579    /// What a rule is called outside this process. The index is not it: a rule added at the top of
580    /// the file moves every index below it, and a report from last week would then be a report
581    /// about the wrong rules. The file and the line do not move that way and are somewhere to look.
582    #[test]
583    fn a_rule_is_written_down_as_the_place_it_is_written_at() {
584        let mut fired = Fired::new();
585        fired.mark(0);
586        let listing = fired.listing(&TABLE);
587        let first =
588            format!("fired {}:{} {}", TABLE.source, TABLE.rules[0].line, TABLE.rules[0].pattern);
589        assert!(listing.contains(&first), "{listing}");
590        assert!(listing.lines().next().is_some_and(|line| line.starts_with('#')), "{listing}");
591    }
592
593    /// Every rule is listed and not only the ones that fired, which is what lets one of these files
594    /// be read on its own. A reader that only got the rules that fired would have to parse the rule
595    /// file to find out what the rest of them were.
596    #[test]
597    fn one_file_says_what_the_whole_rule_set_is() {
598        let listing = Fired::new().listing(&TABLE);
599        let lines: Vec<&str> = listing.lines().collect();
600        assert_eq!(lines.len(), TABLE.rules.len() + 1, "one line per rule and one for the count");
601        assert_eq!(
602            lines.iter().filter(|line| line.starts_with("unused ")).count(),
603            TABLE.rules.len()
604        );
605        assert!(lines[0].contains(&format!("0 of {} rules", TABLE.rules.len())), "{}", lines[0]);
606    }
607
608    /// A compilation is many functions and a command line is many files, and the question is about
609    /// all of them at once. Merging is also what keeps the answer the same however the work was
610    /// scheduled, which is the rule `spec/03-architecture.md` section 3.7 holds everything to.
611    #[test]
612    fn what_two_runs_reached_is_what_either_of_them_reached() {
613        let mut one = Fired::new();
614        one.mark(3);
615        one.mark(3);
616        assert_eq!(one.count(), 1, "a rule that fires twice is one rule");
617        let mut two = Fired::new();
618        two.mark(0);
619        two.mark(9);
620        one.merge(&two);
621        assert_eq!(one.count(), 3);
622        assert!(one.has(0) && one.has(3) && one.has(9));
623        assert!(!one.has(1));
624
625        // The merge is symmetric, since neither order of two files is the right one.
626        let mut back = Fired::new();
627        back.mark(0);
628        back.mark(9);
629        let mut three = Fired::new();
630        three.mark(3);
631        back.merge(&three);
632        assert_eq!(back, one);
633    }
634}