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