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