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 [`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}