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