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