use crate::{
pass::PassManager, ConstProp, ConstantFold, DeadArgElim, DeadCodeElim, Gvn, Inliner, Ipcp,
LoopUnroll, Mem2Reg,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptLevel {
O0,
O1,
O2,
O3,
}
impl OptLevel {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"o0" | "0" => Some(Self::O0),
"o1" | "1" => Some(Self::O1),
"o2" | "2" => Some(Self::O2),
"o3" | "3" => Some(Self::O3),
_ => None,
}
}
}
pub fn build_pipeline(level: OptLevel) -> PassManager {
let mut pm = PassManager::new();
match level {
OptLevel::O0 => {
}
OptLevel::O1 => {
pm.add_function_pass(Mem2Reg);
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
}
OptLevel::O2 => {
pm.add_function_pass(Mem2Reg);
pm.add_module_pass(Inliner::default());
pm.add_function_pass(Gvn);
pm.add_function_pass(LoopUnroll::default());
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
pm.add_function_pass(Gvn);
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
}
OptLevel::O3 => {
pm.add_function_pass(Mem2Reg);
pm.add_module_pass(Ipcp);
pm.add_module_pass(DeadArgElim);
pm.add_module_pass(Inliner {
size_limit: 100,
max_inline_depth: 16,
hot_loop_bonus: 100,
});
pm.add_function_pass(Gvn);
pm.add_function_pass(LoopUnroll {
factor: 8,
max_trip_count: 16,
});
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
pm.add_function_pass(Gvn);
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
pm.add_function_pass(ConstantFold);
pm.add_function_pass(ConstProp);
pm.add_function_pass(DeadCodeElim);
}
}
pm
}
#[cfg(test)]
mod tests {
use super::*;
use llvm_ir::{Builder, Context, InstrKind, Linkage, Module, ValueRef};
use llvm_ir_parser::parser::parse;
fn make_dead_code_fn() -> (Context, Module) {
let mut ctx = Context::new();
let mut module = Module::new("test");
let mut b = Builder::new(&mut ctx, &mut module);
b.add_function(
"main",
b.ctx.i32_ty,
vec![],
vec![],
false,
Linkage::External,
);
let entry = b.add_block("entry");
b.position_at_end(entry);
let c1 = b.const_int(b.ctx.i32_ty, 1);
let c2 = b.const_int(b.ctx.i32_ty, 2);
let c100 = b.const_int(b.ctx.i32_ty, 100);
let c7 = b.const_int(b.ctx.i32_ty, 7);
let dead1 = b.build_add("dead1", c1, c2);
let _dead2 = b.build_mul("dead2", dead1, c100);
b.build_ret(c7);
(ctx, module)
}
#[test]
fn o2_preserves_return_semantics_and_reduces_body_size_vs_o0() {
let (mut ctx_o0, mut m_o0) = make_dead_code_fn();
let (mut ctx_o2, mut m_o2) = make_dead_code_fn();
let mut pm_o0 = build_pipeline(OptLevel::O0);
let mut pm_o2 = build_pipeline(OptLevel::O2);
pm_o0.run_until_fixed_point(&mut ctx_o0, &mut m_o0, 3);
pm_o2.run_until_fixed_point(&mut ctx_o2, &mut m_o2, 8);
let f0 = &m_o0.functions[0];
let f2 = &m_o2.functions[0];
let o0_body_len = f0.blocks[0].body.len();
let o2_body_len = f2.blocks[0].body.len();
assert!(
o2_body_len < o0_body_len,
"O2 should reduce instruction count (o0={}, o2={})",
o0_body_len,
o2_body_len
);
let t0 = f0.blocks[0].terminator.expect("o0 terminator");
let t2 = f2.blocks[0].terminator.expect("o2 terminator");
match (&f0.instr(t0).kind, &f2.instr(t2).kind) {
(InstrKind::Ret { val: Some(v0) }, InstrKind::Ret { val: Some(v2) }) => {
assert_eq!(
*v0,
ValueRef::Constant(ctx_o0.const_int(ctx_o0.i32_ty, 7)),
"o0 should still return constant 7"
);
assert_eq!(
*v2,
ValueRef::Constant(ctx_o2.const_int(ctx_o2.i32_ty, 7)),
"o2 should return constant 7"
);
}
_ => panic!("expected both pipelines to end with ret i32 7"),
}
}
#[test]
fn parse_opt_level_variants() {
assert_eq!(OptLevel::parse("O0"), Some(OptLevel::O0));
assert_eq!(OptLevel::parse("1"), Some(OptLevel::O1));
assert_eq!(OptLevel::parse("o2"), Some(OptLevel::O2));
assert_eq!(OptLevel::parse(" 3 "), Some(OptLevel::O3));
assert_eq!(OptLevel::parse("Ox"), None);
}
#[test]
fn o1_folds_trivial_constant_expression() {
let mut ctx = Context::new();
let mut module = Module::new("fold");
let mut b = Builder::new(&mut ctx, &mut module);
b.add_function(
"main",
b.ctx.i32_ty,
vec![],
vec![],
false,
Linkage::External,
);
let entry = b.add_block("entry");
b.position_at_end(entry);
let c2 = b.const_int(b.ctx.i32_ty, 2);
let sum = b.build_add("sum", c2, c2);
b.build_ret(sum);
let mut pm = build_pipeline(OptLevel::O1);
pm.run_until_fixed_point(&mut ctx, &mut module, 4);
assert_eq!(module.functions[0].blocks[0].body.len(), 0);
let tid = module.functions[0].blocks[0].terminator.expect("ret");
match &module.functions[0].instr(tid).kind {
InstrKind::Ret {
val: Some(ValueRef::Constant(cid)),
} => match ctx.get_const(*cid) {
llvm_ir::ConstantData::Int { val, .. } => assert_eq!(*val, 4),
other => panic!("unexpected constant kind: {other:?}"),
},
other => panic!("expected constant return after O1, got {other:?}"),
}
}
fn parse_dbg_fixture() -> (Context, Module) {
let src = r#"
source_filename = "pipeline_dbg.c"
define i32 @f(i32 %a, i32 %b) {
entry:
%s = add i32 %a, %b, !dbg !12
ret i32 %s, !dbg !13
}
!llvm.dbg.cu = !{!0}
!0 = !DICompileUnit(language: DW_LANG_C99, file: !1)
!1 = !DIFile(filename: "pipeline_dbg.c", directory: ".")
!12 = !DILocation(line: 10, column: 2, scope: !0)
!13 = !DILocation(line: 11, column: 3, scope: !0)
"#;
parse(src).expect("parse debug fixture")
}
fn assert_debug_metadata_survives(module: &Module) {
let f = &module.functions[0];
assert!(
!f.instr_dbg_locs.is_empty(),
"function should retain at least one !dbg location"
);
assert!(
module
.named_metadata
.iter()
.any(|(k, _)| k == "llvm.dbg.cu"),
"named metadata llvm.dbg.cu must be preserved"
);
for loc_id in f.instr_dbg_locs.values() {
assert!(
module.debug_location(*loc_id).is_some(),
"missing debug location for !dbg !{}",
loc_id
);
}
}
#[test]
fn debug_metadata_survives_o1_o2_o3_pipelines() {
for level in [OptLevel::O1, OptLevel::O2, OptLevel::O3] {
let (mut ctx, mut module) = parse_dbg_fixture();
let mut pm = build_pipeline(level);
pm.run_until_fixed_point(&mut ctx, &mut module, 8);
assert_debug_metadata_survives(&module);
}
}
}