rucc_opt/pipeline.rs
1//! The pipelines, one per optimization level, and the manager that runs one.
2//!
3//! Section 9.1 of `spec/09-optimizer.md` says the pipelines are written out rather than assembled
4//! from flags, and gives the reason: the prior art ran the same pipeline at every level and named
5//! that as a limitation. A level here is a list of pass names, and the list is the definition of
6//! the level rather than something that emerges from which flags happen to be set.
7//!
8//! Section 9.10 says the manager is deliberately boring. There is no adaptive ordering and no
9//! scheduling heuristic, because document 03's determinism rule needs the same input to produce
10//! the same output on every host and predictability is worth more than the last percent.
11//!
12//! What the manager does beyond running the list is the four things that make a pass debuggable:
13//! it counts each pass's transformations against its fuel, it collects what each pass said it did
14//! and did not do, it dumps the IR around whichever passes were asked for, and it verifies any
15//! function a pass changed.
16//!
17//! That last one is section 41.4 of `spec/optimizer/41-correctness.md`, which reads GCC's
18//! `execute_function_todo` and takes six things from it. Three of them are already true here by
19//! construction and are worth naming so that nobody looks for them. GCC verifies what the IR
20//! currently is, by consulting `curr_properties`, because its IR passes through GENERIC, GIMPLE
21//! with and without a CFG, GIMPLE in SSA, and RTL. rucc has one IR, it is in SSA from the moment
22//! the lowering walk builds it, and it always has a CFG, so the applicable set never varies and a
23//! bitmask saying so would have nothing to say. GCC guards the verifiers with `!seen_error()`,
24//! because after a user error the IR is legitimately malformed and an internal error raised over
25//! it hides the real diagnostic. Here the optimizer is not reached at all after a parse, check or
26//! lowering error, which is the same guard placed one level up where it cannot be forgotten. And
27//! GCC asserts that a verifier did not change the dominator state. Here a verifier takes the
28//! module by shared reference, so that is a type error rather than an assertion.
29//!
30//! What is left of the six is the part below: verify what changed, not everything, and say which
31//! function it was.
32
33use std::collections::HashMap;
34use std::fmt::Write as _;
35use std::sync::Arc;
36
37use rucc_base::{Interner, Symbol};
38use rucc_ir::{FuncId, Module, Pic};
39use rucc_session::OptLevel;
40
41use crate::{
42 Analyses, CallGraph, Fuel, Gates, Machine, Pass, Preserved, Stats, dce, extents, heap, image,
43 ipasra, ipcp, libcall, load, modref, nofree, number, outside, params, pass, purity, reload,
44};
45
46/// The passes that read a summary [`nofree::annotate`], [`extents::annotate`],
47/// [`params::annotate`] or [`heap::annotate`] writes onto the IR.
48///
49/// A list rather than one name because there will be more of them: section 7.5 asks for three more
50/// summary fields and section 7.3's lifetime elimination is the next thing to want this one. A pass
51/// that reads a summary and is not named here reads whatever the last build left, which is nothing,
52/// so the cost of forgetting to add a name is a missed optimization.
53///
54/// The five after the first are `crate::discharge`'s measurement runs, and leaving them out was a
55/// missed optimization of exactly that kind: a run measuring what an object says about itself, with
56/// no table saying how big any global is, answers that objects say nothing, and the number looks
57/// like a result rather than like a list with a name missing from it.
58const READS_SUMMARIES: &[&str] = &[
59 "discharge",
60 "discharge-objects",
61 "discharge-dominance",
62 "discharge-summaries",
63 "discharge-narrow",
64 "discharge-every",
65];
66
67/// Which passes build an alias oracle, and so want the module facts it asks about.
68///
69/// A list for the same reason as the one above, and it will grow the same way: the redundant load
70/// elimination of `spec/optimizer/16-gvn-and-pre.md` section 16.2 and the dead store elimination of
71/// document 17 both want one, and neither is written. A pass left off here builds its oracle on an
72/// empty table, which answers `May` to every question it would have used the module for, so what
73/// forgetting a name costs is a missed optimization rather than a wrong answer.
74const READS_OUTSIDE: &[&str] = &[load::NAME, reload::NAME];
75
76/// Which passes ask what a call is allowed to do.
77///
78/// Two, which are two of the four consumers section 34.6 of `spec/optimizer/34-ipa.md` names.
79/// Document 17's dead code elimination deletes a call whose result nothing reads. Document 16's
80/// value numbering makes two calls with the same arguments one value. The other two turned out to
81/// want the finer answer rather than this one and are in the list below: document 27.1's
82/// speculation predicate and document 08.4's call handling both read the mod and ref summaries,
83/// which say which memory rather than whether any. A list from the start for the reason the two
84/// above it are lists, which is that a pass left out of one reads the empty answer and loses an
85/// optimization rather than producing a wrong program.
86const READS_PURITY: &[&str] = &[dce::NAME, number::NAME];
87
88/// Which passes ask what a call does to the memory it was handed.
89///
90/// The two that move a load, which is the question section 34.6 says the per parameter answer is
91/// worth having for: whether the call in the middle of this loop can have written the array about
92/// to be reloaded. A list for the same reason as the three above it.
93const READS_MODREF: &[&str] = &[load::NAME, reload::NAME];
94
95/// `-O0`. Two passes, and neither of them is an optimization. Section 9.1 gives this level SSA
96/// construction, which the lowering walk in `spec/08-ir.md` already does, and mem2reg for the
97/// allocas that are left, which is the next pass to be written.
98///
99/// `expect` is here because what it takes out is a node the front end writes for every
100/// `__builtin_expect` in the program and gcc writes for none of them. Left standing it would be an
101/// instruction in the output of a level whose whole contract is that it emits what it was given.
102///
103/// `simplify-cfg` is here because a branch on a condition that is a constant is not a missed
104/// optimization, it is a call to a function the program never calls, and a program that calls a
105/// function it never calls is one that does not link. That is issue 359, gcc removes the code at
106/// every level including this one, and a `-O0` that emitted it would be a `-O0` some correct
107/// programs cannot be built at. Nothing else runs, and no analysis beyond the graph the pass
108/// reads reachability out of is computed.
109const O0: &[&str] = &["expect", "simplify-cfg"];
110
111/// `-O1`. Section 9.1 asks for one e-graph round, conservative inlining, simplify-CFG, SROA,
112/// GVN, DCE, LICM and the loop canonicalizations. Folding, control flow simplification and dead
113/// code elimination are the part of that which exists, with the peephole among them. They run in
114/// that order because folding and the peephole are what make most of the dead code there is to
115/// eliminate, because a constant a fold produced is a branch condition the control flow pass can
116/// then read, and because the comparison that branch was on is dead once it has.
117///
118/// `image` has a `fold` on each side of it, which is the other place in this list where a pass is
119/// named twice in a row, and both of them are the position rather than the pass. What it reads is
120/// a load from a `const` global at a constant byte offset, and until something has folded the
121/// address there is no constant byte offset: a subscript arrives from the front end as the index
122/// sign extended and multiplied by the element size, so without the `fold` ahead of it every array
123/// and every string in the program is a load it cannot answer. The `fold` behind it is the mirror
124/// of that. What `image` writes is a constant where a load stood, and what stands on top of it is
125/// whatever the program did with the value it read, so a `const double` converted to an `int` and
126/// compared against one is three folds in a row and only the first of them is this pass. Nothing
127/// later in the list would do it in time: the branch passes read the condition, and a condition
128/// still spelled as a conversion of a constant is a branch they leave standing. Running `image`
129/// before the pipeline rather than inside it is the alternative that does not work, and
130/// `crate::image` says at length why.
131///
132/// The peephole runs on both sides of `narrow`, which is the one place in this list where a pass
133/// is named twice, so the reason is worth stating. The rewrite table is written at a width, and
134/// the widths below `int` are unreachable from C source: the integer promotions mean an addition
135/// of two `char` values arrives here as an `add.i32`, so a rule about `add.i8` matches nothing
136/// that a front end can produce. `narrow` is what puts the width back, and it is therefore the
137/// only producer the narrow half of the table has. Running the peephole only before it left
138/// sixty nine of the first hundred and twenty five rules unable to fire on any program, which is
139/// issue 505 and is what the corpus measured. Running it only after it would give up the smaller
140/// trees the peephole hands `narrow`, since a subtree `narrow` redoes has to have one reader and
141/// an identity left standing is a second one. Both sides costs one more walk over each function
142/// and is what the pass is for.
143///
144/// `phiopt` comes after `thread` and the order between them is not arbitrary. Both look at a
145/// diamond whose arms carry a value to a join. Where the join then branches on that value,
146/// threading removes a branch and costs nothing, and if-conversion would have turned the same
147/// shape into a `select` the join branches on instead, which is strictly worse. Threading first
148/// leaves if-conversion the diamonds whose value is used rather than tested, which are the ones it
149/// is for.
150///
151/// `prune` is between `phiopt` and `simplify-cfg` and both sides of that are load bearing. It reads
152/// document 10's ranges off the graph to find a branch that can only go one way and a switch case
153/// nothing can reach, so it has to run after the two passes that change the graph most. What it
154/// leaves is a jump where a branch was and a block nothing reaches, and `simplify-cfg` is the pass
155/// that takes those out, so it has to run before it rather than after.
156///
157/// `canon` is where document 26's loop pipeline opens, so it goes after the value level passes and
158/// before the cleanup. It gives every loop a preheader, one latch, exits of its own and loop closed
159/// form, which is what lets the loop passes that follow it write `insert at the end of the
160/// preheader` rather than each making one. On its own it generates nothing: the blocks it adds are
161/// empty and the parameters it adds have one argument each, and `simplify-cfg` runs straight after
162/// it and takes both back out to a fixed point. That is section 26.7's arrangement, and it is why
163/// the position matters more than the pass does until the loop passes land on top of it.
164///
165/// `header-copy` is section 26.7's third step and `canon` runs again after it, which is the same
166/// section's instruction to re-canonicalize the loops it changed. It has to: what the copy leaves
167/// is a loop entered from a block that branches two ways, and a block that branches two ways is not
168/// a preheader. Nothing between the two needs the properties, so the second run is bookkeeping
169/// against the loop passes that come later rather than something this level's output depends on,
170/// and `simplify-cfg` after it takes out the blocks and parameters both runs added that nothing
171/// used.
172///
173/// `licm` comes after the copy and the canonicalization behind it, and section 27.1 says why it has
174/// to. What it may move in front of a loop depends on what runs on every entry to the loop, and
175/// after that pair that is the whole body rather than the header alone. Running it before the
176/// copy would leave it the header, which is most of the pass's value gone. It is also the reason
177/// the copy exists, so the two are one arrangement read from either end.
178///
179/// It is in the three speed levels and not in `-Os` or `-Oz`. Moving a computation out of a loop
180/// does not remove one, so there are no bytes in it for a level whose cost model is size, and the
181/// one thing it can cost is a spill inside the loop, which is bytes. That trade is worth making for
182/// time and there is nothing on the other side of it for space.
183///
184/// `unroll` runs after `licm` and only at the two speed levels. After, because what it does is copy
185/// the body, and a computation licm has already moved in front of the loop is one the copies do not
186/// each get their own of. It needs the same shape licm does and for the same reason, a loop that
187/// tests at the bottom with a preheader in front of it, so it sits at the end of the same run of
188/// loop passes rather than anywhere of its own. `simplify-cfg` straight after it is what turns the
189/// chain of copies into one block, since each copy now ends in a jump to the next and a block with
190/// one way in and one way out is a block that goes away.
191///
192/// `number` goes straight after that `simplify-cfg` and immediately before `load-forward`, and the
193/// two are one arrangement rather than two passes that happen to be adjacent. On its own it removes
194/// an instruction here and there, because the arithmetic a person writes is not usually written
195/// twice. What it is really for is the arithmetic the front end writes underneath: a subscript
196/// lowered twice is the same multiply and add twice, and giving the two one name is what turns a
197/// store and a load that `load-forward` was refusing into a store and a load of the same address.
198/// Running it the other way round would leave the pass after it nothing it did not already have.
199///
200/// `load-forward` goes next, and the position is the whole of what
201/// the pass is worth. It is the block local half of document 16, so what it can find is bounded by
202/// how much code is in one block, and `simplify-cfg` merging the straight line chains is what makes
203/// the blocks the largest they are ever going to be. At the two speed levels that position is also
204/// just after `unroll`, which is where the case the pass was written for lives: the body
205/// copies now sit in one block, and a copy that stored to an array slot and read it straight back
206/// is a store and a load of the same address with nothing in between.
207///
208/// `fold` runs a second time after it, and it is not there out of habit. What forwarding leaves
209/// behind is a value that arrived as a constant through memory, `grid[i] = 3` read back as a load
210/// that is now the literal three, and nothing else this late in the list would fold the arithmetic
211/// on top of it. The first `fold` ran before any of this existed.
212///
213/// `simplify` runs after that second `fold` for the same reason the second `fold` runs at all.
214/// Folding does not remove an instruction whose answer is a constant somebody still adds, it only
215/// writes the constant down, and an index folded to zero leaves a `ptr_add x, 0` behind. That is
216/// an identity the peephole takes and nothing else in the list is about. The unrolled body is
217/// where they come from: the copy that runs first subscripts the array at zero, so the multiply
218/// that worked its offset out is a multiply by zero, and until now the last thing any level did to
219/// that arithmetic was fold it. The add of zero reached the selector and was written out as an
220/// `addq $0`. Over the corpus at `-O2` the run is worth 3000 bytes across 1830 programs, 224 of
221/// them smaller and 4 larger, with every result unchanged.
222///
223/// A second `simplify-cfg` runs after that `simplify` at `-O1`, `-Os` and `-Oz`, and the two speed
224/// levels already had one further down for what `ivopts` and the second `licm` leave behind. What
225/// it is for is the branch nobody has to take any more. Forwarding a load turns a comparison of
226/// what was read into a comparison of what was written, `fold` settles it, and what is then left is
227/// a conditional branch on a constant with a block on the other side of it that the program cannot
228/// reach. Nothing else at these three levels looks at an edge after `load-forward` has run, so
229/// until now the branch and the block it guards were both written out. The block is usually the
230/// interesting half, since it is where the work that was never going to happen is, and at the two
231/// size levels a block that goes is bytes that go.
232///
233/// `hoist` is the first of the two check passes and it runs where it does because of what is above
234/// it. It needs a loop that tests at the bottom, which is what `header-copy` makes, and it needs a
235/// preheader to put a check in, which is what the `canon` after it puts back. Running it before
236/// `discharge` rather than after is deliberate as well: what it leaves in the preheader is a check
237/// over the whole range the loop sweeps, and that is a fact `discharge` can then use on anything
238/// else in front of the loop that is about the same bytes.
239///
240/// `discharge` is second to last, between `hoist` and `dce`, and both neighbours are the reason. It
241/// reads the dominator tree to find a safety check whose bytes an earlier check already covered, so
242/// it wants the graph after the block merging rather than before, when a straight run of code is
243/// still several blocks and a fact does not reach the check it would cover. What it leaves behind
244/// is the `cap_of` the check it removed was reading, which nothing now reads, so `dce` after it is
245/// what makes the function smaller rather than shorter by one instruction. It is in every level
246/// except `-O0`, which keeps every check on purpose: document 14 measures against a build where
247/// nothing was discharged, and that build is `-O0`.
248///
249/// `short-circuit-free` is the collapse of section 22.5 restricted to the cases that cost nothing,
250/// which are the ones where both halves of the `&&` ask about the same two values, so that what it
251/// writes is one comparison or a constant and not an and on top of two comparisons. The full
252/// version of that pass is a speed for size trade and belongs to `-O2`. This version is not a trade
253/// at all, so there is nothing here to decline. It sits where `-O2` puts the full pass, which is
254/// immediately above `thread`, and that pass is the reason: threading turns the shape this matches
255/// into one it does not.
256const O1: &[&str] = &[
257 "expect",
258 "fold",
259 "image",
260 "fold",
261 "simplify",
262 "narrow",
263 "simplify",
264 "short-circuit-free",
265 "thread",
266 "phiopt",
267 "prune",
268 "canon",
269 "header-copy",
270 "canon",
271 "licm",
272 "simplify-cfg",
273 "number",
274 "load-forward",
275 "fold",
276 "simplify",
277 "simplify-cfg",
278 "hoist",
279 "discharge",
280 "dead-plane",
281 "coalesce",
282 "dce",
283 "loop-delete",
284];
285
286/// `-O2`. The level the code quality claim is about. Section 9.1 asks for two e-graph rounds
287/// around the loop pipeline, the full inlining cost model, Memory SSA and the full alias
288/// analysis stack, and then the scalar and machine passes on top.
289///
290/// `short-circuit` is the one pass here that `-O1` does not have, and section 22.5 is where the
291/// level comes from. It folds the two branches of an `a && b` into one, which costs the right
292/// operand's work on the path that was skipping it and buys a branch the machine no longer has to
293/// guess. That is a trade worth making when the aim is speed and the branch is hard to call, and
294/// it is not one to make by default, which is what `-O1` is. What `-O1` has in its place is
295/// `short-circuit-free`, which is the same pass taking only the collapses that cost nothing, so
296/// this one is the trade and not the transformation.
297///
298/// It runs before `thread` and `phiopt` rather than after, and the order is not arbitrary. Both of
299/// those look at edges, and the collapse removes a block and turns two edges into one, so running
300/// it first hands them a smaller graph with nothing lost. The other way round, threading is free
301/// to give the second branch's block another predecessor, and a block two edges reach is one the
302/// collapse will not touch, so a chain that was foldable stops being foldable.
303///
304/// `canon` and `licm` run a second time after `split`, and that pair is the only thing here that
305/// looks at what `split` wrote. A guard goes in the preheader of the loop being split, which for an
306/// inner loop is a block inside the loops around it, and the guard asks the runtime how big each
307/// object is. On a matrix multiply that is four queries per entry to the innermost loop, two of
308/// them about a pointer that has not changed since it was allocated, and the pass that would take
309/// those out ran seven passes ago. `spec/safe-memory/13-performance.md` section 13.1 measured the
310/// cost and tamnd/rucc#893 is the rest of it.
311///
312/// `ivopts` goes last of the loop passes, because it is the one that decides what the loop's
313/// variables finally are and everything above it is still moving code around. It is followed by
314/// `simplify-cfg` and the pair cannot be separated. Section 28.4 has a loop stop asking its counter
315/// anything, and the counter goes on being incremented round the loop until the parameter carrying
316/// it is taken away. `crate::dce` says in its own documentation that it cannot do that, because the
317/// only reader left is the addition feeding the parameter back and a use count never reaches zero
318/// on a cycle. `crate::simplify_cfg` can, and says it was written for this. Without it the loop
319/// pays for the new pointer and keeps the old counter as well, which over the corpus is about half
320/// of what choosing badly costs.
321const O2: &[&str] = &[
322 "expect",
323 "fold",
324 "image",
325 "fold",
326 "simplify",
327 "narrow",
328 "simplify",
329 "switch-conv",
330 "short-circuit",
331 "thread",
332 "phiopt",
333 "prune",
334 "canon",
335 "header-copy",
336 "canon",
337 "licm",
338 "unroll",
339 "simplify-cfg",
340 "number",
341 "load-forward",
342 "redundant-load",
343 "fold",
344 "simplify",
345 "hoist",
346 "split",
347 "canon",
348 "licm",
349 "ivopts",
350 "simplify-cfg",
351 "discharge",
352 "dead-plane",
353 "coalesce",
354 "dce",
355 "loop-delete",
356];
357
358/// `-O3`. `-O2` plus loop vectorization, larger inlining and unrolling thresholds, interchange
359/// and distribution where the dependence analysis is confident, and function specialization.
360const O3: &[&str] = &[
361 "expect",
362 "fold",
363 "image",
364 "fold",
365 "simplify",
366 "narrow",
367 "simplify",
368 "switch-conv",
369 "short-circuit",
370 "thread",
371 "phiopt",
372 "prune",
373 "canon",
374 "header-copy",
375 "canon",
376 "licm",
377 "unroll",
378 "simplify-cfg",
379 "number",
380 "load-forward",
381 "redundant-load",
382 "fold",
383 "simplify",
384 "hoist",
385 "split",
386 "canon",
387 "licm",
388 "ivopts",
389 "simplify-cfg",
390 "discharge",
391 "dead-plane",
392 "coalesce",
393 "dce",
394 "loop-delete",
395];
396
397/// `-Os`. `-O2`'s passes under a size cost model: inlining only where it shrinks, no unrolling
398/// and no vectorization.
399///
400/// The second peephole is here rather than cut for size, because every rule it can fire replaces
401/// a term with a strictly smaller one. Tier one of `spec/optimizer/13-rewrite-rules.md` is
402/// defined that way, so a level that wants smaller code wants more of it and not less.
403///
404/// `short-circuit` is the pass this level drops from `-O2`, for the mirror of that reason. What it
405/// removes is a branch, which is time, and what it adds is the right operand's instructions on a
406/// path that did not run them and an and on top. The code comes out no smaller and usually a byte
407/// or two larger, so a level whose cost model is size has nothing to gain from it. What it keeps is
408/// `short-circuit-free` below.
409///
410/// `hoist` is dropped here as well, and the reason is the same trade read the other way.
411/// It takes a check out of a loop body and puts one in the preheader, plus the address arithmetic
412/// the new check needs, so the loop runs faster and the function is a few instructions larger. That
413/// is a speed transformation with a size cost, which is what `-Os` and `-Oz` are for declining.
414///
415/// `short-circuit-free` is here for the reason the `-O1` list gives, and the reason reads the same
416/// at a level whose cost model is size. The collapse it is restricted to takes a branch and a block
417/// away and adds nothing, so declining it would be declining something smaller.
418///
419/// `header-copy-small` is the same pass `-O1` and above run under section 26.6's smaller budget.
420/// The copy is code growth and this level pays for it once per loop, so five instructions is what
421/// it will pay. What it gets back is a body that is one region and an exit test at the bottom,
422/// which is slightly smaller in the steady state, so the trade is worth making at a limit that
423/// keeps the header small and not at one that copies twenty instructions to save two.
424const OS: &[&str] = &[
425 "expect",
426 "fold",
427 "image",
428 "fold",
429 "simplify",
430 "narrow",
431 "simplify",
432 "switch-conv",
433 "short-circuit-free",
434 "thread",
435 "phiopt",
436 "prune",
437 "canon",
438 "header-copy-small",
439 "canon",
440 "simplify-cfg",
441 "number",
442 "load-forward",
443 "fold",
444 "simplify",
445 "simplify-cfg",
446 "discharge",
447 "dead-plane",
448 "coalesce",
449 "dce",
450 "loop-delete",
451];
452
453/// `-Oz`. `-Os` and additionally the outliner, with instruction selection preferring the smaller
454/// encoding wherever there is a choice.
455///
456/// Header copying is the pass this level drops from `-Os`, which section 26.6 asks for by name. It
457/// is the one loop canonicalization that makes the function bigger, `-Oz` is the level that would
458/// rather have the branch than the bytes, and every reason to want the do-while form here is a
459/// speed reason.
460const OZ: &[&str] = &[
461 "expect",
462 "fold",
463 "image",
464 "fold",
465 "simplify",
466 "narrow",
467 "simplify",
468 "switch-conv",
469 "short-circuit-free",
470 "thread",
471 "phiopt",
472 "prune",
473 "canon",
474 "simplify-cfg",
475 "number",
476 "load-forward",
477 "fold",
478 "simplify",
479 "simplify-cfg",
480 "discharge",
481 "dead-plane",
482 "coalesce",
483 "dce",
484 "loop-delete",
485];
486
487/// The passes this level runs, before the command line adds to or removes from them.
488#[must_use]
489pub const fn for_level(level: OptLevel) -> &'static [&'static str] {
490 match level {
491 OptLevel::O0 => O0,
492 OptLevel::O1 => O1,
493 OptLevel::O2 => O2,
494 OptLevel::O3 => O3,
495 OptLevel::Os => OS,
496 OptLevel::Oz => OZ,
497 }
498}
499
500/// Which passes the IR is written out around.
501///
502/// Empty by default, which is the whole point: a dump is a debugging aid and writing files
503/// nobody asked for is not one.
504#[derive(Debug, Clone, Default, PartialEq, Eq)]
505pub struct Dumps {
506 /// Every pass, on both sides.
507 all: bool,
508 /// The passes to write out before.
509 before: Vec<String>,
510 /// The passes to write out after.
511 after: Vec<String>,
512}
513
514impl Dumps {
515 /// Adds one `-fdump-ir=` argument.
516 ///
517 /// # Errors
518 ///
519 /// When the argument is not `all`, `before-<pass>` or `after-<pass>`, or when it names a
520 /// pass this compiler does not have. A misspelled pass name that quietly dumped nothing
521 /// would look exactly like a pass that did not run.
522 pub fn add(&mut self, spec: &str) -> Result<(), String> {
523 if spec == "all" {
524 self.all = true;
525 return Ok(());
526 }
527 let (side, name) = match spec.split_once('-') {
528 Some(("before", name)) => (&mut self.before, name),
529 Some(("after", name)) => (&mut self.after, name),
530 _ => {
531 return Err(format!(
532 "`{spec}` is not a dump this compiler makes, which are `all`, \
533 `before-<pass>` and `after-<pass>`"
534 ));
535 }
536 };
537 if pass::find(name).is_none() {
538 return Err(format!("`{name}` is not a pass this compiler has, see --print-pipeline"));
539 }
540 side.push(name.to_owned());
541 Ok(())
542 }
543
544 /// Whether anything is dumped at all.
545 #[must_use]
546 pub fn is_empty(&self) -> bool {
547 !self.all && self.before.is_empty() && self.after.is_empty()
548 }
549
550 /// Whether the IR is written out before this pass runs.
551 #[must_use]
552 pub fn wants_before(&self, name: &str) -> bool {
553 self.all || self.before.iter().any(|it| it == name)
554 }
555
556 /// Whether the IR is written out after this pass runs.
557 #[must_use]
558 pub fn wants_after(&self, name: &str) -> bool {
559 self.all || self.after.iter().any(|it| it == name)
560 }
561}
562
563/// What the command line asked the optimizer for.
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub struct Options {
566 /// Which pipeline to start from.
567 pub level: OptLevel,
568 /// The passes `-f<name>` added and `-fno-<name>` removed, in the order they were given, so
569 /// that the last mention of a pass is the one that decides.
570 pub toggles: Vec<(String, bool)>,
571 /// What `-fpass-fuel=<pass>=<n>` limited, by pass name.
572 pub fuel: HashMap<String, u32>,
573 /// What `-fpass-fuel-global=<n>` limited the whole pipeline to, across every pass.
574 ///
575 /// This is the outer search of the two in section 4.5 of
576 /// `spec/optimizer/04-pass-manager.md`. Halving this finds the pass, and halving
577 /// `-fpass-fuel` for that pass finds the rewrite inside it. Two searches of twenty
578 /// compilations each beat one search over a space nobody knows the shape of.
579 pub global_fuel: Option<u32>,
580 /// What `-fdisable-<pass>` and `-fenable-<pass>` said about which functions a pass runs on.
581 pub gates: Gates,
582 /// What `-fdump-ir=` asked to see.
583 pub dumps: Dumps,
584 /// Whether the verifier runs after every pass that changed anything.
585 pub verify: bool,
586 /// Which definitions in this module something else may replace at load time.
587 ///
588 /// The analyses that read a body and write down what they found have to stop at a name like
589 /// that, because the body they read is not the one that will run. [`Pic::Library`] is the
590 /// answer when the object may end up in a shared library and the exported names in it are
591 /// interposable, which is what `-fPIC` alone means and is gcc's default.
592 ///
593 /// [`Pic::Executable`] is the answer for everything else, and that includes
594 /// `-fno-semantic-interposition`, where the build has promised that the definition here is the
595 /// one that runs. It is a promise and not a deduction, and it is the one every distribution
596 /// makes, because a library that cannot inline its own functions into each other pays for the
597 /// possibility of an interposition that never happens.
598 ///
599 /// This is not the same value the code generator is given. How an address is reached does not
600 /// change under that promise, and gcc does not change it either: a variable a shared library
601 /// exports is still read out of the global offset table, because the promise is about which
602 /// definition runs rather than about how many copies of the variable there are.
603 pub interposition: Pic,
604 /// Whether a call to a library function may be taken to mean what the standard says it means.
605 ///
606 /// `-fno-builtin` and `-ffreestanding` turned around, which is the pair section 20.1 of
607 /// `spec/optimizer/20-idioms-and-libcalls.md` describes. False stops [`crate::libcall`] from
608 /// reading a `printf` as anything but a call to whatever the program links against.
609 pub builtins: bool,
610 /// The library names `-fno-builtin-<name>` took away one at a time.
611 pub no_builtin: Vec<String>,
612}
613
614impl Default for Options {
615 /// The default level with nothing added to it, and the verifier on in a debug build, which
616 /// is what section 9.10 asks for.
617 fn default() -> Self {
618 Self {
619 level: OptLevel::default(),
620 toggles: Vec::new(),
621 fuel: HashMap::new(),
622 global_fuel: None,
623 gates: Gates::default(),
624 dumps: Dumps::default(),
625 verify: cfg!(debug_assertions),
626 interposition: Pic::Executable,
627 builtins: true,
628 no_builtin: Vec::new(),
629 }
630 }
631}
632
633impl Options {
634 /// The options a level asks for on its own.
635 #[must_use]
636 pub fn for_level(level: OptLevel) -> Self {
637 Self { level, ..Self::default() }
638 }
639
640 /// The passes the level and the `-f` flags chose, in order, before the gates are consulted.
641 ///
642 /// A pass named by `-f<name>` that the level did not choose is appended, because the only
643 /// place it could go that does not need an ordering rule nobody wrote down is the end.
644 #[must_use]
645 pub fn chosen(&self) -> Vec<&'static str> {
646 let mut names: Vec<&str> = for_level(self.level).to_vec();
647 for (name, on) in &self.toggles {
648 let name = name.as_str();
649 match *on {
650 true if !names.contains(&name) => names.push(name),
651 true => {}
652 // A pass that says it is required stays, since turning it off is a compile that
653 // fails rather than one that optimizes less. See [`Pass::required`].
654 false => names.retain(|it| *it != name || required(it)),
655 }
656 }
657 names.into_iter().filter_map(pass::find).map(Pass::name).collect()
658 }
659
660 /// Whether a module at a time transformation the level asked for is still asked for.
661 ///
662 /// [`Options::chosen`] cannot answer this. Everything it returns is a [`Pass`], which is one
663 /// function at a time, and section 34.6's propagation is a module at a time because what a
664 /// parameter holds is something the callers say. The last word wins, as it does there, so a
665 /// command line with both spellings on it means the one written second.
666 #[must_use]
667 pub fn wants(&self, name: &str) -> bool {
668 self.toggles.iter().rfind(|(it, _)| it == name).is_none_or(|&(_, on)| on)
669 }
670}
671
672/// Whether the pass of that name is one `-fno-<name>` does not turn off.
673fn required(name: &str) -> bool {
674 pass::find(name).is_some_and(|pass| pass.required())
675}
676
677impl Options {
678 /// The passes that will run, in order, over at least one function.
679 ///
680 /// A pass `-fenable-<name>` reached that the level did not choose is appended after them,
681 /// for the same reason and in the same place. It runs only over the functions the gate names,
682 /// which is the whole point of the flag: a pass being in this list is not the same question as
683 /// a pass running on the function somebody is looking at.
684 #[must_use]
685 pub fn passes(&self) -> Vec<&'static dyn Pass> {
686 let mut names = self.chosen();
687 for name in self.gates.enabled() {
688 // Through the pass list rather than straight from the gate, because the name the
689 // pass holds outlives this call and the one the gate holds does not.
690 let Some(found) = pass::find(name) else { continue };
691 if !names.contains(&found.name()) {
692 names.push(found.name());
693 }
694 }
695 names.into_iter().filter_map(pass::find).collect()
696 }
697}
698
699/// One written out copy of the IR.
700#[derive(Debug, Clone, PartialEq, Eq)]
701pub struct Dump {
702 /// What to call it, which is a number, a side and a pass name, as in `01-after-fold`. The
703 /// number is there so that a directory listing is in the order the passes ran.
704 pub name: String,
705 /// The module, in the textual form from `spec/08-ir.md`.
706 pub text: String,
707}
708
709/// What one pass had to say about one function.
710///
711/// One of these per pass per function with a body, whether or not the pass said anything, because
712/// a pass that reports nothing being visible as a pass that reports nothing is the point of the
713/// record. Section 42.2 of `spec/optimizer/42-measurement.md` has the argument.
714#[derive(Debug, Clone, PartialEq, Eq)]
715pub struct Remark {
716 /// Which pass, by the name a `-f` flag spells.
717 pub pass: &'static str,
718 /// Which function, by the name in the source.
719 pub func: Symbol,
720 /// What it said.
721 pub stats: Stats,
722}
723
724/// What running the pipeline produced beyond the changed module.
725#[derive(Debug, Clone, Default, PartialEq, Eq)]
726pub struct Report {
727 /// The dumps asked for, in the order they were taken. The manager does not write files,
728 /// because nothing below the driver in `spec/18-package-layout.md` knows what a file is.
729 pub dumps: Vec<Dump>,
730 /// A pass that left the IR in a state the verifier refuses, named, with what it said.
731 pub broke: Vec<String>,
732 /// How much fuel each pass spent, which is the number a bisection halves.
733 pub spent: Vec<(&'static str, u32)>,
734 /// What every pass said about every function, in the order the passes ran and then in the
735 /// order the module holds its functions. This is what `-fopt-info` prints.
736 pub remarks: Vec<Remark>,
737}
738
739impl Report {
740 /// Everything one pass said across the whole module, added up.
741 ///
742 /// The counts of an event are addable across functions because an event names a site in a
743 /// pass rather than a fact about a program, which is the reason [`crate::stats::Event::what`]
744 /// is a fixed string.
745 #[must_use]
746 pub fn totals(&self, pass: &str) -> Stats {
747 let mut total = Stats::new();
748 for remark in self.remarks.iter().filter(|it| it.pass == pass) {
749 total.merge(&remark.stats);
750 }
751 total
752 }
753}
754
755/// Runs the pipeline over the module.
756///
757/// Every pass sees every function with a body, one at a time, and a pass runs over the whole
758/// module before the next one starts. That order is what makes the dumps readable: a dump is
759/// the state of the program between two passes rather than between two functions.
760pub fn run(module: &mut Module, names: &mut Interner, opts: &Options) -> Report {
761 let mut report = Report::default();
762 let chosen = opts.chosen();
763 // One cache per function, kept across passes because a pass runs over the whole module
764 // before the next one starts. A cache that lived only as long as one function would be
765 // thrown away between every pass and would never answer a second question. Section 4.2 of
766 // `spec/optimizer/04-pass-manager.md` is the plan for turning the loop inside out, and the
767 // day that happens this map becomes a local in the inner loop.
768 let mut cached: HashMap<FuncId, Analyses> = HashMap::new();
769 // The machine, once for the module, because every function in it is compiled for the same
770 // target at the same goal. It goes into each function's cache rather than into a parameter of
771 // its own, per `crate::machine`.
772 let machine = Machine::of(module, opts.level);
773 // What the whole pipeline has left, which every pass draws its own allowance out of and
774 // gives the unspent part of back. A pass past the end of it is given nothing rather than
775 // skipped, so it still runs, still reports, and still transforms nothing.
776 let mut budget = opts.global_fuel;
777 // What each pass has left of what `-fpass-fuel` gave it. One allowance across every place
778 // the list names that pass, rather than one allowance each, because the number in the flag
779 // is meant to be the number of rewrites that happened. A peephole that runs twice under
780 // `-fpass-fuel=simplify=5` and rewrites ten things would make the bisection in section 4.5
781 // of `spec/optimizer/04-pass-manager.md` step over the rewrite it was looking for.
782 let mut allowance = opts.fuel.clone();
783 let passes = opts.passes();
784 // Before anything runs, because each of these is a fact about the module and every pass after
785 // this sees one function. Only when a pass in this run reads them: a flag nothing looks at
786 // would show up in every `-O0` dump and mean nothing to anybody reading one.
787 if passes.iter().any(|pass| READS_SUMMARIES.contains(&pass.name())) {
788 nofree::annotate(module, names, opts.interposition);
789 extents::annotate(module, opts.interposition);
790 params::annotate(module, opts.interposition);
791 heap::annotate(module, names);
792 }
793 // In the same place and for the same reason, except that this one is read by a pass rather
794 // than by a summary, so it is handed over on the analysis cache instead of written onto the
795 // module. Only when the run has that pass in it, since it is a copy of the module's read only
796 // data and nothing else would ever look at it.
797 let images = if passes.iter().any(|pass| pass.name() == image::NAME) {
798 Arc::new(image::Images::of(module, opts.interposition))
799 } else {
800 Arc::default()
801 };
802 // And the same again for the alias oracle's half of the module, which is what each name
803 // refers to, what a callee is declared to do, the tree of type nodes and the layout. Built
804 // only for a run with a pass that asks, since the empty one answers `May` and every pass here
805 // is correct against that.
806 let outside = if passes.iter().any(|pass| READS_OUTSIDE.contains(&pass.name())) {
807 Arc::new(outside::Outside::of(module))
808 } else {
809 Arc::default()
810 };
811 // And once more for what each function is allowed to do, which wants the call graph under it
812 // and is the one thing here that reads every body in the module rather than looking at the
813 // outside of each one. Section 34.6 puts it at `-O1` and above, which is where gcc turns
814 // `-fipa-pure-const` on, and the level is the gate rather than the pass list alone because
815 // `-O0` has `dce` in it and the promise of that level is compile time.
816 let wants_purity =
817 opts.level != OptLevel::O0 && passes.iter().any(|pass| READS_PURITY.contains(&pass.name()));
818 // And the per parameter answer a level above that, where section 34.6 puts it and where gcc
819 // turns `-fipa-modref` on for anything that is not `-O0` or a debug build. A level above
820 // because this one reads every instruction of every body rather than every call in each one,
821 // so it is the more expensive of the two and `-O1` is the level whose promise is compile time.
822 let wants_modref = !matches!(opts.level, OptLevel::O0 | OptLevel::O1)
823 && passes.iter().any(|pass| READS_MODREF.contains(&pass.name()));
824 // And section 34.6's propagation, at the level it puts it at, which is where gcc turns
825 // `-fipa-cp` on (`gcc/opts.cc:654`). A transformation rather than an analysis, so it is not in
826 // the pass list: everything in that list is a [`Pass`], which is one function at a time, and
827 // what a parameter holds is something the callers say. The level decides and `-fno-ipa-cp`
828 // overrides, which is what the list itself gets from [`Options::chosen`].
829 let wants_ipcp = !matches!(opts.level, OptLevel::O0 | OptLevel::O1) && opts.wants(ipcp::NAME);
830 // And section 34.6's other half, at the same level, which is where gcc turns `-fipa-sra` on as
831 // well. After the propagation rather than before it: a parameter the propagation turned into a
832 // constant in the body is a parameter nothing reads any more, and this is what then takes it
833 // out along with the argument at every call.
834 let wants_ipasra =
835 !matches!(opts.level, OptLevel::O0 | OptLevel::O1) && opts.wants(ipasra::NAME);
836 // Before the call graph, because it is the one transformation here that takes a call away
837 // altogether and a graph built over the module after it is the smaller of the two. `-O1` and
838 // above, which is where gcc folds these, and off under `-fno-builtin` or `-ffreestanding`,
839 // since a freestanding program left with a call to a `puts` it never wrote will not link.
840 if opts.level != OptLevel::O0 && opts.builtins && opts.wants(libcall::NAME) {
841 let mut fuel = match (allowance.get(libcall::NAME).copied(), budget) {
842 (Some(count), Some(left)) => Fuel::of(count.min(left)),
843 (Some(count), None) => Fuel::of(count),
844 (None, Some(left)) => Fuel::of(left),
845 (None, None) => Fuel::unlimited(),
846 };
847 let folded = libcall::fold(module, names, &opts.no_builtin, opts.interposition, &mut fuel);
848 for (id, stats) in folded {
849 if opts.verify {
850 if let Err(errors) = rucc_ir::verify_func(module, &module[id], names) {
851 let func = names.resolve(module[id].name);
852 for error in errors {
853 report.broke.push(format!(
854 "the {} pass left invalid IR in {func}, {error}",
855 libcall::NAME
856 ));
857 }
858 }
859 }
860 report.remarks.push(Remark { pass: libcall::NAME, func: module[id].name, stats });
861 }
862 report.spent.push((libcall::NAME, fuel.spent()));
863 if let Some(left) = &mut budget {
864 *left -= fuel.spent();
865 }
866 if let Some(left) = allowance.get_mut(libcall::NAME) {
867 *left -= fuel.spent();
868 }
869 }
870 // One graph for all four, because building it is a walk over the module and none of them adds
871 // an edge to it. The two transformations take edges away, by leaving a call nothing reaches or
872 // an address nothing hands out, and a graph that still holds those is the conservative one.
873 let graph = (wants_purity || wants_modref || wants_ipcp || wants_ipasra)
874 .then(|| CallGraph::of(module, opts.interposition));
875 // Before the two below rather than after them, because it is the one of the three that changes
876 // a body, and an answer worked out from a body should be worked out from the body the passes
877 // will see. It leaves the edges alone, so the graph under it is the same graph either way.
878 if let (true, Some(graph)) = (wants_ipcp, graph.as_ref()) {
879 let mut fuel = match (allowance.get(ipcp::NAME).copied(), budget) {
880 (Some(count), Some(left)) => Fuel::of(count.min(left)),
881 (Some(count), None) => Fuel::of(count),
882 (None, Some(left)) => Fuel::of(left),
883 (None, None) => Fuel::unlimited(),
884 };
885 for (id, stats) in ipcp::propagate(module, graph, &mut fuel) {
886 if opts.verify {
887 if let Err(errors) = rucc_ir::verify_func(module, &module[id], names) {
888 let func = names.resolve(module[id].name);
889 for error in errors {
890 report.broke.push(format!(
891 "the {} pass left invalid IR in {func}, {error}",
892 ipcp::NAME
893 ));
894 }
895 }
896 }
897 report.remarks.push(Remark { pass: ipcp::NAME, func: module[id].name, stats });
898 }
899 report.spent.push((ipcp::NAME, fuel.spent()));
900 if let Some(left) = &mut budget {
901 *left -= fuel.spent();
902 }
903 if let Some(left) = allowance.get_mut(ipcp::NAME) {
904 *left -= fuel.spent();
905 }
906 }
907 if let (true, Some(graph)) = (wants_ipasra, graph.as_ref()) {
908 let mut fuel = match (allowance.get(ipasra::NAME).copied(), budget) {
909 (Some(count), Some(left)) => Fuel::of(count.min(left)),
910 (Some(count), None) => Fuel::of(count),
911 (None, Some(left)) => Fuel::of(left),
912 (None, None) => Fuel::unlimited(),
913 };
914 for (id, stats) in ipasra::remove(module, graph, names, &mut fuel) {
915 if opts.verify {
916 if let Err(errors) = rucc_ir::verify_func(module, &module[id], names) {
917 let func = names.resolve(module[id].name);
918 for error in errors {
919 report.broke.push(format!(
920 "the {} pass left invalid IR in {func}, {error}",
921 ipasra::NAME
922 ));
923 }
924 }
925 }
926 report.remarks.push(Remark { pass: ipasra::NAME, func: module[id].name, stats });
927 }
928 report.spent.push((ipasra::NAME, fuel.spent()));
929 if let Some(left) = &mut budget {
930 *left -= fuel.spent();
931 }
932 if let Some(left) = allowance.get_mut(ipasra::NAME) {
933 *left -= fuel.spent();
934 }
935 }
936 let purity = match (wants_purity, graph.as_ref()) {
937 (true, Some(graph)) => {
938 let mut facts = purity::Facts::of_module(module, names);
939 purity::infer(module, graph, &mut facts);
940 Arc::new(facts)
941 }
942 _ => Arc::default(),
943 };
944 let modref = match (wants_modref, graph.as_ref()) {
945 (true, Some(graph)) => {
946 let mut summaries = modref::Summaries::of_module(module);
947 modref::summarize(module, graph, &mut summaries);
948 Arc::new(summaries)
949 }
950 _ => Arc::default(),
951 };
952 for (index, pass) in passes.into_iter().enumerate() {
953 let name = pass.name();
954 if opts.dumps.wants_before(name) {
955 report.dumps.push(dump(index, "before", name, module, names));
956 }
957 let mut fuel = match (allowance.get(name).copied(), budget) {
958 // Whichever limit is tighter, because two limits that disagree mean the one that
959 // stops first, and a bisection that started with the global one has to stay inside
960 // it while the per pass one is halved.
961 (Some(count), Some(left)) => Fuel::of(count.min(left)),
962 (Some(count), None) => Fuel::of(count),
963 (None, Some(left)) => Fuel::of(left),
964 (None, None) => Fuel::unlimited(),
965 };
966 // What the level and the `-f` flags decided, which is what a gate overrides for the
967 // functions it names and leaves alone for the ones it does not.
968 let default = chosen.contains(&name);
969 for id in module.funcs() {
970 if module[id].is_declaration() {
971 continue;
972 }
973 if !opts.gates.allows(name, default, id.raw(), names.resolve(module[id].name)) {
974 // No remark either. A pass that did not run on a function has nothing to say
975 // about it, and a record saying it found nothing would read as a pass that
976 // looked.
977 continue;
978 }
979 let an = cached.entry(id).or_insert_with(|| {
980 Analyses::new(machine)
981 .reading(Arc::clone(&images))
982 .about(Arc::clone(&outside))
983 .calling(Arc::clone(&purity))
984 .touching(Arc::clone(&modref))
985 });
986 let stats = pass.run(&mut module[id], an, &mut fuel);
987 // A pass that changed nothing preserved everything, whatever it says about itself,
988 // so the cheap case does not need every pass to have a second opinion about it.
989 // A pass that did change something is taken at its word, and in a checked build the
990 // word is checked.
991 let keeps = if stats.changed() { pass.preserves() } else { Preserved::ALL };
992 for broken in an.settle(&module[id], keeps, opts.verify) {
993 let func = names.resolve(module[id].name);
994 report.broke.push(format!(
995 "the {name} pass said it preserved {} of {func} and did not",
996 broken.name()
997 ));
998 }
999 // Here rather than after the pass, and this function rather than the module. A pass
1000 // is a function pass, so the only thing it can have broken is the function it was
1001 // given, and walking the other ones again after every one of them is the quadratic
1002 // walk `rucc_ir::verify_func` exists to avoid. Doing it here is also what lets the
1003 // message name the function, which the module walk could not, and it puts the
1004 // failure next to the pass that caused it rather than at the end of the module.
1005 if stats.changed() && opts.verify {
1006 if let Err(errors) = rucc_ir::verify_func(module, &module[id], names) {
1007 let func = names.resolve(module[id].name);
1008 for error in errors {
1009 report
1010 .broke
1011 .push(format!("the {name} pass left invalid IR in {func}, {error}"));
1012 }
1013 }
1014 }
1015 // The record is the only place the manager learns that anything happened, which is
1016 // why the pass cannot leave recording until later. See `crate::stats`.
1017 report.remarks.push(Remark { pass: name, func: module[id].name, stats });
1018 }
1019 // Added to rather than pushed, so a pass the list names twice is one line here with what
1020 // both of its runs spent. That is the number a bisection halves, and two lines under one
1021 // name would be two numbers where the flag takes one.
1022 match report.spent.iter_mut().find(|(it, _)| *it == name) {
1023 Some((_, total)) => *total += fuel.spent(),
1024 None => report.spent.push((name, fuel.spent())),
1025 }
1026 if let Some(left) = &mut budget {
1027 // Never below zero, because the allowance the pass was given was at most this.
1028 *left -= fuel.spent();
1029 }
1030 if let Some(left) = allowance.get_mut(name) {
1031 // Same, and for the same reason.
1032 *left -= fuel.spent();
1033 }
1034 if opts.dumps.wants_after(name) {
1035 report.dumps.push(dump(index, "after", name, module, names));
1036 }
1037 }
1038 report
1039}
1040
1041/// The module written out, under a name that sorts in the order the passes ran.
1042fn dump(index: usize, side: &str, name: &str, module: &Module, names: &Interner) -> Dump {
1043 Dump { name: format!("{index:02}-{side}-{name}"), text: rucc_ir::print(module, names) }
1044}
1045
1046/// Renders what `--print-pipeline` prints.
1047///
1048/// One line per pass, numbered from one, with what the pass does after it. A level that runs
1049/// nothing says so rather than printing an empty list, because an empty answer and a broken
1050/// command look the same.
1051#[must_use]
1052pub fn print(opts: &Options) -> String {
1053 let mut out = String::new();
1054 let _ = writeln!(out, "level: {}", opts.level);
1055 // Only when it was asked for, so the listing of a compilation nobody is bisecting is the
1056 // same listing it has always been. A run under a budget is a run whose output is not the
1057 // one the level asked for, and the listing is where that has to be visible.
1058 if let Some(count) = opts.global_fuel {
1059 let _ = writeln!(out, "global fuel: {count}");
1060 }
1061 let passes = opts.passes();
1062 if passes.is_empty() {
1063 let _ = writeln!(out, "no passes");
1064 return out;
1065 }
1066 for (index, pass) in passes.iter().enumerate() {
1067 let _ = write!(out, "{}: {}, {}", index + 1, pass.name(), pass.describe());
1068 // Only when a gate mentions the pass, so the listing of a compilation nobody is
1069 // debugging is the same listing it has always been.
1070 if let Some(note) = opts.gates.note(pass.name()) {
1071 let _ = write!(out, " [{note}]");
1072 }
1073 out.push('\n');
1074 }
1075 out
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use rucc_base::Interner;
1081 use rucc_ir::{
1082 Builder, Extra, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
1083 Signature, Type,
1084 };
1085 use rucc_session::OptLevel;
1086 use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
1087
1088 use super::{Dumps, Options, for_level};
1089 use crate::stats::Kind;
1090 use crate::{Pass, ipasra, ipcp, libcall, pass};
1091
1092 /// A module with one function whose body has something to fold in it.
1093 fn module() -> (Interner, Module) {
1094 let mut names = Interner::new();
1095 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1096 let mut module = Module::new(names.intern("test.c"), &target);
1097 let func = foldable(&mut names, "f");
1098 module.add_func(func);
1099 (names, module)
1100 }
1101
1102 /// A module with two of them, called `f` and `g`, in that order, so `f` is function 0.
1103 fn two_functions() -> (Interner, Module) {
1104 let mut names = Interner::new();
1105 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1106 let mut module = Module::new(names.intern("test.c"), &target);
1107 for name in ["f", "g"] {
1108 let func = foldable(&mut names, name);
1109 module.add_func(func);
1110 }
1111 (names, module)
1112 }
1113
1114 /// A module with one function holding two identities the peephole takes, on a value that
1115 /// arrives as a parameter so that folding cannot get to them first.
1116 fn identities() -> (Interner, Module) {
1117 let mut names = Interner::new();
1118 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1119 let mut module = Module::new(names.intern("test.c"), &target);
1120 let i32_ = Type::int(32);
1121 let mut func = Func::new(
1122 names.intern("h"),
1123 Signature::new().with_params(&[i32_]).with_returns(&[i32_]),
1124 );
1125 let entry = func.create_block();
1126 let x = func.append_param(entry, i32_);
1127 let mut build = Builder::new(&mut func, entry);
1128 let zero = build.iconst(i32_, 0);
1129 let one = build.iconst(i32_, 1);
1130 let sum = build.binary(Opcode::Add, x, zero, Flags::NONE);
1131 let product = build.binary(Opcode::Mul, sum, one, Flags::NONE);
1132 build.ret(&[product]);
1133 module.add_func(func);
1134 (names, module)
1135 }
1136
1137 /// A function that returns a sign extension of a constant, which folding rewrites.
1138 fn foldable(names: &mut Interner, name: &str) -> Func {
1139 let mut func =
1140 Func::new(names.intern(name), Signature::new().with_returns(&[Type::int(64)]));
1141 let block = func.create_block();
1142 let mut build = Builder::new(&mut func, block);
1143 let narrow = build.iconst(Type::int(32), 7);
1144 let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
1145 build.ret(&[wide]);
1146 func
1147 }
1148
1149 /// Whether the pass said anything about the function, which it only does when it ran on it.
1150 fn spoke_about(report: &super::Report, pass: &str, func: &str, names: &Interner) -> bool {
1151 report.remarks.iter().any(|it| it.pass == pass && names.resolve(it.func) == func)
1152 }
1153
1154 /// A module with a loop short enough for the unroller to flatten, over an array a parameter
1155 /// points at.
1156 ///
1157 /// Four iterations, which is a trip count the unroller takes whole. The copy that runs first
1158 /// subscripts the array at zero, so what works its offset out is a multiply by zero, and
1159 /// folding that is what leaves the addition this is here to look for.
1160 fn a_short_loop() -> (Interner, Module) {
1161 let mut names = Interner::new();
1162 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1163 let mut module = Module::new(names.intern("test.c"), &target);
1164 let (i32_, i64_) = (Type::int(32), Type::int(64));
1165 let signature = Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]);
1166 let mut func = Func::new(names.intern("sum"), signature);
1167 let entry = func.create_block();
1168 let head = func.create_block();
1169 let body = func.create_block();
1170 let exit = func.create_block();
1171 let p = func.append_param(entry, Type::PTR);
1172 let i = func.append_param(head, i32_);
1173 let acc = func.append_param(head, i32_);
1174
1175 let mut build = Builder::new(&mut func, entry);
1176 let zero = build.iconst(i32_, 0);
1177 build.jump(head, &[zero, zero]);
1178
1179 let mut build = Builder::new(&mut func, head);
1180 let four = build.iconst(i32_, 4);
1181 let more = build.icmp(IntPred::Slt, i, four);
1182 build.br_if(more, body, &[], exit, &[]);
1183
1184 let mut build = Builder::new(&mut func, body);
1185 let wide = build.unary(Opcode::SExt, i, i64_);
1186 let scale = build.iconst(i64_, 4);
1187 let offset = build.binary(Opcode::Mul, wide, scale, Flags::NSW);
1188 let at = build.binary(Opcode::PtrAdd, p, offset, Flags::NONE);
1189 let read = build.load(i32_, at, plain(), Flags::NONE);
1190 let total = build.binary(Opcode::Add, acc, read, Flags::NONE);
1191 let one = build.iconst(i32_, 1);
1192 let next = build.binary(Opcode::Add, i, one, Flags::NSW);
1193 build.jump(head, &[next, total]);
1194
1195 let mut build = Builder::new(&mut func, exit);
1196 build.ret(&[acc]);
1197 module.add_func(func);
1198 (names, module)
1199 }
1200
1201 /// Memory with nothing said about it, which is what a plain subscript reads through.
1202 fn plain() -> MemInfo {
1203 MemInfo {
1204 size: 4,
1205 align: 4,
1206 order: MemOrder::NotAtomic,
1207 tbaa: None,
1208 owns: 0,
1209 restrict: Restrict::NONE,
1210 }
1211 }
1212
1213 /// Every addition in the module whose right operand is the constant zero.
1214 fn adds_of_zero(module: &Module) -> usize {
1215 let mut found = 0;
1216 for id in module.funcs() {
1217 let func = &module[id];
1218 for block in func.blocks() {
1219 for inst in func.insts(block) {
1220 if !matches!(func[inst].opcode, Opcode::Add | Opcode::PtrAdd) {
1221 continue;
1222 }
1223 let args = &func[func[inst].args];
1224 let Some(&rhs) = args.get(1) else { continue };
1225 let rucc_ir::Def::Result { inst: from, .. } = func[rhs].def else { continue };
1226 if func[from].opcode != Opcode::IConst {
1227 continue;
1228 }
1229 let Extra::Imm(at) = func[from].extra else { continue };
1230 found += usize::from(func[at].signed(func[rhs].ty) == 0);
1231 }
1232 }
1233 }
1234 found
1235 }
1236
1237 /// An index the unroller worked out to zero does not leave the addition behind.
1238 ///
1239 /// The peephole is what removes it and the peephole used to run only near the top of the
1240 /// list, before the unroller had made any of these. Folding writes the constant down and
1241 /// leaves the addition, so an `add x, 0` reached the selector and was written out as an
1242 /// `addq $0` the machine runs for nothing. tamnd/rucc#875.
1243 #[test]
1244 fn an_index_folded_to_zero_is_not_added_to_anything() {
1245 let (mut names, mut module) = a_short_loop();
1246 assert_eq!(adds_of_zero(&module), 0, "the fixture already has one before anything runs");
1247 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1248 assert!(report.broke.is_empty(), "{:?}", report.broke);
1249 assert!(spent(&report, "unroll").is_some_and(|it| it > 0), "the loop was not unrolled");
1250 assert_eq!(adds_of_zero(&module), 0, "{}", rucc_ir::print(&module, &names));
1251 }
1252
1253 #[test]
1254 fn every_pass_a_pipeline_names_is_a_pass_that_exists() {
1255 for level in
1256 [OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
1257 {
1258 for name in for_level(level) {
1259 assert!(
1260 pass::find(name).is_some(),
1261 "{level} names `{name}` and no pass answers to it"
1262 );
1263 }
1264 }
1265 }
1266
1267 #[test]
1268 fn a_pass_a_pipeline_names_twice_is_never_named_twice_in_a_row() {
1269 // Running a pass again after another pass has been through is the point of naming it
1270 // twice, and `simplify` around `narrow` is why the rule that used to be here, which was
1271 // that no level names a pass twice at all, is not the rule any more. Two runs with
1272 // nothing between them is still a mistake: the second one sees exactly what the first
1273 // one finished with, so it can only report that it found nothing.
1274 for level in
1275 [OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
1276 {
1277 for pair in for_level(level).windows(2) {
1278 assert_ne!(pair[0], pair[1], "{level} runs `{}` twice in a row", pair[0]);
1279 }
1280 }
1281 }
1282
1283 #[test]
1284 fn a_pass_the_pipeline_runs_twice_gets_one_allowance_and_reports_one_number() {
1285 // `-fpass-fuel=<pass>=<n>` is halved to find one rewrite, so the number in the flag has
1286 // to be the number of rewrites that happened however many times the list names the pass.
1287 // The peephole is named more than once from `-O1` up and the function below holds two
1288 // identities it takes, so a cap of one has to stop after one rather than after one per
1289 // occurrence.
1290 assert!(for_level(OptLevel::O2).iter().filter(|it| **it == "simplify").count() > 1);
1291
1292 let (mut names, mut module) = identities();
1293 let free = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1294 assert_eq!(spent(&free, "simplify"), Some(2), "{:?}", free.spent);
1295
1296 let (mut names, mut module) = identities();
1297 let mut opts = Options::for_level(OptLevel::O2);
1298 opts.fuel.insert("simplify".to_owned(), 1);
1299 let capped = super::run(&mut module, &mut names, &opts);
1300 assert_eq!(capped.spent.iter().filter(|(name, _)| *name == "simplify").count(), 1);
1301 assert_eq!(spent(&capped, "simplify"), Some(1), "{:?}", capped.spent);
1302 }
1303
1304 #[test]
1305 fn an_identity_only_the_narrow_pass_can_produce_is_still_taken() {
1306 // Issue 505, and the reason the peephole is named on both sides of `narrow`. C promotes
1307 // before it operates, so `unsigned char x; (unsigned char)(x & 255)` arrives here as a
1308 // thirty two bit `and` of a zero extension, and the rule that says `and` with every bit
1309 // set is the value has nothing at eight bits to match. `narrow` is the only producer that
1310 // width has. Before this ran twice the `and.i8` below reached the back end untouched.
1311 let mut names = Interner::new();
1312 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1313 let mut module = Module::new(names.intern("test.c"), &target);
1314 let (i8_, i32_) = (Type::int(8), Type::int(32));
1315 let mut func =
1316 Func::new(names.intern("f"), Signature::new().with_params(&[i8_]).with_returns(&[i8_]));
1317 let entry = func.create_block();
1318 let x = func.append_param(entry, i8_);
1319 let mut build = Builder::new(&mut func, entry);
1320 let wide = build.unary(Opcode::ZExt, x, i32_);
1321 let mask = build.iconst(i32_, 255);
1322 let kept = build.binary(Opcode::And, wide, mask, Flags::NONE);
1323 let back = build.unary(Opcode::Trunc, kept, i8_);
1324 build.ret(&[back]);
1325 module.add_func(func);
1326
1327 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1328 assert!(report.broke.is_empty(), "{:?}", report.broke);
1329 let text = rucc_ir::print(&module, &names);
1330 assert!(!text.contains("and."), "the masking survived the pipeline\n{text}");
1331 }
1332
1333 /// What a pass spent, or `None` if it did not run.
1334 fn spent(report: &super::Report, pass: &str) -> Option<u32> {
1335 report.spent.iter().find(|(name, _)| *name == pass).map(|&(_, count)| count)
1336 }
1337
1338 /// The names of the passes a set of options would run, in order.
1339 fn names(opts: &Options) -> Vec<&'static str> {
1340 opts.passes().into_iter().map(Pass::name).collect()
1341 }
1342
1343 #[test]
1344 fn every_level_that_splits_a_loop_looks_at_what_the_split_wrote() {
1345 // A guard goes in the preheader of the loop being split, which for an inner loop is inside
1346 // the loops around it, and it asks the runtime how big an object is. Nothing after `split`
1347 // moves anything, so a level that splits and then stops leaves those queries where they
1348 // cost the most.
1349 for level in [super::O1, super::O2, super::O3, super::OS, super::OZ] {
1350 let Some(at) = level.iter().position(|pass| *pass == "split") else {
1351 continue;
1352 };
1353 assert!(
1354 level[at..].contains(&"licm"),
1355 "a level splits a loop and never looks at the guard again"
1356 );
1357 }
1358 }
1359
1360 #[test]
1361 fn every_level_that_chooses_induction_variables_takes_the_old_one_away_afterwards() {
1362 // The counter a loop stops asking anything is still incremented round it, and what removes
1363 // the parameter carrying it is `simplify-cfg` rather than `dce`. See the comment on `O2`.
1364 // A level that chooses and then stops keeps both variables and is worse off than if it had
1365 // never chosen at all.
1366 for level in [super::O1, super::O2, super::O3, super::OS, super::OZ] {
1367 let Some(at) = level.iter().position(|pass| *pass == "ivopts") else {
1368 continue;
1369 };
1370 assert!(
1371 level[at + 1..].contains(&"simplify-cfg"),
1372 "a level chooses induction variables and leaves the one it stopped using behind"
1373 );
1374 }
1375 }
1376
1377 #[test]
1378 fn every_run_of_the_pass_that_reads_a_summary_is_named_as_one_that_does() {
1379 // A run left off the list gets no table of globals and no caller guarantees, and answers
1380 // that there were none rather than that nobody built them.
1381 for pass in pass::PASSES {
1382 let name = pass.name();
1383 assert_eq!(
1384 name.starts_with("discharge"),
1385 super::READS_SUMMARIES.contains(&name),
1386 "`{name}` and READS_SUMMARIES disagree about whether it reads a summary"
1387 );
1388 }
1389 }
1390
1391 #[test]
1392 fn the_level_that_optimizes_nothing_still_removes_what_nothing_reaches() {
1393 // Two passes at `-O0`, and neither of them is an optimization. See the comment on the
1394 // level itself, and issue 359.
1395 assert_eq!(names(&Options::for_level(OptLevel::O0)), ["expect", "simplify-cfg"]);
1396 assert!(names(&Options::for_level(OptLevel::O2)).len() > 1);
1397 }
1398
1399 #[test]
1400 fn a_pass_is_removed_by_no_and_added_by_the_bare_name_and_the_last_word_wins() {
1401 let mut opts = Options::for_level(OptLevel::O2);
1402 opts.toggles.push(("fold".to_owned(), false));
1403 assert!(!names(&opts).contains(&"fold"), "{:?}", names(&opts));
1404 opts.toggles.push(("fold".to_owned(), true));
1405 assert!(names(&opts).contains(&"fold"), "{:?}", names(&opts));
1406
1407 let mut off = Options::for_level(OptLevel::O0);
1408 off.toggles.push(("fold".to_owned(), true));
1409 assert_eq!(
1410 names(&off),
1411 ["expect", "simplify-cfg", "fold"],
1412 "a pass the level did not choose is still reachable"
1413 );
1414 }
1415
1416 #[test]
1417 fn asking_for_a_pass_twice_does_not_run_it_twice() {
1418 let mut opts = Options::for_level(OptLevel::O2);
1419 let before = names(&opts);
1420 opts.toggles.push(("fold".to_owned(), true));
1421 assert_eq!(names(&opts), before);
1422 }
1423
1424 #[test]
1425 fn the_pipeline_listing_names_the_level_and_every_pass_in_order() {
1426 let text = super::print(&Options::for_level(OptLevel::O2));
1427 assert!(text.starts_with("level: -O2\n"), "{text}");
1428 assert!(text.contains("1: expect, "), "{text}");
1429 assert!(text.contains("2: fold, "), "{text}");
1430 // Turning off everything the level asked for leaves the one pass that cannot be turned
1431 // off, since the back end has no rule for what it removes. See `Pass::required`.
1432 let mut none = Options::for_level(OptLevel::O0);
1433 none.toggles.push(("expect".to_owned(), false));
1434 none.toggles.push(("simplify-cfg".to_owned(), false));
1435 let none = super::print(&none);
1436 assert!(none.contains("1: expect, "), "{none}");
1437 assert!(!none.contains("simplify-cfg"), "{none}");
1438 }
1439
1440 #[test]
1441 fn running_the_pipeline_changes_the_module_and_reports_what_it_spent() {
1442 let (mut names, mut module) = module();
1443 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1444 // Folding rewrites the sign extension into a constant, and then the constant it was
1445 // extending is read by nothing and dead code elimination takes it out. One
1446 // transformation each, which is what the two of them together are for. Asserted by
1447 // name rather than as the whole vector, so a pass added later does not fail this.
1448 assert_eq!(spent(&report, "fold"), Some(1));
1449 assert_eq!(spent(&report, "dce"), Some(1));
1450 assert!(report.broke.is_empty(), "{:?}", report.broke);
1451 assert!(report.dumps.is_empty(), "nothing asked for a dump");
1452 assert!(rucc_ir::print(&module, &names).contains("iconst.i64 7"));
1453 }
1454
1455 #[test]
1456 fn the_analyses_survive_a_pass_that_keeps_them_and_not_one_that_does_not() {
1457 // The pipeline half of the analysis manager. A branch on a constant, so `simplify-cfg`
1458 // has something to do and says it preserved nothing, and the whole run comes out with
1459 // the verifier and the manager both satisfied. What a pass that lied would produce is in
1460 // `crate::analysis`, where a lie can be told on purpose.
1461 let mut names = Interner::new();
1462 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1463 let mut module = Module::new(names.intern("test.c"), &target);
1464 let mut func = Func::new(names.intern("f"), Signature::new());
1465 let entry = func.create_block();
1466 let dead = func.create_block();
1467 let exit = func.create_block();
1468 let mut build = Builder::new(&mut func, entry);
1469 let never = build.iconst(Type::int(1), 0);
1470 build.br_if(never, dead, &[], exit, &[]);
1471 for block in [dead, exit] {
1472 let mut build = Builder::new(&mut func, block);
1473 build.ret(&[]);
1474 }
1475 module.add_func(func);
1476 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1477 // The fold, and then the merge of the arm it left with one way into it.
1478 assert_eq!(spent(&report, "simplify-cfg"), Some(2));
1479 assert!(report.broke.is_empty(), "{:?}", report.broke);
1480 let text = rucc_ir::print(&module, &names);
1481 // The labels, which start a line, and not the mentions of one, which are indented. One
1482 // left: the arm nothing reaches went, and the arm that is always taken came up into the
1483 // entry, which is what is left of the branch.
1484 assert_eq!(text.matches("\nblock").count(), 1, "there is more than one block:\n{text}");
1485 }
1486
1487 #[test]
1488 fn no_pass_that_optimizes_runs_at_no_optimization_however_much_there_is_to_do() {
1489 let (mut names, mut module) = module();
1490 let before = rucc_ir::print(&module, &names);
1491 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O0));
1492 // The two passes the level runs looked, found no `__builtin_expect`, no branch they could
1493 // read and no block nothing reaches, and spent nothing. The constant arithmetic the fixture
1494 // is full of is still there, which is the part of `-O0` that has not changed.
1495 assert_eq!(report.spent, vec![("expect", 0), ("simplify-cfg", 0)]);
1496 assert_eq!(rucc_ir::print(&module, &names), before);
1497 }
1498
1499 #[test]
1500 fn a_gate_takes_a_pass_away_from_one_function_and_leaves_the_other_alone() {
1501 let (mut names, mut module) = two_functions();
1502 let mut opts = Options::for_level(OptLevel::O2);
1503 opts.gates.add(false, "fold=g").expect("g is a function and fold is a pass");
1504 let report = super::run(&mut module, &mut names, &opts);
1505 assert!(spoke_about(&report, "fold", "f", &names));
1506 assert!(!spoke_about(&report, "fold", "g", &names), "fold ran where it was gated off");
1507 assert!(spoke_about(&report, "dce", "g", &names), "one pass gated off is not all of them");
1508 // What the gate is for: the two functions came out different, and the difference is one
1509 // pass on one function rather than a level on a file.
1510 let text = rucc_ir::print(&module, &names);
1511 assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
1512 }
1513
1514 #[test]
1515 fn a_function_can_be_gated_by_the_number_it_has_in_the_module() {
1516 let (mut names, mut module) = two_functions();
1517 let mut opts = Options::for_level(OptLevel::O2);
1518 opts.gates.add(false, "fold=0").expect("0 is a function and fold is a pass");
1519 let report = super::run(&mut module, &mut names, &opts);
1520 assert!(!spoke_about(&report, "fold", "f", &names), "function 0 is the first one");
1521 assert!(spoke_about(&report, "fold", "g", &names));
1522 }
1523
1524 #[test]
1525 fn enabling_a_pass_reaches_one_function_at_a_level_that_did_not_ask_for_it() {
1526 let (mut names, mut module) = two_functions();
1527 let mut opts = Options::for_level(OptLevel::O0);
1528 opts.gates.add(true, "fold=1").expect("1 is a function and fold is a pass");
1529 let running: Vec<&str> = opts.passes().into_iter().map(Pass::name).collect();
1530 assert_eq!(
1531 running,
1532 ["expect", "simplify-cfg", "fold"],
1533 "the flag has to put the pass in the pipeline"
1534 );
1535 let report = super::run(&mut module, &mut names, &opts);
1536 assert!(!spoke_about(&report, "fold", "f", &names), "nothing asked for f");
1537 assert!(spoke_about(&report, "fold", "g", &names));
1538 let text = rucc_ir::print(&module, &names);
1539 assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
1540 }
1541
1542 #[test]
1543 fn a_pass_gated_off_everywhere_runs_on_nothing_and_still_says_so() {
1544 let (mut names, mut module) = two_functions();
1545 let before = rucc_ir::print(&module, &names);
1546 let mut opts = Options::for_level(OptLevel::O2);
1547 for pass in pass::PASSES {
1548 opts.gates.add(false, pass.name()).expect("a pass in the list is a pass that exists");
1549 }
1550 let report = super::run(&mut module, &mut names, &opts);
1551 assert!(report.remarks.is_empty(), "a pass that did not run has nothing to report");
1552 assert_eq!(spent(&report, "fold"), Some(0), "the pass is still in the pipeline");
1553 assert_eq!(rucc_ir::print(&module, &names), before);
1554 }
1555
1556 #[test]
1557 fn the_pipeline_listing_says_which_passes_a_gate_touched() {
1558 let mut opts = Options::for_level(OptLevel::O2);
1559 // `narrow` rather than `fold`, because a gate names a pass and the level runs some of its
1560 // passes more than once. A note on one of those is printed against every run of it, and
1561 // the count at the bottom would then be counting repeats rather than what it is asking.
1562 opts.gates.add(false, "narrow=2-4").expect("narrow is a pass");
1563 let text = super::print(&opts);
1564 assert!(text.contains("6: narrow, "), "{text}");
1565 assert!(text.contains("[off for 2-4]"), "{text}");
1566 assert_eq!(text.matches('[').count(), 1, "a pass no gate mentions says nothing extra");
1567 }
1568
1569 #[test]
1570 fn every_pass_at_no_fuel_leaves_the_module_exactly_as_it_found_it() {
1571 // The check section 9.10 asks for by name, and the reason it is here rather than in each
1572 // pass is that it has to hold for every pass that is ever added.
1573 for pass in pass::PASSES {
1574 let (mut names, mut module) = module();
1575 let before = rucc_ir::print(&module, &names);
1576 let mut opts = Options::for_level(OptLevel::O0);
1577 // The level's own passes out of the way first, so that what this measures is the one
1578 // pass under test. A pass turned off and then on again is on, so this is right for
1579 // those passes as well as for the others. `expect` cannot be turned off, so it is
1580 // starved of fuel instead and is expected in the report ahead of the pass under test.
1581 opts.toggles.push(("simplify-cfg".to_owned(), false));
1582 opts.toggles.push((pass.name().to_owned(), true));
1583 opts.fuel.insert("expect".to_owned(), 0);
1584 opts.fuel.insert(pass.name().to_owned(), 0);
1585 let report = super::run(&mut module, &mut names, &opts);
1586 let mut want = vec![("expect", 0)];
1587 if pass.name() != "expect" {
1588 want.push((pass.name(), 0));
1589 }
1590 assert_eq!(report.spent, want, "{} spent fuel it had none of", pass.name());
1591 assert_eq!(
1592 rucc_ir::print(&module, &names),
1593 before,
1594 "{} transformed the module at fuel zero",
1595 pass.name()
1596 );
1597 }
1598 }
1599
1600 #[test]
1601 fn fuel_is_shared_across_the_functions_of_a_module() {
1602 let mut names = Interner::new();
1603 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1604 let mut module = Module::new(names.intern("test.c"), &target);
1605 for which in ["f", "g"] {
1606 let mut func =
1607 Func::new(names.intern(which), Signature::new().with_returns(&[Type::int(64)]));
1608 let block = func.create_block();
1609 let mut build = Builder::new(&mut func, block);
1610 let narrow = build.iconst(Type::int(32), 7);
1611 let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
1612 build.ret(&[wide]);
1613 module.add_func(func);
1614 }
1615 let mut opts = Options::for_level(OptLevel::O2);
1616 opts.fuel.insert("fold".to_owned(), 1);
1617 let report = super::run(&mut module, &mut names, &opts);
1618 // One fold across both functions, because fuel is per pass and per compilation. Dead
1619 // code elimination has its own and spends it on the constant the one fold orphaned.
1620 assert_eq!(spent(&report, "fold"), Some(1));
1621 assert_eq!(spent(&report, "dce"), Some(1));
1622 let text = rucc_ir::print(&module, &names);
1623 assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
1624 }
1625
1626 #[test]
1627 fn global_fuel_is_spent_by_the_passes_in_order_and_the_rest_get_none() {
1628 let (mut names, mut module) = module();
1629 let mut opts = Options::for_level(OptLevel::O2);
1630 opts.global_fuel = Some(1);
1631 let report = super::run(&mut module, &mut names, &opts);
1632 // Folding is first and there is one thing to fold, so it takes the one unit and dead
1633 // code elimination gets nothing. Without the budget it would have taken the constant
1634 // that fold orphaned, which is what the other test measures.
1635 assert_eq!(spent(&report, "fold"), Some(1));
1636 assert_eq!(spent(&report, "dce"), Some(0));
1637 let text = rucc_ir::print(&module, &names);
1638 assert!(text.contains("iconst.i64 7"), "{text}");
1639 assert!(text.contains("iconst.i32 7"), "the orphaned constant is still there, {text}");
1640 }
1641
1642 #[test]
1643 fn a_budget_of_nothing_leaves_the_module_alone_and_still_runs_every_pass() {
1644 let (mut names, mut module) = module();
1645 let before = rucc_ir::print(&module, &names);
1646 let mut opts = Options::for_level(OptLevel::O2);
1647 opts.global_fuel = Some(0);
1648 let report = super::run(&mut module, &mut names, &opts);
1649 assert_eq!(rucc_ir::print(&module, &names), before);
1650 assert!(report.spent.iter().all(|(_, spent)| *spent == 0), "{:?}", report.spent);
1651 // Every pass, because a pass out of fuel is a pass that ran and did nothing rather than
1652 // a pass that was skipped, and a bisection that skipped passes would be searching a
1653 // different pipeline at every step. One line per name rather than one per place the list
1654 // names it, because what a name was given is one allowance across all of them.
1655 let mut want: Vec<&str> = opts.passes().into_iter().map(Pass::name).collect();
1656 // And the three transformations that are not in that list, because they are a module at a
1657 // time rather than one function at a time. They spend out of the same budget and are
1658 // bisected the same way, so they belong in the same accounting.
1659 want.push(ipcp::NAME);
1660 want.push(ipasra::NAME);
1661 want.push(libcall::NAME);
1662 want.sort_unstable();
1663 want.dedup();
1664 let mut got: Vec<&str> = report.spent.iter().map(|&(name, _)| name).collect();
1665 got.sort_unstable();
1666 assert_eq!(got, want);
1667 }
1668
1669 #[test]
1670 fn the_module_at_a_time_removal_is_on_at_the_level_and_off_when_the_flag_says_so() {
1671 let mut opts = Options::for_level(OptLevel::O2);
1672 assert!(opts.wants(ipasra::NAME));
1673 opts.toggles.push((ipasra::NAME.to_owned(), false));
1674 assert!(!opts.wants(ipasra::NAME));
1675 }
1676
1677 #[test]
1678 fn the_module_at_a_time_propagation_is_on_at_the_level_and_off_when_the_flag_says_so() {
1679 // The same reading of a toggle that [`Options::chosen`] gives the pass list, done by hand
1680 // because what this names is not a pass.
1681 let mut opts = Options::for_level(OptLevel::O2);
1682 assert!(opts.wants(ipcp::NAME));
1683 opts.toggles.push((ipcp::NAME.to_owned(), false));
1684 assert!(!opts.wants(ipcp::NAME));
1685 opts.toggles.push((ipcp::NAME.to_owned(), true));
1686 assert!(opts.wants(ipcp::NAME), "the last word on the command line is the one that wins");
1687 }
1688
1689 #[test]
1690 fn the_tighter_of_the_two_limits_is_the_one_that_stops_the_pass() {
1691 // A pass allowed more than the budget gets the budget.
1692 let (mut names, mut under) = module();
1693 let mut opts = Options::for_level(OptLevel::O2);
1694 opts.global_fuel = Some(0);
1695 opts.fuel.insert("fold".to_owned(), 9);
1696 assert_eq!(spent(&super::run(&mut under, &mut names, &opts), "fold"), Some(0));
1697
1698 // And a pass allowed less than the budget keeps its own limit, with the budget left
1699 // over for whatever comes after it.
1700 let (mut names, mut over) = module();
1701 let mut opts = Options::for_level(OptLevel::O2);
1702 opts.global_fuel = Some(9);
1703 opts.fuel.insert("fold".to_owned(), 0);
1704 let report = super::run(&mut over, &mut names, &opts);
1705 assert_eq!(spent(&report, "fold"), Some(0));
1706 assert_eq!(spent(&report, "dce"), Some(0), "nothing was orphaned for it to remove");
1707 }
1708
1709 #[test]
1710 fn the_pipeline_listing_says_when_there_is_a_budget_and_says_nothing_when_there_is_not() {
1711 let opts = Options::for_level(OptLevel::O2);
1712 assert!(!super::print(&opts).contains("global fuel"));
1713 let with = Options { global_fuel: Some(12), ..Options::for_level(OptLevel::O2) };
1714 assert!(super::print(&with).contains("global fuel: 12"), "{}", super::print(&with));
1715 }
1716
1717 #[test]
1718 fn a_dump_is_taken_on_the_side_that_asked_for_it_and_not_the_other() {
1719 let (mut names, mut module) = module();
1720 let mut opts = Options::for_level(OptLevel::O2);
1721 opts.dumps.add("after-fold").expect("a pass that exists");
1722 let report = super::run(&mut module, &mut names, &opts);
1723 // The level folds three times, twice at the top on either side of `image` and once after
1724 // the loop pipeline, and what a dump request names is a pass rather than a position, so
1725 // every run is written out. The side is what this is about: not one of the three is a
1726 // `before`.
1727 assert_eq!(report.dumps.len(), 3, "every run of the pass, one dump each");
1728 assert!(
1729 report.dumps.iter().all(|dump| dump.name.ends_with("-after-fold")),
1730 "{:?}",
1731 report.dumps.iter().map(|dump| &dump.name).collect::<Vec<&String>>()
1732 );
1733 assert_eq!(report.dumps[0].name, "01-after-fold");
1734 assert!(report.dumps[0].text.contains("iconst.i64 7"));
1735 }
1736
1737 #[test]
1738 fn asking_for_all_dumps_gives_both_sides_of_every_pass() {
1739 let (mut interner, mut module) = module();
1740 let opts = {
1741 let mut opts = Options::for_level(OptLevel::O2);
1742 opts.dumps.add("all").expect("all is always a dump");
1743 opts
1744 };
1745 let report = super::run(&mut module, &mut interner, &opts);
1746 // Both sides of every pass in the level, numbered by position, whatever the level
1747 // holds. Written out of the pipeline rather than as a literal, because the point of
1748 // the test is the pairing and the numbering and not which passes exist this month.
1749 let taken: Vec<&str> = report.dumps.iter().map(|d| d.name.as_str()).collect();
1750 let expected: Vec<String> = names(&opts)
1751 .into_iter()
1752 .enumerate()
1753 .flat_map(|(at, name)| {
1754 [format!("{at:02}-before-{name}"), format!("{at:02}-after-{name}")]
1755 })
1756 .collect();
1757 assert_eq!(taken, expected);
1758 // Either side of the fold, which is the pass that has something to do to this fixture,
1759 // found by name rather than by position so that a pass in front of it does not move it.
1760 let side = |which: &str| {
1761 let tail = format!("-{which}-fold");
1762 let dump = report.dumps.iter().find(|dump| dump.name.ends_with(&tail));
1763 dump.expect("the level folds").text.clone()
1764 };
1765 assert!(side("before").contains("sext.i64"));
1766 assert!(!side("after").contains("sext.i64"));
1767 }
1768
1769 #[test]
1770 fn every_pass_leaves_a_record_for_every_function_whether_or_not_it_had_anything_to_say() {
1771 let (mut names, mut module) = module();
1772 let opts = Options::for_level(OptLevel::O2);
1773 let report = super::run(&mut module, &mut names, &opts);
1774 let ran: Vec<&'static str> = opts.passes().into_iter().map(Pass::name).collect();
1775 // One function in the fixture, so one record per pass, and the passes in the order they
1776 // ran. A pass that found nothing is in here with an empty record, which is the point:
1777 // a pass that fires on nothing is either dead code or a bug, and output that leaves it
1778 // out cannot say which.
1779 let seen: Vec<&'static str> = report.remarks.iter().map(|it| it.pass).collect();
1780 assert_eq!(seen, ran);
1781 assert!(report.remarks.iter().all(|it| names.resolve(it.func) == "f"));
1782 assert!(
1783 report.remarks.iter().any(|it| it.pass == "simplify" && it.stats.is_empty()),
1784 "there is nothing in the fixture for the peephole to do"
1785 );
1786 }
1787
1788 #[test]
1789 fn a_pass_spends_one_unit_of_fuel_for_each_rewrite_it_reports() {
1790 // The invariant that keeps the record honest, checked over every pass rather than
1791 // written into each one. Fuel is taken immediately before a transformation and a
1792 // rewrite is recorded immediately after it, so the two counts are the same number
1793 // arrived at from two directions. A pass where they disagree either transformed without
1794 // asking, which breaks bisection, or rewrote without recording, which means the manager
1795 // did not run the verifier over what it produced.
1796 let (mut names, mut module) = module();
1797 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1798 for (pass, spent) in &report.spent {
1799 assert_eq!(
1800 report.totals(pass).total(Kind::Optimized),
1801 *spent,
1802 "{pass} spent {spent} units of fuel and did not say on what"
1803 );
1804 }
1805 assert!(report.spent.iter().any(|(_, spent)| *spent > 0), "nothing happened at all");
1806 }
1807
1808 #[test]
1809 fn what_the_passes_said_is_what_opt_info_prints() {
1810 let (mut names, mut module) = module();
1811 let report = super::run(&mut module, &mut names, &Options::for_level(OptLevel::O2));
1812 let text = crate::optinfo::render("t.c", &report, &names, crate::Wants::all());
1813 assert!(
1814 text.contains(
1815 "t.c: f: optimized: instruction with constant operands folded to a constant (1) [fold]"
1816 ),
1817 "{text}"
1818 );
1819 assert!(
1820 text.contains(
1821 "t.c: f: optimized: instruction with no effects and no users removed (1) [dce]"
1822 ),
1823 "{text}"
1824 );
1825 // Nothing in the fixture is a miss, so asking only for the misses gets nothing back,
1826 // and that is different from the flag having been left off.
1827 let mut misses = crate::Wants::none();
1828 misses.add("missed").expect("that kind exists");
1829 assert_eq!(crate::optinfo::render("t.c", &report, &names, misses), "");
1830 }
1831
1832 #[test]
1833 fn the_verifier_says_which_function_it_refused_and_leaves_the_others_out_of_it() {
1834 // Two functions with the same foldable body, and a block in the second one that nothing
1835 // reaches, which the verifier refuses. The pass is not what put it there, and the
1836 // complaint says the pass anyway, because a pass that hands back a function the
1837 // verifier will not take is where the search has to start whoever wrote the block.
1838 let mut names = Interner::new();
1839 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1840 let mut module = Module::new(names.intern("test.c"), &target);
1841 module.add_func(foldable(&mut names, "f"));
1842 let mut g = foldable(&mut names, "g");
1843 let stranded = g.create_block();
1844 let mut build = Builder::new(&mut g, stranded);
1845 let seven = build.iconst(Type::int(64), 7);
1846 build.ret(&[seven]);
1847 module.add_func(g);
1848
1849 // Folding on its own, because simplify-CFG would take the stranded block out and there
1850 // would be nothing left to complain about.
1851 let mut opts = Options::for_level(OptLevel::O0);
1852 opts.toggles.push(("simplify-cfg".to_owned(), false));
1853 opts.toggles.push(("fold".to_owned(), true));
1854 opts.verify = true;
1855 let report = super::run(&mut module, &mut names, &opts);
1856
1857 assert_eq!(report.broke.len(), 1, "{:?}", report.broke);
1858 let complaint = &report.broke[0];
1859 assert!(complaint.starts_with("the fold pass left invalid IR in g,"), "{complaint}");
1860 assert!(complaint.contains("this block is not reachable"), "{complaint}");
1861 }
1862
1863 #[test]
1864 fn a_function_a_pass_did_not_change_is_not_verified_after_it() {
1865 // The stranded block is in `f` this time and `f` has nothing to fold, so the pass runs
1866 // over an invalid function, changes nothing, and says nothing. That is the whole trade:
1867 // the verifier answers for the rewrite that just happened, and a function no rewrite
1868 // touched was already answered for when it was built.
1869 let mut names = Interner::new();
1870 let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1871 let mut module = Module::new(names.intern("test.c"), &target);
1872 let mut f = Func::new(names.intern("f"), Signature::new().with_returns(&[Type::int(64)]));
1873 for _ in 0..2 {
1874 let block = f.create_block();
1875 let mut build = Builder::new(&mut f, block);
1876 let seven = build.iconst(Type::int(64), 7);
1877 build.ret(&[seven]);
1878 }
1879 module.add_func(f);
1880 module.add_func(foldable(&mut names, "g"));
1881
1882 let mut opts = Options::for_level(OptLevel::O0);
1883 opts.toggles.push(("simplify-cfg".to_owned(), false));
1884 opts.toggles.push(("fold".to_owned(), true));
1885 opts.verify = true;
1886 let report = super::run(&mut module, &mut names, &opts);
1887
1888 assert!(report.broke.is_empty(), "{:?}", report.broke);
1889 // And it did run on it, so this is the verifier staying quiet rather than the pass
1890 // being skipped.
1891 assert!(spoke_about(&report, "fold", "f", &names));
1892 }
1893
1894 #[test]
1895 fn a_dump_of_a_pass_that_does_not_exist_is_refused_rather_than_ignored() {
1896 let mut dumps = Dumps::default();
1897 assert!(dumps.add("after-no-such-pass").is_err());
1898 assert!(dumps.add("sideways-fold").is_err());
1899 assert!(dumps.add("fold").is_err());
1900 assert!(dumps.is_empty());
1901 }
1902}