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