use std::collections::HashMap;
use std::fmt::Write as _;
use rucc_base::Interner;
use rucc_ir::Module;
use rucc_session::OptLevel;
use crate::{Fuel, Pass, pass};
const O0: &[&str] = &[];
const O1: &[&str] = &["fold", "simplify", "dce"];
const O2: &[&str] = &["fold", "simplify", "dce"];
const O3: &[&str] = &["fold", "simplify", "dce"];
const OS: &[&str] = &["fold", "simplify", "dce"];
const OZ: &[&str] = &["fold", "simplify", "dce"];
#[must_use]
pub const fn for_level(level: OptLevel) -> &'static [&'static str] {
match level {
OptLevel::O0 => O0,
OptLevel::O1 => O1,
OptLevel::O2 => O2,
OptLevel::O3 => O3,
OptLevel::Os => OS,
OptLevel::Oz => OZ,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Dumps {
all: bool,
before: Vec<String>,
after: Vec<String>,
}
impl Dumps {
pub fn add(&mut self, spec: &str) -> Result<(), String> {
if spec == "all" {
self.all = true;
return Ok(());
}
let (side, name) = match spec.split_once('-') {
Some(("before", name)) => (&mut self.before, name),
Some(("after", name)) => (&mut self.after, name),
_ => {
return Err(format!(
"`{spec}` is not a dump this compiler makes, which are `all`, \
`before-<pass>` and `after-<pass>`"
));
}
};
if pass::find(name).is_none() {
return Err(format!("`{name}` is not a pass this compiler has, see --print-pipeline"));
}
side.push(name.to_owned());
Ok(())
}
#[must_use]
pub fn is_empty(&self) -> bool {
!self.all && self.before.is_empty() && self.after.is_empty()
}
#[must_use]
pub fn wants_before(&self, name: &str) -> bool {
self.all || self.before.iter().any(|it| it == name)
}
#[must_use]
pub fn wants_after(&self, name: &str) -> bool {
self.all || self.after.iter().any(|it| it == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
pub level: OptLevel,
pub toggles: Vec<(String, bool)>,
pub fuel: HashMap<String, u32>,
pub dumps: Dumps,
pub verify: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
level: OptLevel::default(),
toggles: Vec::new(),
fuel: HashMap::new(),
dumps: Dumps::default(),
verify: cfg!(debug_assertions),
}
}
}
impl Options {
#[must_use]
pub fn for_level(level: OptLevel) -> Self {
Self { level, ..Self::default() }
}
#[must_use]
pub fn passes(&self) -> Vec<&'static dyn Pass> {
let mut names: Vec<&str> = for_level(self.level).to_vec();
for (name, on) in &self.toggles {
let name = name.as_str();
match *on {
true if !names.contains(&name) => names.push(name),
true => {}
false => names.retain(|it| *it != name),
}
}
names.into_iter().filter_map(pass::find).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dump {
pub name: String,
pub text: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Report {
pub dumps: Vec<Dump>,
pub broke: Vec<String>,
pub spent: Vec<(&'static str, u32)>,
}
pub fn run(module: &mut Module, names: &Interner, opts: &Options) -> Report {
let mut report = Report::default();
for (index, pass) in opts.passes().into_iter().enumerate() {
let name = pass.name();
if opts.dumps.wants_before(name) {
report.dumps.push(dump(index, "before", name, module, names));
}
let mut fuel = match opts.fuel.get(name) {
Some(&count) => Fuel::of(count),
None => Fuel::unlimited(),
};
let mut changed = false;
for id in module.funcs() {
if module[id].is_declaration() {
continue;
}
changed |= pass.run(&mut module[id], &mut fuel);
}
report.spent.push((name, fuel.spent()));
if changed && opts.verify {
if let Err(errors) = rucc_ir::verify(module, names) {
for error in errors {
report.broke.push(format!("the {name} pass left invalid IR, {error}"));
}
}
}
if opts.dumps.wants_after(name) {
report.dumps.push(dump(index, "after", name, module, names));
}
}
report
}
fn dump(index: usize, side: &str, name: &str, module: &Module, names: &Interner) -> Dump {
Dump { name: format!("{index:02}-{side}-{name}"), text: rucc_ir::print(module, names) }
}
#[must_use]
pub fn print(opts: &Options) -> String {
let mut out = String::new();
let _ = writeln!(out, "level: {}", opts.level);
let passes = opts.passes();
if passes.is_empty() {
let _ = writeln!(out, "no passes");
return out;
}
for (index, pass) in passes.iter().enumerate() {
let _ = writeln!(out, "{}: {}, {}", index + 1, pass.name(), pass.describe());
}
out
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Builder, Func, Module, Opcode, Signature, Type};
use rucc_session::OptLevel;
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::{Dumps, Options, for_level};
use crate::{Pass, pass};
fn module() -> (Interner, Module) {
let mut names = Interner::new();
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let mut module = Module::new(names.intern("test.c"), &target);
let mut func =
Func::new(names.intern("f"), Signature::new().with_returns(&[Type::int(64)]));
let block = func.create_block();
let mut build = Builder::new(&mut func, block);
let narrow = build.iconst(Type::int(32), 7);
let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
build.ret(&[wide]);
module.add_func(func);
(names, module)
}
#[test]
fn every_pass_a_pipeline_names_is_a_pass_that_exists() {
for level in
[OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
{
for name in for_level(level) {
assert!(
pass::find(name).is_some(),
"{level} names `{name}` and no pass answers to it"
);
}
}
}
#[test]
fn no_pipeline_names_a_pass_twice() {
for level in
[OptLevel::O0, OptLevel::O1, OptLevel::O2, OptLevel::O3, OptLevel::Os, OptLevel::Oz]
{
let names = for_level(level);
for (index, name) in names.iter().enumerate() {
assert!(!names[index + 1..].contains(name), "{level} runs `{name}` twice");
}
}
}
fn spent(report: &super::Report, pass: &str) -> Option<u32> {
report.spent.iter().find(|(name, _)| *name == pass).map(|&(_, count)| count)
}
fn names(opts: &Options) -> Vec<&'static str> {
opts.passes().into_iter().map(Pass::name).collect()
}
#[test]
fn nothing_runs_at_no_optimization_and_something_runs_above_it() {
assert!(Options::for_level(OptLevel::O0).passes().is_empty());
assert!(!Options::for_level(OptLevel::O2).passes().is_empty());
}
#[test]
fn a_pass_is_removed_by_no_and_added_by_the_bare_name_and_the_last_word_wins() {
let mut opts = Options::for_level(OptLevel::O2);
opts.toggles.push(("fold".to_owned(), false));
assert!(!names(&opts).contains(&"fold"), "{:?}", names(&opts));
opts.toggles.push(("fold".to_owned(), true));
assert!(names(&opts).contains(&"fold"), "{:?}", names(&opts));
let mut off = Options::for_level(OptLevel::O0);
off.toggles.push(("fold".to_owned(), true));
assert_eq!(names(&off), ["fold"], "a pass the level did not choose is still reachable");
}
#[test]
fn asking_for_a_pass_twice_does_not_run_it_twice() {
let mut opts = Options::for_level(OptLevel::O2);
let before = names(&opts);
opts.toggles.push(("fold".to_owned(), true));
assert_eq!(names(&opts), before);
}
#[test]
fn the_pipeline_listing_names_the_level_and_every_pass_in_order() {
let text = super::print(&Options::for_level(OptLevel::O2));
assert!(text.starts_with("level: -O2\n"), "{text}");
assert!(text.contains("1: fold, "), "{text}");
let none = super::print(&Options::for_level(OptLevel::O0));
assert!(none.contains("no passes"), "{none}");
}
#[test]
fn running_the_pipeline_changes_the_module_and_reports_what_it_spent() {
let (names, mut module) = module();
let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O2));
assert_eq!(spent(&report, "fold"), Some(1));
assert_eq!(spent(&report, "dce"), Some(1));
assert!(report.broke.is_empty(), "{:?}", report.broke);
assert!(report.dumps.is_empty(), "nothing asked for a dump");
assert!(rucc_ir::print(&module, &names).contains("iconst.i64 7"));
}
#[test]
fn no_pass_runs_at_no_optimization_however_much_there_is_to_do() {
let (names, mut module) = module();
let before = rucc_ir::print(&module, &names);
let report = super::run(&mut module, &names, &Options::for_level(OptLevel::O0));
assert!(report.spent.is_empty());
assert_eq!(rucc_ir::print(&module, &names), before);
}
#[test]
fn every_pass_at_no_fuel_leaves_the_module_exactly_as_it_found_it() {
for pass in pass::PASSES {
let (names, mut module) = module();
let before = rucc_ir::print(&module, &names);
let mut opts = Options::for_level(OptLevel::O0);
opts.toggles.push((pass.name().to_owned(), true));
opts.fuel.insert(pass.name().to_owned(), 0);
let report = super::run(&mut module, &names, &opts);
assert_eq!(
report.spent,
vec![(pass.name(), 0)],
"{} spent fuel it had none of",
pass.name()
);
assert_eq!(
rucc_ir::print(&module, &names),
before,
"{} transformed the module at fuel zero",
pass.name()
);
}
}
#[test]
fn fuel_is_shared_across_the_functions_of_a_module() {
let mut names = Interner::new();
let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
let mut module = Module::new(names.intern("test.c"), &target);
for which in ["f", "g"] {
let mut func =
Func::new(names.intern(which), Signature::new().with_returns(&[Type::int(64)]));
let block = func.create_block();
let mut build = Builder::new(&mut func, block);
let narrow = build.iconst(Type::int(32), 7);
let wide = build.unary(Opcode::SExt, narrow, Type::int(64));
build.ret(&[wide]);
module.add_func(func);
}
let mut opts = Options::for_level(OptLevel::O2);
opts.fuel.insert("fold".to_owned(), 1);
let report = super::run(&mut module, &names, &opts);
assert_eq!(spent(&report, "fold"), Some(1));
assert_eq!(spent(&report, "dce"), Some(1));
let text = rucc_ir::print(&module, &names);
assert_eq!(text.matches("sext.i64").count(), 1, "{text}");
}
#[test]
fn a_dump_is_taken_on_the_side_that_asked_for_it_and_not_the_other() {
let (names, mut module) = module();
let mut opts = Options::for_level(OptLevel::O2);
opts.dumps.add("after-fold").expect("a pass that exists");
let report = super::run(&mut module, &names, &opts);
assert_eq!(report.dumps.len(), 1);
assert_eq!(report.dumps[0].name, "00-after-fold");
assert!(report.dumps[0].text.contains("iconst.i64 7"));
}
#[test]
fn asking_for_all_dumps_gives_both_sides_of_every_pass() {
let (interner, mut module) = module();
let opts = {
let mut opts = Options::for_level(OptLevel::O2);
opts.dumps.add("all").expect("all is always a dump");
opts
};
let report = super::run(&mut module, &interner, &opts);
let taken: Vec<&str> = report.dumps.iter().map(|d| d.name.as_str()).collect();
let expected: Vec<String> = names(&opts)
.into_iter()
.enumerate()
.flat_map(|(at, name)| {
[format!("{at:02}-before-{name}"), format!("{at:02}-after-{name}")]
})
.collect();
assert_eq!(taken, expected);
assert!(report.dumps[0].text.contains("sext.i64"));
assert!(!report.dumps[1].text.contains("sext.i64"));
}
#[test]
fn a_dump_of_a_pass_that_does_not_exist_is_refused_rather_than_ignored() {
let mut dumps = Dumps::default();
assert!(dumps.add("after-no-such-pass").is_err());
assert!(dumps.add("sideways-fold").is_err());
assert!(dumps.add("fold").is_err());
assert!(dumps.is_empty());
}
}