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