Skip to main content

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 _;
35
36use rucc_base::{Interner, Symbol};
37use rucc_ir::{FuncId, Module};
38use rucc_session::OptLevel;
39
40use crate::{
41    Analyses, Fuel, Gates, Machine, Pass, Preserved, Stats, extents, heap, nofree, params, pass,
42};
43
44/// The passes that read a summary [`nofree::annotate`], [`extents::annotate`],
45/// [`params::annotate`] or [`heap::annotate`] writes onto the IR.
46///
47/// A list rather than one name because there will be more of them: section 7.5 asks for three more
48/// summary fields and section 7.3's lifetime elimination is the next thing to want this one. A pass
49/// that reads a summary and is not named here reads whatever the last build left, which is nothing,
50/// so the cost of forgetting to add a name is a missed optimization.
51const READS_SUMMARIES: &[&str] = &["discharge"];
52
53/// `-O0`. One pass, and it is not an optimization. Section 9.1 gives this level SSA
54/// construction, which the lowering walk in `spec/08-ir.md` already does, and mem2reg for the
55/// allocas that are left, which is the next pass to be written.
56///
57/// `simplify-cfg` is here because a branch on a condition that is a constant is not a missed
58/// optimization, it is a call to a function the program never calls, and a program that calls a
59/// function it never calls is one that does not link. That is issue 359, gcc removes the code at
60/// every level including this one, and a `-O0` that emitted it would be a `-O0` some correct
61/// programs cannot be built at. Nothing else runs, and no analysis beyond the graph the pass
62/// reads reachability out of is computed.
63const O0: &[&str] = &["simplify-cfg"];
64
65/// `-O1`. Section 9.1 asks for one e-graph round, conservative inlining, simplify-CFG, SROA,
66/// GVN, DCE, LICM and the loop canonicalizations. Folding, control flow simplification and dead
67/// code elimination are the part of that which exists, with the peephole among them. They run in
68/// that order because folding and the peephole are what make most of the dead code there is to
69/// eliminate, because a constant a fold produced is a branch condition the control flow pass can
70/// then read, and because the comparison that branch was on is dead once it has.
71///
72/// The peephole runs on both sides of `narrow`, which is the one place in this list where a pass
73/// is named twice, so the reason is worth stating. The rewrite table is written at a width, and
74/// the widths below `int` are unreachable from C source: the integer promotions mean an addition
75/// of two `char` values arrives here as an `add.i32`, so a rule about `add.i8` matches nothing
76/// that a front end can produce. `narrow` is what puts the width back, and it is therefore the
77/// only producer the narrow half of the table has. Running the peephole only before it left
78/// sixty nine of the first hundred and twenty five rules unable to fire on any program, which is
79/// issue 505 and is what the corpus measured. Running it only after it would give up the smaller
80/// trees the peephole hands `narrow`, since a subtree `narrow` redoes has to have one reader and
81/// an identity left standing is a second one. Both sides costs one more walk over each function
82/// and is what the pass is for.
83///
84/// `phiopt` comes after `thread` and the order between them is not arbitrary. Both look at a
85/// diamond whose arms carry a value to a join. Where the join then branches on that value,
86/// threading removes a branch and costs nothing, and if-conversion would have turned the same
87/// shape into a `select` the join branches on instead, which is strictly worse. Threading first
88/// leaves if-conversion the diamonds whose value is used rather than tested, which are the ones it
89/// is for.
90///
91/// `prune` is between `phiopt` and `simplify-cfg` and both sides of that are load bearing. It reads
92/// document 10's ranges off the graph to find a branch that can only go one way and a switch case
93/// nothing can reach, so it has to run after the two passes that change the graph most. What it
94/// leaves is a jump where a branch was and a block nothing reaches, and `simplify-cfg` is the pass
95/// that takes those out, so it has to run before it rather than after.
96///
97/// `canon` is where document 26's loop pipeline opens, so it goes after the value level passes and
98/// before the cleanup. It gives every loop a preheader, one latch, exits of its own and loop closed
99/// form, which is what lets the loop passes that follow it write `insert at the end of the
100/// preheader` rather than each making one. On its own it generates nothing: the blocks it adds are
101/// empty and the parameters it adds have one argument each, and `simplify-cfg` runs straight after
102/// it and takes both back out to a fixed point. That is section 26.7's arrangement, and it is why
103/// the position matters more than the pass does until the loop passes land on top of it.
104///
105/// `header-copy` is section 26.7's third step and `canon` runs again after it, which is the same
106/// section's instruction to re-canonicalize the loops it changed. It has to: what the copy leaves
107/// is a loop entered from a block that branches two ways, and a block that branches two ways is not
108/// a preheader. Nothing between the two needs the properties, so the second run is bookkeeping
109/// against the loop passes that come later rather than something this level's output depends on,
110/// and `simplify-cfg` after it takes out the blocks and parameters both runs added that nothing
111/// used.
112///
113/// `licm` comes after the copy and the canonicalization behind it, and section 27.1 says why it has
114/// to. What it may move in front of a loop depends on what runs on every entry to the loop, and
115/// after that pair that is the whole body rather than the header alone. Running it before the
116/// copy would leave it the header, which is most of the pass's value gone. It is also the reason
117/// the copy exists, so the two are one arrangement read from either end.
118///
119/// It is in the three speed levels and not in `-Os` or `-Oz`. Moving a computation out of a loop
120/// does not remove one, so there are no bytes in it for a level whose cost model is size, and the
121/// one thing it can cost is a spill inside the loop, which is bytes. That trade is worth making for
122/// time and there is nothing on the other side of it for space.
123///
124/// `unroll` runs after `licm` and only at the two speed levels. After, because what it does is copy
125/// the body, and a computation licm has already moved in front of the loop is one the copies do not
126/// each get their own of. It needs the same shape licm does and for the same reason, a loop that
127/// tests at the bottom with a preheader in front of it, so it sits at the end of the same run of
128/// loop passes rather than anywhere of its own. `simplify-cfg` straight after it is what turns the
129/// chain of copies into one block, since each copy now ends in a jump to the next and a block with
130/// one way in and one way out is a block that goes away.
131///
132/// `hoist` is the first of the two check passes and it runs where it does because of what is above
133/// it. It needs a loop that tests at the bottom, which is what `header-copy` makes, and it needs a
134/// preheader to put a check in, which is what the `canon` after it puts back. Running it before
135/// `discharge` rather than after is deliberate as well: what it leaves in the preheader is a check
136/// over the whole range the loop sweeps, and that is a fact `discharge` can then use on anything
137/// else in front of the loop that is about the same bytes.
138///
139/// `discharge` is second to last, between `hoist` and `dce`, and both neighbours are the reason. It
140/// reads the dominator tree to find a safety check whose bytes an earlier check already covered, so
141/// it wants the graph after the block merging rather than before, when a straight run of code is
142/// still several blocks and a fact does not reach the check it would cover. What it leaves behind
143/// is the `cap_of` the check it removed was reading, which nothing now reads, so `dce` after it is
144/// what makes the function smaller rather than shorter by one instruction. It is in every level
145/// except `-O0`, which keeps every check on purpose: document 14 measures against a build where
146/// nothing was discharged, and that build is `-O0`.
147const O1: &[&str] = &[
148    "fold",
149    "simplify",
150    "narrow",
151    "simplify",
152    "thread",
153    "phiopt",
154    "prune",
155    "canon",
156    "header-copy",
157    "canon",
158    "licm",
159    "simplify-cfg",
160    "hoist",
161    "discharge",
162    "dce",
163];
164
165/// `-O2`. The level the code quality claim is about. Section 9.1 asks for two e-graph rounds
166/// around the loop pipeline, the full inlining cost model, Memory SSA and the full alias
167/// analysis stack, and then the scalar and machine passes on top.
168///
169/// `short-circuit` is the one pass here that `-O1` does not have, and section 22.5 is where the
170/// level comes from. It folds the two branches of an `a && b` into one, which costs the right
171/// operand's work on the path that was skipping it and buys a branch the machine no longer has to
172/// guess. That is a trade worth making when the aim is speed and the branch is hard to call, and
173/// it is not one to make by default, which is what `-O1` is.
174///
175/// It runs before `thread` and `phiopt` rather than after, and the order is not arbitrary. Both of
176/// those look at edges, and the collapse removes a block and turns two edges into one, so running
177/// it first hands them a smaller graph with nothing lost. The other way round, threading is free
178/// to give the second branch's block another predecessor, and a block two edges reach is one the
179/// collapse will not touch, so a chain that was foldable stops being foldable.
180const O2: &[&str] = &[
181    "fold",
182    "simplify",
183    "narrow",
184    "simplify",
185    "short-circuit",
186    "thread",
187    "phiopt",
188    "prune",
189    "canon",
190    "header-copy",
191    "canon",
192    "licm",
193    "unroll",
194    "simplify-cfg",
195    "hoist",
196    "discharge",
197    "dce",
198];
199
200/// `-O3`. `-O2` plus loop vectorization, larger inlining and unrolling thresholds, interchange
201/// and distribution where the dependence analysis is confident, and function specialization.
202const O3: &[&str] = &[
203    "fold",
204    "simplify",
205    "narrow",
206    "simplify",
207    "short-circuit",
208    "thread",
209    "phiopt",
210    "prune",
211    "canon",
212    "header-copy",
213    "canon",
214    "licm",
215    "unroll",
216    "simplify-cfg",
217    "hoist",
218    "discharge",
219    "dce",
220];
221
222/// `-Os`. `-O2`'s passes under a size cost model: inlining only where it shrinks, no unrolling
223/// and no vectorization.
224///
225/// The second peephole is here rather than cut for size, because every rule it can fire replaces
226/// a term with a strictly smaller one. Tier one of `spec/optimizer/13-rewrite-rules.md` is
227/// defined that way, so a level that wants smaller code wants more of it and not less.
228///
229/// `short-circuit` is the pass this level drops from `-O2`, for the mirror of that reason. What it
230/// removes is a branch, which is time, and what it adds is the right operand's instructions on a
231/// path that did not run them and an and on top. The code comes out no smaller and usually a byte
232/// or two larger, so a level whose cost model is size has nothing to gain from it.
233///
234/// `hoist` is dropped here as well, and the reason is the same trade read the other way.
235/// It takes a check out of a loop body and puts one in the preheader, plus the address arithmetic
236/// the new check needs, so the loop runs faster and the function is a few instructions larger. That
237/// is a speed transformation with a size cost, which is what `-Os` and `-Oz` are for declining.
238///
239/// `header-copy-small` is the same pass `-O1` and above run under section 26.6's smaller budget.
240/// The copy is code growth and this level pays for it once per loop, so five instructions is what
241/// it will pay. What it gets back is a body that is one region and an exit test at the bottom,
242/// which is slightly smaller in the steady state, so the trade is worth making at a limit that
243/// keeps the header small and not at one that copies twenty instructions to save two.
244const OS: &[&str] = &[
245    "fold",
246    "simplify",
247    "narrow",
248    "simplify",
249    "thread",
250    "phiopt",
251    "prune",
252    "canon",
253    "header-copy-small",
254    "canon",
255    "simplify-cfg",
256    "discharge",
257    "dce",
258];
259
260/// `-Oz`. `-Os` and additionally the outliner, with instruction selection preferring the smaller
261/// encoding wherever there is a choice.
262///
263/// Header copying is the pass this level drops from `-Os`, which section 26.6 asks for by name. It
264/// is the one loop canonicalization that makes the function bigger, `-Oz` is the level that would
265/// rather have the branch than the bytes, and every reason to want the do-while form here is a
266/// speed reason.
267const OZ: &[&str] = &[
268    "fold",
269    "simplify",
270    "narrow",
271    "simplify",
272    "thread",
273    "phiopt",
274    "prune",
275    "canon",
276    "simplify-cfg",
277    "discharge",
278    "dce",
279];
280
281/// The passes this level runs, before the command line adds to or removes from them.
282#[must_use]
283pub const fn for_level(level: OptLevel) -> &'static [&'static str] {
284    match level {
285        OptLevel::O0 => O0,
286        OptLevel::O1 => O1,
287        OptLevel::O2 => O2,
288        OptLevel::O3 => O3,
289        OptLevel::Os => OS,
290        OptLevel::Oz => OZ,
291    }
292}
293
294/// Which passes the IR is written out around.
295///
296/// Empty by default, which is the whole point: a dump is a debugging aid and writing files
297/// nobody asked for is not one.
298#[derive(Debug, Clone, Default, PartialEq, Eq)]
299pub struct Dumps {
300    /// Every pass, on both sides.
301    all: bool,
302    /// The passes to write out before.
303    before: Vec<String>,
304    /// The passes to write out after.
305    after: Vec<String>,
306}
307
308impl Dumps {
309    /// Adds one `-fdump-ir=` argument.
310    ///
311    /// # Errors
312    ///
313    /// When the argument is not `all`, `before-<pass>` or `after-<pass>`, or when it names a
314    /// pass this compiler does not have. A misspelled pass name that quietly dumped nothing
315    /// would look exactly like a pass that did not run.
316    pub fn add(&mut self, spec: &str) -> Result<(), String> {
317        if spec == "all" {
318            self.all = true;
319            return Ok(());
320        }
321        let (side, name) = match spec.split_once('-') {
322            Some(("before", name)) => (&mut self.before, name),
323            Some(("after", name)) => (&mut self.after, name),
324            _ => {
325                return Err(format!(
326                    "`{spec}` is not a dump this compiler makes, which are `all`, \
327                     `before-<pass>` and `after-<pass>`"
328                ));
329            }
330        };
331        if pass::find(name).is_none() {
332            return Err(format!("`{name}` is not a pass this compiler has, see --print-pipeline"));
333        }
334        side.push(name.to_owned());
335        Ok(())
336    }
337
338    /// Whether anything is dumped at all.
339    #[must_use]
340    pub fn is_empty(&self) -> bool {
341        !self.all && self.before.is_empty() && self.after.is_empty()
342    }
343
344    /// Whether the IR is written out before this pass runs.
345    #[must_use]
346    pub fn wants_before(&self, name: &str) -> bool {
347        self.all || self.before.iter().any(|it| it == name)
348    }
349
350    /// Whether the IR is written out after this pass runs.
351    #[must_use]
352    pub fn wants_after(&self, name: &str) -> bool {
353        self.all || self.after.iter().any(|it| it == name)
354    }
355}
356
357/// What the command line asked the optimizer for.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct Options {
360    /// Which pipeline to start from.
361    pub level: OptLevel,
362    /// The passes `-f<name>` added and `-fno-<name>` removed, in the order they were given, so
363    /// that the last mention of a pass is the one that decides.
364    pub toggles: Vec<(String, bool)>,
365    /// What `-fpass-fuel=<pass>=<n>` limited, by pass name.
366    pub fuel: HashMap<String, u32>,
367    /// What `-fpass-fuel-global=<n>` limited the whole pipeline to, across every pass.
368    ///
369    /// This is the outer search of the two in section 4.5 of
370    /// `spec/optimizer/04-pass-manager.md`. Halving this finds the pass, and halving
371    /// `-fpass-fuel` for that pass finds the rewrite inside it. Two searches of twenty
372    /// compilations each beat one search over a space nobody knows the shape of.
373    pub global_fuel: Option<u32>,
374    /// What `-fdisable-<pass>` and `-fenable-<pass>` said about which functions a pass runs on.
375    pub gates: Gates,
376    /// What `-fdump-ir=` asked to see.
377    pub dumps: Dumps,
378    /// Whether the verifier runs after every pass that changed anything.
379    pub verify: bool,
380}
381
382impl Default for Options {
383    /// The default level with nothing added to it, and the verifier on in a debug build, which
384    /// is what section 9.10 asks for.
385    fn default() -> Self {
386        Self {
387            level: OptLevel::default(),
388            toggles: Vec::new(),
389            fuel: HashMap::new(),
390            global_fuel: None,
391            gates: Gates::default(),
392            dumps: Dumps::default(),
393            verify: cfg!(debug_assertions),
394        }
395    }
396}
397
398impl Options {
399    /// The options a level asks for on its own.
400    #[must_use]
401    pub fn for_level(level: OptLevel) -> Self {
402        Self { level, ..Self::default() }
403    }
404
405    /// The passes the level and the `-f` flags chose, in order, before the gates are consulted.
406    ///
407    /// A pass named by `-f<name>` that the level did not choose is appended, because the only
408    /// place it could go that does not need an ordering rule nobody wrote down is the end.
409    #[must_use]
410    pub fn chosen(&self) -> Vec<&'static str> {
411        let mut names: Vec<&str> = for_level(self.level).to_vec();
412        for (name, on) in &self.toggles {
413            let name = name.as_str();
414            match *on {
415                true if !names.contains(&name) => names.push(name),
416                true => {}
417                false => names.retain(|it| *it != name),
418            }
419        }
420        names.into_iter().filter_map(pass::find).map(Pass::name).collect()
421    }
422
423    /// The passes that will run, in order, over at least one function.
424    ///
425    /// A pass `-fenable-<name>` reached that the level did not choose is appended after them,
426    /// for the same reason and in the same place. It runs only over the functions the gate names,
427    /// which is the whole point of the flag: a pass being in this list is not the same question as
428    /// a pass running on the function somebody is looking at.
429    #[must_use]
430    pub fn passes(&self) -> Vec<&'static dyn Pass> {
431        let mut names = self.chosen();
432        for name in self.gates.enabled() {
433            // Through the pass list rather than straight from the gate, because the name the
434            // pass holds outlives this call and the one the gate holds does not.
435            let Some(found) = pass::find(name) else { continue };
436            if !names.contains(&found.name()) {
437                names.push(found.name());
438            }
439        }
440        names.into_iter().filter_map(pass::find).collect()
441    }
442}
443
444/// One written out copy of the IR.
445#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct Dump {
447    /// What to call it, which is a number, a side and a pass name, as in `01-after-fold`. The
448    /// number is there so that a directory listing is in the order the passes ran.
449    pub name: String,
450    /// The module, in the textual form from `spec/08-ir.md`.
451    pub text: String,
452}
453
454/// What one pass had to say about one function.
455///
456/// One of these per pass per function with a body, whether or not the pass said anything, because
457/// a pass that reports nothing being visible as a pass that reports nothing is the point of the
458/// record. Section 42.2 of `spec/optimizer/42-measurement.md` has the argument.
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct Remark {
461    /// Which pass, by the name a `-f` flag spells.
462    pub pass: &'static str,
463    /// Which function, by the name in the source.
464    pub func: Symbol,
465    /// What it said.
466    pub stats: Stats,
467}
468
469/// What running the pipeline produced beyond the changed module.
470#[derive(Debug, Clone, Default, PartialEq, Eq)]
471pub struct Report {
472    /// The dumps asked for, in the order they were taken. The manager does not write files,
473    /// because nothing below the driver in `spec/18-package-layout.md` knows what a file is.
474    pub dumps: Vec<Dump>,
475    /// A pass that left the IR in a state the verifier refuses, named, with what it said.
476    pub broke: Vec<String>,
477    /// How much fuel each pass spent, which is the number a bisection halves.
478    pub spent: Vec<(&'static str, u32)>,
479    /// What every pass said about every function, in the order the passes ran and then in the
480    /// order the module holds its functions. This is what `-fopt-info` prints.
481    pub remarks: Vec<Remark>,
482}
483
484impl Report {
485    /// Everything one pass said across the whole module, added up.
486    ///
487    /// The counts of an event are addable across functions because an event names a site in a
488    /// pass rather than a fact about a program, which is the reason [`crate::stats::Event::what`]
489    /// is a fixed string.
490    #[must_use]
491    pub fn totals(&self, pass: &str) -> Stats {
492        let mut total = Stats::new();
493        for remark in self.remarks.iter().filter(|it| it.pass == pass) {
494            total.merge(&remark.stats);
495        }
496        total
497    }
498}
499
500/// Runs the pipeline over the module.
501///
502/// Every pass sees every function with a body, one at a time, and a pass runs over the whole
503/// module before the next one starts. That order is what makes the dumps readable: a dump is
504/// the state of the program between two passes rather than between two functions.
505pub fn run(module: &mut Module, names: &Interner, opts: &Options) -> Report {
506    let mut report = Report::default();
507    let chosen = opts.chosen();
508    // One cache per function, kept across passes because a pass runs over the whole module
509    // before the next one starts. A cache that lived only as long as one function would be
510    // thrown away between every pass and would never answer a second question. Section 4.2 of
511    // `spec/optimizer/04-pass-manager.md` is the plan for turning the loop inside out, and the
512    // day that happens this map becomes a local in the inner loop.
513    let mut cached: HashMap<FuncId, Analyses> = HashMap::new();
514    // The machine, once for the module, because every function in it is compiled for the same
515    // target at the same goal. It goes into each function's cache rather than into a parameter of
516    // its own, per `crate::machine`.
517    let machine = Machine::of(module, opts.level);
518    // What the whole pipeline has left, which every pass draws its own allowance out of and
519    // gives the unspent part of back. A pass past the end of it is given nothing rather than
520    // skipped, so it still runs, still reports, and still transforms nothing.
521    let mut budget = opts.global_fuel;
522    // What each pass has left of what `-fpass-fuel` gave it. One allowance across every place
523    // the list names that pass, rather than one allowance each, because the number in the flag
524    // is meant to be the number of rewrites that happened. A peephole that runs twice under
525    // `-fpass-fuel=simplify=5` and rewrites ten things would make the bisection in section 4.5
526    // of `spec/optimizer/04-pass-manager.md` step over the rewrite it was looking for.
527    let mut allowance = opts.fuel.clone();
528    let passes = opts.passes();
529    // Before anything runs, because each of these is a fact about the module and every pass after
530    // this sees one function. Only when a pass in this run reads them: a flag nothing looks at
531    // would show up in every `-O0` dump and mean nothing to anybody reading one.
532    if passes.iter().any(|pass| READS_SUMMARIES.contains(&pass.name())) {
533        nofree::annotate(module, names);
534        extents::annotate(module);
535        params::annotate(module);
536        heap::annotate(module, names);
537    }
538    for (index, pass) in passes.into_iter().enumerate() {
539        let name = pass.name();
540        if opts.dumps.wants_before(name) {
541            report.dumps.push(dump(index, "before", name, module, names));
542        }
543        let mut fuel = match (allowance.get(name).copied(), budget) {
544            // Whichever limit is tighter, because two limits that disagree mean the one that
545            // stops first, and a bisection that started with the global one has to stay inside
546            // it while the per pass one is halved.
547            (Some(count), Some(left)) => Fuel::of(count.min(left)),
548            (Some(count), None) => Fuel::of(count),
549            (None, Some(left)) => Fuel::of(left),
550            (None, None) => Fuel::unlimited(),
551        };
552        // What the level and the `-f` flags decided, which is what a gate overrides for the
553        // functions it names and leaves alone for the ones it does not.
554        let default = chosen.contains(&name);
555        for id in module.funcs() {
556            if module[id].is_declaration() {
557                continue;
558            }
559            if !opts.gates.allows(name, default, id.raw(), names.resolve(module[id].name)) {
560                // No remark either. A pass that did not run on a function has nothing to say
561                // about it, and a record saying it found nothing would read as a pass that
562                // looked.
563                continue;
564            }
565            let an = cached.entry(id).or_insert_with(|| Analyses::new(machine));
566            let stats = pass.run(&mut module[id], an, &mut fuel);
567            // A pass that changed nothing preserved everything, whatever it says about itself,
568            // so the cheap case does not need every pass to have a second opinion about it.
569            // A pass that did change something is taken at its word, and in a checked build the
570            // word is checked.
571            let keeps = if stats.changed() { pass.preserves() } else { Preserved::ALL };
572            for broken in an.settle(&module[id], keeps, opts.verify) {
573                let func = names.resolve(module[id].name);
574                report.broke.push(format!(
575                    "the {name} pass said it preserved {} of {func} and did not",
576                    broken.name()
577                ));
578            }
579            // Here rather than after the pass, and this function rather than the module. A pass
580            // is a function pass, so the only thing it can have broken is the function it was
581            // given, and walking the other ones again after every one of them is the quadratic
582            // walk `rucc_ir::verify_func` exists to avoid. Doing it here is also what lets the
583            // message name the function, which the module walk could not, and it puts the
584            // failure next to the pass that caused it rather than at the end of the module.
585            if stats.changed() && opts.verify {
586                if let Err(errors) = rucc_ir::verify_func(module, &module[id], names) {
587                    let func = names.resolve(module[id].name);
588                    for error in errors {
589                        report
590                            .broke
591                            .push(format!("the {name} pass left invalid IR in {func}, {error}"));
592                    }
593                }
594            }
595            // The record is the only place the manager learns that anything happened, which is
596            // why the pass cannot leave recording until later. See `crate::stats`.
597            report.remarks.push(Remark { pass: name, func: module[id].name, stats });
598        }
599        // Added to rather than pushed, so a pass the list names twice is one line here with what
600        // both of its runs spent. That is the number a bisection halves, and two lines under one
601        // name would be two numbers where the flag takes one.
602        match report.spent.iter_mut().find(|(it, _)| *it == name) {
603            Some((_, total)) => *total += fuel.spent(),
604            None => report.spent.push((name, fuel.spent())),
605        }
606        if let Some(left) = &mut budget {
607            // Never below zero, because the allowance the pass was given was at most this.
608            *left -= fuel.spent();
609        }
610        if let Some(left) = allowance.get_mut(name) {
611            // Same, and for the same reason.
612            *left -= fuel.spent();
613        }
614        if opts.dumps.wants_after(name) {
615            report.dumps.push(dump(index, "after", name, module, names));
616        }
617    }
618    report
619}
620
621/// The module written out, under a name that sorts in the order the passes ran.
622fn dump(index: usize, side: &str, name: &str, module: &Module, names: &Interner) -> Dump {
623    Dump { name: format!("{index:02}-{side}-{name}"), text: rucc_ir::print(module, names) }
624}
625
626/// Renders what `--print-pipeline` prints.
627///
628/// One line per pass, numbered from one, with what the pass does after it. A level that runs
629/// nothing says so rather than printing an empty list, because an empty answer and a broken
630/// command look the same.
631#[must_use]
632pub fn print(opts: &Options) -> String {
633    let mut out = String::new();
634    let _ = writeln!(out, "level: {}", opts.level);
635    // Only when it was asked for, so the listing of a compilation nobody is bisecting is the
636    // same listing it has always been. A run under a budget is a run whose output is not the
637    // one the level asked for, and the listing is where that has to be visible.
638    if let Some(count) = opts.global_fuel {
639        let _ = writeln!(out, "global fuel: {count}");
640    }
641    let passes = opts.passes();
642    if passes.is_empty() {
643        let _ = writeln!(out, "no passes");
644        return out;
645    }
646    for (index, pass) in passes.iter().enumerate() {
647        let _ = write!(out, "{}: {}, {}", index + 1, pass.name(), pass.describe());
648        // Only when a gate mentions the pass, so the listing of a compilation nobody is
649        // debugging is the same listing it has always been.
650        if let Some(note) = opts.gates.note(pass.name()) {
651            let _ = write!(out, " [{note}]");
652        }
653        out.push('\n');
654    }
655    out
656}
657
658#[cfg(test)]
659mod tests {
660    use rucc_base::Interner;
661    use rucc_ir::{Builder, Flags, Func, Module, Opcode, Signature, Type};
662    use rucc_session::OptLevel;
663    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
664
665    use super::{Dumps, Options, for_level};
666    use crate::stats::Kind;
667    use crate::{Pass, pass};
668
669    /// A module with one function whose body has something to fold in it.
670    fn module() -> (Interner, Module) {
671        let mut names = Interner::new();
672        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
673        let mut module = Module::new(names.intern("test.c"), &target);
674        let func = foldable(&mut names, "f");
675        module.add_func(func);
676        (names, module)
677    }
678
679    /// A module with two of them, called `f` and `g`, in that order, so `f` is function 0.
680    fn two_functions() -> (Interner, Module) {
681        let mut names = Interner::new();
682        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
683        let mut module = Module::new(names.intern("test.c"), &target);
684        for name in ["f", "g"] {
685            let func = foldable(&mut names, name);
686            module.add_func(func);
687        }
688        (names, module)
689    }
690
691    /// A module with one function holding two identities the peephole takes, on a value that
692    /// arrives as a parameter so that folding cannot get to them first.
693    fn identities() -> (Interner, Module) {
694        let mut names = Interner::new();
695        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
696        let mut module = Module::new(names.intern("test.c"), &target);
697        let i32_ = Type::int(32);
698        let mut func = Func::new(
699            names.intern("h"),
700            Signature::new().with_params(&[i32_]).with_returns(&[i32_]),
701        );
702        let entry = func.create_block();
703        let x = func.append_param(entry, i32_);
704        let mut build = Builder::new(&mut func, entry);
705        let zero = build.iconst(i32_, 0);
706        let one = build.iconst(i32_, 1);
707        let sum = build.binary(Opcode::Add, x, zero, Flags::NONE);
708        let product = build.binary(Opcode::Mul, sum, one, Flags::NONE);
709        build.ret(&[product]);
710        module.add_func(func);
711        (names, module)
712    }
713
714    /// A function that returns a sign extension of a constant, which folding rewrites.
715    fn foldable(names: &mut Interner, name: &str) -> Func {
716        let mut func =
717            Func::new(names.intern(name), Signature::new().with_returns(&[Type::int(64)]));
718        let block = func.create_block();
719        let mut build = Builder::new(&mut func, block);
720        let narrow = build.iconst(Type::int(32), 7);
721        let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
722        build.ret(&[wide]);
723        func
724    }
725
726    /// Whether the pass said anything about the function, which it only does when it ran on it.
727    fn spoke_about(report: &super::Report, pass: &str, func: &str, names: &Interner) -> bool {
728        report.remarks.iter().any(|it| it.pass == pass && names.resolve(it.func) == func)
729    }
730
731    #[test]
732    fn every_pass_a_pipeline_names_is_a_pass_that_exists() {
733        for level in
734            [OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
735        {
736            for name in for_level(level) {
737                assert!(
738                    pass::find(name).is_some(),
739                    "{level} names `{name}` and no pass answers to it"
740                );
741            }
742        }
743    }
744
745    #[test]
746    fn a_pass_a_pipeline_names_twice_is_never_named_twice_in_a_row() {
747        // Running a pass again after another pass has been through is the point of naming it
748        // twice, and `simplify` around `narrow` is why the rule that used to be here, which was
749        // that no level names a pass twice at all, is not the rule any more. Two runs with
750        // nothing between them is still a mistake: the second one sees exactly what the first
751        // one finished with, so it can only report that it found nothing.
752        for level in
753            [OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
754        {
755            for pair in for_level(level).windows(2) {
756                assert_ne!(pair[0], pair[1], "{level} runs `{}` twice in a row", pair[0]);
757            }
758        }
759    }
760
761    #[test]
762    fn a_pass_the_pipeline_runs_twice_gets_one_allowance_and_reports_one_number() {
763        // `-fpass-fuel=<pass>=<n>` is halved to find one rewrite, so the number in the flag has
764        // to be the number of rewrites that happened however many times the list names the pass.
765        // The peephole is named twice from `-O1` up and the function below holds two identities
766        // it takes, so a cap of one has to stop after one rather than after one per occurrence.
767        assert_eq!(for_level(OptLevel::O2).iter().filter(|it| **it == "simplify").count(), 2);
768
769        let (names, mut module) = identities();
770        let free = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
771        assert_eq!(spent(&free, "simplify"), Some(2), "{:?}", free.spent);
772
773        let (names, mut module) = identities();
774        let mut opts = Options::for_level(OptLevel::O2);
775        opts.fuel.insert("simplify".to_owned(), 1);
776        let capped = super::run(&mut module, &names, &opts);
777        assert_eq!(capped.spent.iter().filter(|(name, _)| *name == "simplify").count(), 1);
778        assert_eq!(spent(&capped, "simplify"), Some(1), "{:?}", capped.spent);
779    }
780
781    #[test]
782    fn an_identity_only_the_narrow_pass_can_produce_is_still_taken() {
783        // Issue 505, and the reason the peephole is named on both sides of `narrow`. C promotes
784        // before it operates, so `unsigned char x; (unsigned char)(x & 255)` arrives here as a
785        // thirty two bit `and` of a zero extension, and the rule that says `and` with every bit
786        // set is the value has nothing at eight bits to match. `narrow` is the only producer that
787        // width has. Before this ran twice the `and.i8` below reached the back end untouched.
788        let mut names = Interner::new();
789        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
790        let mut module = Module::new(names.intern("test.c"), &target);
791        let (i8_, i32_) = (Type::int(8), Type::int(32));
792        let mut func =
793            Func::new(names.intern("f"), Signature::new().with_params(&[i8_]).with_returns(&[i8_]));
794        let entry = func.create_block();
795        let x = func.append_param(entry, i8_);
796        let mut build = Builder::new(&mut func, entry);
797        let wide = build.unary(Opcode::ZExt, x, i32_);
798        let mask = build.iconst(i32_, 255);
799        let kept = build.binary(Opcode::And, wide, mask, Flags::NONE);
800        let back = build.unary(Opcode::Trunc, kept, i8_);
801        build.ret(&[back]);
802        module.add_func(func);
803
804        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
805        assert!(report.broke.is_empty(), "{:?}", report.broke);
806        let text = rucc_ir::print(&module, &names);
807        assert!(!text.contains("and."), "the masking survived the pipeline\n{text}");
808    }
809
810    /// What a pass spent, or `None` if it did not run.
811    fn spent(report: &super::Report, pass: &str) -> Option<u32> {
812        report.spent.iter().find(|(name, _)| *name == pass).map(|&(_, count)| count)
813    }
814
815    /// The names of the passes a set of options would run, in order.
816    fn names(opts: &Options) -> Vec<&'static str> {
817        opts.passes().into_iter().map(Pass::name).collect()
818    }
819
820    #[test]
821    fn the_level_that_optimizes_nothing_still_removes_what_nothing_reaches() {
822        // One pass at `-O0`, and it is the one that is not an optimization. See the comment on
823        // the level itself, and issue 359.
824        assert_eq!(names(&Options::for_level(OptLevel::O0)), ["simplify-cfg"]);
825        assert!(names(&Options::for_level(OptLevel::O2)).len() > 1);
826    }
827
828    #[test]
829    fn a_pass_is_removed_by_no_and_added_by_the_bare_name_and_the_last_word_wins() {
830        let mut opts = Options::for_level(OptLevel::O2);
831        opts.toggles.push(("fold".to_owned(), false));
832        assert!(!names(&opts).contains(&"fold"), "{:?}", names(&opts));
833        opts.toggles.push(("fold".to_owned(), true));
834        assert!(names(&opts).contains(&"fold"), "{:?}", names(&opts));
835
836        let mut off = Options::for_level(OptLevel::O0);
837        off.toggles.push(("fold".to_owned(), true));
838        assert_eq!(
839            names(&off),
840            ["simplify-cfg", "fold"],
841            "a pass the level did not choose is still reachable"
842        );
843    }
844
845    #[test]
846    fn asking_for_a_pass_twice_does_not_run_it_twice() {
847        let mut opts = Options::for_level(OptLevel::O2);
848        let before = names(&opts);
849        opts.toggles.push(("fold".to_owned(), true));
850        assert_eq!(names(&opts), before);
851    }
852
853    #[test]
854    fn the_pipeline_listing_names_the_level_and_every_pass_in_order() {
855        let text = super::print(&Options::for_level(OptLevel::O2));
856        assert!(text.starts_with("level: -O2\n"), "{text}");
857        assert!(text.contains("1: fold, "), "{text}");
858        let mut none = Options::for_level(OptLevel::O0);
859        none.toggles.push(("simplify-cfg".to_owned(), false));
860        let none = super::print(&none);
861        assert!(none.contains("no passes"), "{none}");
862    }
863
864    #[test]
865    fn running_the_pipeline_changes_the_module_and_reports_what_it_spent() {
866        let (names, mut module) = module();
867        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
868        // Folding rewrites the sign extension into a constant, and then the constant it was
869        // extending is read by nothing and dead code elimination takes it out. One
870        // transformation each, which is what the two of them together are for. Asserted by
871        // name rather than as the whole vector, so a pass added later does not fail this.
872        assert_eq!(spent(&report, "fold"), Some(1));
873        assert_eq!(spent(&report, "dce"), Some(1));
874        assert!(report.broke.is_empty(), "{:?}", report.broke);
875        assert!(report.dumps.is_empty(), "nothing asked for a dump");
876        assert!(rucc_ir::print(&module, &names).contains("iconst.i64 7"));
877    }
878
879    #[test]
880    fn the_analyses_survive_a_pass_that_keeps_them_and_not_one_that_does_not() {
881        // The pipeline half of the analysis manager. A branch on a constant, so `simplify-cfg`
882        // has something to do and says it preserved nothing, and the whole run comes out with
883        // the verifier and the manager both satisfied. What a pass that lied would produce is in
884        // `crate::analysis`, where a lie can be told on purpose.
885        let mut names = Interner::new();
886        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
887        let mut module = Module::new(names.intern("test.c"), &target);
888        let mut func = Func::new(names.intern("f"), Signature::new());
889        let entry = func.create_block();
890        let dead = func.create_block();
891        let exit = func.create_block();
892        let mut build = Builder::new(&mut func, entry);
893        let never = build.iconst(Type::int(1), 0);
894        build.br_if(never, dead, &[], exit, &[]);
895        for block in [dead, exit] {
896            let mut build = Builder::new(&mut func, block);
897            build.ret(&[]);
898        }
899        module.add_func(func);
900        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
901        // The fold, and then the merge of the arm it left with one way into it.
902        assert_eq!(spent(&report, "simplify-cfg"), Some(2));
903        assert!(report.broke.is_empty(), "{:?}", report.broke);
904        let text = rucc_ir::print(&module, &names);
905        // The labels, which start a line, and not the mentions of one, which are indented. One
906        // left: the arm nothing reaches went, and the arm that is always taken came up into the
907        // entry, which is what is left of the branch.
908        assert_eq!(text.matches("\nblock").count(), 1, "there is more than one block:\n{text}");
909    }
910
911    #[test]
912    fn no_pass_that_optimizes_runs_at_no_optimization_however_much_there_is_to_do() {
913        let (names, mut module) = module();
914        let before = rucc_ir::print(&module, &names);
915        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O0));
916        // The one pass the level runs looked, found no branch it could read and no block nothing
917        // reaches, and spent nothing. The constant arithmetic the fixture is full of is still
918        // there, which is the part of `-O0` that has not changed.
919        assert_eq!(report.spent, vec![("simplify-cfg", 0)]);
920        assert_eq!(rucc_ir::print(&module, &names), before);
921    }
922
923    #[test]
924    fn a_gate_takes_a_pass_away_from_one_function_and_leaves_the_other_alone() {
925        let (names, mut module) = two_functions();
926        let mut opts = Options::for_level(OptLevel::O2);
927        opts.gates.add(false, "fold=g").expect("g is a function and fold is a pass");
928        let report = super::run(&mut module, &names, &opts);
929        assert!(spoke_about(&report, "fold", "f", &names));
930        assert!(!spoke_about(&report, "fold", "g", &names), "fold ran where it was gated off");
931        assert!(spoke_about(&report, "dce", "g", &names), "one pass gated off is not all of them");
932        // What the gate is for: the two functions came out different, and the difference is one
933        // pass on one function rather than a level on a file.
934        let text = rucc_ir::print(&module, &names);
935        assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
936    }
937
938    #[test]
939    fn a_function_can_be_gated_by_the_number_it_has_in_the_module() {
940        let (names, mut module) = two_functions();
941        let mut opts = Options::for_level(OptLevel::O2);
942        opts.gates.add(false, "fold=0").expect("0 is a function and fold is a pass");
943        let report = super::run(&mut module, &names, &opts);
944        assert!(!spoke_about(&report, "fold", "f", &names), "function 0 is the first one");
945        assert!(spoke_about(&report, "fold", "g", &names));
946    }
947
948    #[test]
949    fn enabling_a_pass_reaches_one_function_at_a_level_that_did_not_ask_for_it() {
950        let (names, mut module) = two_functions();
951        let mut opts = Options::for_level(OptLevel::O0);
952        opts.gates.add(true, "fold=1").expect("1 is a function and fold is a pass");
953        let running: Vec<&str> = opts.passes().into_iter().map(Pass::name).collect();
954        assert_eq!(
955            running,
956            ["simplify-cfg", "fold"],
957            "the flag has to put the pass in the pipeline"
958        );
959        let report = super::run(&mut module, &names, &opts);
960        assert!(!spoke_about(&report, "fold", "f", &names), "nothing asked for f");
961        assert!(spoke_about(&report, "fold", "g", &names));
962        let text = rucc_ir::print(&module, &names);
963        assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
964    }
965
966    #[test]
967    fn a_pass_gated_off_everywhere_runs_on_nothing_and_still_says_so() {
968        let (names, mut module) = two_functions();
969        let before = rucc_ir::print(&module, &names);
970        let mut opts = Options::for_level(OptLevel::O2);
971        for pass in pass::PASSES {
972            opts.gates.add(false, pass.name()).expect("a pass in the list is a pass that exists");
973        }
974        let report = super::run(&mut module, &names, &opts);
975        assert!(report.remarks.is_empty(), "a pass that did not run has nothing to report");
976        assert_eq!(spent(&report, "fold"), Some(0), "the pass is still in the pipeline");
977        assert_eq!(rucc_ir::print(&module, &names), before);
978    }
979
980    #[test]
981    fn the_pipeline_listing_says_which_passes_a_gate_touched() {
982        let mut opts = Options::for_level(OptLevel::O2);
983        opts.gates.add(false, "fold=2-4").expect("fold is a pass");
984        let text = super::print(&opts);
985        assert!(text.contains("1: fold, "), "{text}");
986        assert!(text.contains("[off for 2-4]"), "{text}");
987        assert_eq!(text.matches('[').count(), 1, "a pass no gate mentions says nothing extra");
988    }
989
990    #[test]
991    fn every_pass_at_no_fuel_leaves_the_module_exactly_as_it_found_it() {
992        // The check section 9.10 asks for by name, and the reason it is here rather than in each
993        // pass is that it has to hold for every pass that is ever added.
994        for pass in pass::PASSES {
995            let (names, mut module) = module();
996            let before = rucc_ir::print(&module, &names);
997            let mut opts = Options::for_level(OptLevel::O0);
998            // The level's own pass out of the way first, so that what this measures is the one
999            // pass under test. A pass turned off and then on again is on, so this is right for
1000            // that pass as well as for the others.
1001            opts.toggles.push(("simplify-cfg".to_owned(), false));
1002            opts.toggles.push((pass.name().to_owned(), true));
1003            opts.fuel.insert(pass.name().to_owned(), 0);
1004            let report = super::run(&mut module, &names, &opts);
1005            assert_eq!(
1006                report.spent,
1007                vec![(pass.name(), 0)],
1008                "{} spent fuel it had none of",
1009                pass.name()
1010            );
1011            assert_eq!(
1012                rucc_ir::print(&module, &names),
1013                before,
1014                "{} transformed the module at fuel zero",
1015                pass.name()
1016            );
1017        }
1018    }
1019
1020    #[test]
1021    fn fuel_is_shared_across_the_functions_of_a_module() {
1022        let mut names = Interner::new();
1023        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1024        let mut module = Module::new(names.intern("test.c"), &target);
1025        for which in ["f", "g"] {
1026            let mut func =
1027                Func::new(names.intern(which), Signature::new().with_returns(&[Type::int(64)]));
1028            let block = func.create_block();
1029            let mut build = Builder::new(&mut func, block);
1030            let narrow = build.iconst(Type::int(32), 7);
1031            let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
1032            build.ret(&[wide]);
1033            module.add_func(func);
1034        }
1035        let mut opts = Options::for_level(OptLevel::O2);
1036        opts.fuel.insert("fold".to_owned(), 1);
1037        let report = super::run(&mut module, &names, &opts);
1038        // One fold across both functions, because fuel is per pass and per compilation. Dead
1039        // code elimination has its own and spends it on the constant the one fold orphaned.
1040        assert_eq!(spent(&report, "fold"), Some(1));
1041        assert_eq!(spent(&report, "dce"), Some(1));
1042        let text = rucc_ir::print(&module, &names);
1043        assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
1044    }
1045
1046    #[test]
1047    fn global_fuel_is_spent_by_the_passes_in_order_and_the_rest_get_none() {
1048        let (names, mut module) = module();
1049        let mut opts = Options::for_level(OptLevel::O2);
1050        opts.global_fuel = Some(1);
1051        let report = super::run(&mut module, &names, &opts);
1052        // Folding is first and there is one thing to fold, so it takes the one unit and dead
1053        // code elimination gets nothing. Without the budget it would have taken the constant
1054        // that fold orphaned, which is what the other test measures.
1055        assert_eq!(spent(&report, "fold"), Some(1));
1056        assert_eq!(spent(&report, "dce"), Some(0));
1057        let text = rucc_ir::print(&module, &names);
1058        assert!(text.contains("iconst.i64 7"), "{text}");
1059        assert!(text.contains("iconst.i32 7"), "the orphaned constant is still there, {text}");
1060    }
1061
1062    #[test]
1063    fn a_budget_of_nothing_leaves_the_module_alone_and_still_runs_every_pass() {
1064        let (names, mut module) = module();
1065        let before = rucc_ir::print(&module, &names);
1066        let mut opts = Options::for_level(OptLevel::O2);
1067        opts.global_fuel = Some(0);
1068        let report = super::run(&mut module, &names, &opts);
1069        assert_eq!(rucc_ir::print(&module, &names), before);
1070        assert!(report.spent.iter().all(|(_, spent)| *spent == 0), "{:?}", report.spent);
1071        // Every pass, because a pass out of fuel is a pass that ran and did nothing rather than
1072        // a pass that was skipped, and a bisection that skipped passes would be searching a
1073        // different pipeline at every step. One line per name rather than one per place the list
1074        // names it, because what a name was given is one allowance across all of them.
1075        let mut want: Vec<&str> = opts.passes().into_iter().map(Pass::name).collect();
1076        want.sort_unstable();
1077        want.dedup();
1078        let mut got: Vec<&str> = report.spent.iter().map(|&(name, _)| name).collect();
1079        got.sort_unstable();
1080        assert_eq!(got, want);
1081    }
1082
1083    #[test]
1084    fn the_tighter_of_the_two_limits_is_the_one_that_stops_the_pass() {
1085        // A pass allowed more than the budget gets the budget.
1086        let (names, mut under) = module();
1087        let mut opts = Options::for_level(OptLevel::O2);
1088        opts.global_fuel = Some(0);
1089        opts.fuel.insert("fold".to_owned(), 9);
1090        assert_eq!(spent(&super::run(&mut under, &names, &opts), "fold"), Some(0));
1091
1092        // And a pass allowed less than the budget keeps its own limit, with the budget left
1093        // over for whatever comes after it.
1094        let (names, mut over) = module();
1095        let mut opts = Options::for_level(OptLevel::O2);
1096        opts.global_fuel = Some(9);
1097        opts.fuel.insert("fold".to_owned(), 0);
1098        let report = super::run(&mut over, &names, &opts);
1099        assert_eq!(spent(&report, "fold"), Some(0));
1100        assert_eq!(spent(&report, "dce"), Some(0), "nothing was orphaned for it to remove");
1101    }
1102
1103    #[test]
1104    fn the_pipeline_listing_says_when_there_is_a_budget_and_says_nothing_when_there_is_not() {
1105        let opts = Options::for_level(OptLevel::O2);
1106        assert!(!super::print(&opts).contains("global fuel"));
1107        let with = Options { global_fuel: Some(12), ..Options::for_level(OptLevel::O2) };
1108        assert!(super::print(&with).contains("global fuel: 12"), "{}", super::print(&with));
1109    }
1110
1111    #[test]
1112    fn a_dump_is_taken_on_the_side_that_asked_for_it_and_not_the_other() {
1113        let (names, mut module) = module();
1114        let mut opts = Options::for_level(OptLevel::O2);
1115        opts.dumps.add("after-fold").expect("a pass that exists");
1116        let report = super::run(&mut module, &names, &opts);
1117        assert_eq!(report.dumps.len(), 1);
1118        assert_eq!(report.dumps[0].name, "00-after-fold");
1119        assert!(report.dumps[0].text.contains("iconst.i64 7"));
1120    }
1121
1122    #[test]
1123    fn asking_for_all_dumps_gives_both_sides_of_every_pass() {
1124        let (interner, mut module) = module();
1125        let opts = {
1126            let mut opts = Options::for_level(OptLevel::O2);
1127            opts.dumps.add("all").expect("all is always a dump");
1128            opts
1129        };
1130        let report = super::run(&mut module, &interner, &opts);
1131        // Both sides of every pass in the level, numbered by position, whatever the level
1132        // holds. Written out of the pipeline rather than as a literal, because the point of
1133        // the test is the pairing and the numbering and not which passes exist this month.
1134        let taken: Vec<&str> = report.dumps.iter().map(|d| d.name.as_str()).collect();
1135        let expected: Vec<String> = names(&opts)
1136            .into_iter()
1137            .enumerate()
1138            .flat_map(|(at, name)| {
1139                [format!("{at:02}-before-{name}"), format!("{at:02}-after-{name}")]
1140            })
1141            .collect();
1142        assert_eq!(taken, expected);
1143        assert!(report.dumps[0].text.contains("sext.i64"));
1144        assert!(!report.dumps[1].text.contains("sext.i64"));
1145    }
1146
1147    #[test]
1148    fn every_pass_leaves_a_record_for_every_function_whether_or_not_it_had_anything_to_say() {
1149        let (names, mut module) = module();
1150        let opts = Options::for_level(OptLevel::O2);
1151        let report = super::run(&mut module, &names, &opts);
1152        let ran: Vec<&'static str> = opts.passes().into_iter().map(Pass::name).collect();
1153        // One function in the fixture, so one record per pass, and the passes in the order they
1154        // ran. A pass that found nothing is in here with an empty record, which is the point:
1155        // a pass that fires on nothing is either dead code or a bug, and output that leaves it
1156        // out cannot say which.
1157        let seen: Vec<&'static str> = report.remarks.iter().map(|it| it.pass).collect();
1158        assert_eq!(seen, ran);
1159        assert!(report.remarks.iter().all(|it| names.resolve(it.func) == "f"));
1160        assert!(
1161            report.remarks.iter().any(|it| it.pass == "simplify" && it.stats.is_empty()),
1162            "there is nothing in the fixture for the peephole to do"
1163        );
1164    }
1165
1166    #[test]
1167    fn a_pass_spends_one_unit_of_fuel_for_each_rewrite_it_reports() {
1168        // The invariant that keeps the record honest, checked over every pass rather than
1169        // written into each one. Fuel is taken immediately before a transformation and a
1170        // rewrite is recorded immediately after it, so the two counts are the same number
1171        // arrived at from two directions. A pass where they disagree either transformed without
1172        // asking, which breaks bisection, or rewrote without recording, which means the manager
1173        // did not run the verifier over what it produced.
1174        let (names, mut module) = module();
1175        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
1176        for (pass, spent) in &report.spent {
1177            assert_eq!(
1178                report.totals(pass).total(Kind::Optimized),
1179                *spent,
1180                "{pass} spent {spent} units of fuel and did not say on what"
1181            );
1182        }
1183        assert!(report.spent.iter().any(|(_, spent)| *spent > 0), "nothing happened at all");
1184    }
1185
1186    #[test]
1187    fn what_the_passes_said_is_what_opt_info_prints() {
1188        let (names, mut module) = module();
1189        let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
1190        let text = crate::optinfo::render("t.c", &report, &names, crate::Wants::all());
1191        assert!(
1192            text.contains("t.c: f: optimized: integer instruction folded to a constant (1) [fold]"),
1193            "{text}"
1194        );
1195        assert!(
1196            text.contains(
1197                "t.c: f: optimized: instruction with no effects and no users removed (1) [dce]"
1198            ),
1199            "{text}"
1200        );
1201        // Nothing in the fixture is a miss, so asking only for the misses gets nothing back,
1202        // and that is different from the flag having been left off.
1203        let mut misses = crate::Wants::none();
1204        misses.add("missed").expect("that kind exists");
1205        assert_eq!(crate::optinfo::render("t.c", &report, &names, misses), "");
1206    }
1207
1208    #[test]
1209    fn the_verifier_says_which_function_it_refused_and_leaves_the_others_out_of_it() {
1210        // Two functions with the same foldable body, and a block in the second one that nothing
1211        // reaches, which the verifier refuses. The pass is not what put it there, and the
1212        // complaint says the pass anyway, because a pass that hands back a function the
1213        // verifier will not take is where the search has to start whoever wrote the block.
1214        let mut names = Interner::new();
1215        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1216        let mut module = Module::new(names.intern("test.c"), &target);
1217        module.add_func(foldable(&mut names, "f"));
1218        let mut g = foldable(&mut names, "g");
1219        let stranded = g.create_block();
1220        let mut build = Builder::new(&mut g, stranded);
1221        let seven = build.iconst(Type::int(64), 7);
1222        build.ret(&[seven]);
1223        module.add_func(g);
1224
1225        // Folding on its own, because simplify-CFG would take the stranded block out and there
1226        // would be nothing left to complain about.
1227        let mut opts = Options::for_level(OptLevel::O0);
1228        opts.toggles.push(("simplify-cfg".to_owned(), false));
1229        opts.toggles.push(("fold".to_owned(), true));
1230        opts.verify = true;
1231        let report = super::run(&mut module, &names, &opts);
1232
1233        assert_eq!(report.broke.len(), 1, "{:?}", report.broke);
1234        let complaint = &report.broke[0];
1235        assert!(complaint.starts_with("the fold pass left invalid IR in g,"), "{complaint}");
1236        assert!(complaint.contains("this block is not reachable"), "{complaint}");
1237    }
1238
1239    #[test]
1240    fn a_function_a_pass_did_not_change_is_not_verified_after_it() {
1241        // The stranded block is in `f` this time and `f` has nothing to fold, so the pass runs
1242        // over an invalid function, changes nothing, and says nothing. That is the whole trade:
1243        // the verifier answers for the rewrite that just happened, and a function no rewrite
1244        // touched was already answered for when it was built.
1245        let mut names = Interner::new();
1246        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
1247        let mut module = Module::new(names.intern("test.c"), &target);
1248        let mut f = Func::new(names.intern("f"), Signature::new().with_returns(&[Type::int(64)]));
1249        for _ in 0..2 {
1250            let block = f.create_block();
1251            let mut build = Builder::new(&mut f, block);
1252            let seven = build.iconst(Type::int(64), 7);
1253            build.ret(&[seven]);
1254        }
1255        module.add_func(f);
1256        module.add_func(foldable(&mut names, "g"));
1257
1258        let mut opts = Options::for_level(OptLevel::O0);
1259        opts.toggles.push(("simplify-cfg".to_owned(), false));
1260        opts.toggles.push(("fold".to_owned(), true));
1261        opts.verify = true;
1262        let report = super::run(&mut module, &names, &opts);
1263
1264        assert!(report.broke.is_empty(), "{:?}", report.broke);
1265        // And it did run on it, so this is the verifier staying quiet rather than the pass
1266        // being skipped.
1267        assert!(spoke_about(&report, "fold", "f", &names));
1268    }
1269
1270    #[test]
1271    fn a_dump_of_a_pass_that_does_not_exist_is_refused_rather_than_ignored() {
1272        let mut dumps = Dumps::default();
1273        assert!(dumps.add("after-no-such-pass").is_err());
1274        assert!(dumps.add("sideways-fold").is_err());
1275        assert!(dumps.add("fold").is_err());
1276        assert!(dumps.is_empty());
1277    }
1278}