1use 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
42const O0: &[&str] = &["simplify-cfg"];
53
54const O1: &[&str] = &["fold", "simplify", "narrow", "simplify", "simplify-cfg", "dce"];
73
74const O2: &[&str] = &["fold", "simplify", "narrow", "simplify", "simplify-cfg", "dce"];
78
79const O3: &[&str] = &["fold", "simplify", "narrow", "simplify", "simplify-cfg", "dce"];
82
83const OS: &[&str] = &["fold", "simplify", "narrow", "simplify", "simplify-cfg", "dce"];
90
91const OZ: &[&str] = &["fold", "simplify", "narrow", "simplify", "simplify-cfg", "dce"];
94
95#[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#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub struct Dumps {
114 all: bool,
116 before: Vec<String>,
118 after: Vec<String>,
120}
121
122impl Dumps {
123 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 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 !self.all && self.before.is_empty() && self.after.is_empty()
156 }
157
158 #[must_use]
160 pub fn wants_before(&self, name: &str) -> bool {
161 self.all || self.before.iter().any(|it| it == name)
162 }
163
164 #[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#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct Options {
174 pub level: OptLevel,
176 pub toggles: Vec<(String, bool)>,
179 pub fuel: HashMap<String, u32>,
181 pub global_fuel: Option<u32>,
188 pub gates: Gates,
190 pub dumps: Dumps,
192 pub verify: bool,
194}
195
196impl Default for Options {
197 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 #[must_use]
215 pub fn for_level(level: OptLevel) -> Self {
216 Self { level, ..Self::default() }
217 }
218
219 #[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 #[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 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#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct Dump {
261 pub name: String,
264 pub text: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct Remark {
275 pub pass: &'static str,
277 pub func: Symbol,
279 pub stats: Stats,
281}
282
283#[derive(Debug, Clone, Default, PartialEq, Eq)]
285pub struct Report {
286 pub dumps: Vec<Dump>,
289 pub broke: Vec<String>,
291 pub spent: Vec<(&'static str, u32)>,
293 pub remarks: Vec<Remark>,
296}
297
298impl Report {
299 #[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
314pub fn run(module: &mut Module, names: &Interner, opts: &Options) -> Report {
320 let mut report = Report::default();
321 let chosen = opts.chosen();
322 let mut cached: HashMap<FuncId, Analyses> = HashMap::new();
328 let mut budget = opts.global_fuel;
332 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 (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 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 continue;
364 }
365 let an = cached.entry(id).or_default();
366 let stats = pass.run(&mut module[id], an, &mut fuel);
367 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 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 report.remarks.push(Remark { pass: name, func: module[id].name, stats });
398 }
399 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 *left -= fuel.spent();
409 }
410 if let Some(left) = allowance.get_mut(name) {
411 *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
421fn 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#[must_use]
432pub fn print(opts: &Options) -> String {
433 let mut out = String::new();
434 let _ = writeln!(out, "level: {}", opts.level);
435 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 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 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 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 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 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 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 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 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 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 fn spent(report: &super::Report, pass: &str) -> Option<u32> {
612 report.spent.iter().find(|(name, _)| *name == pass).map(|&(_, count)| count)
613 }
614
615 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}