Skip to main content

llvm_transforms/
pipeline.rs

1//! Standard optimization pipelines (`-O0` through `-O3`).
2//!
3//! These presets provide a stable public API for frontends/examples to avoid
4//! manually assembling pass sequences.
5
6use crate::{
7    pass::PassManager, ConstProp, ConstantFold, DeadArgElim, DeadCodeElim, Gvn, Inliner, Ipcp,
8    LoopUnroll, Mem2Reg,
9};
10
11/// Optimization level preset.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum OptLevel {
14    /// `O0` variant.
15    O0,
16    /// `O1` variant.
17    O1,
18    /// `O2` variant.
19    O2,
20    /// `O3` variant.
21    O3,
22}
23
24impl OptLevel {
25    /// Parse command-line style strings such as `"O2"` or `"2"`.
26    pub fn parse(s: &str) -> Option<Self> {
27        match s.trim().to_ascii_lowercase().as_str() {
28            "o0" | "0" => Some(Self::O0),
29            "o1" | "1" => Some(Self::O1),
30            "o2" | "2" => Some(Self::O2),
31            "o3" | "3" => Some(Self::O3),
32            _ => None,
33        }
34    }
35}
36
37/// Build a pass pipeline for the requested optimization level.
38///
39/// Current implementation uses passes available in this repository today.
40pub fn build_pipeline(level: OptLevel) -> PassManager {
41    let mut pm = PassManager::new();
42
43    match level {
44        OptLevel::O0 => {
45            // Intentionally empty.
46        }
47        OptLevel::O1 => {
48            pm.add_function_pass(Mem2Reg);
49            pm.add_function_pass(ConstantFold);
50            pm.add_function_pass(ConstProp);
51            pm.add_function_pass(DeadCodeElim);
52        }
53        OptLevel::O2 => {
54            pm.add_function_pass(Mem2Reg);
55            pm.add_module_pass(Inliner::default());
56            pm.add_function_pass(Gvn);
57            pm.add_function_pass(LoopUnroll::default());
58            pm.add_function_pass(ConstantFold);
59            pm.add_function_pass(ConstProp);
60            pm.add_function_pass(DeadCodeElim);
61            // Clean up after inlining.
62            pm.add_function_pass(Gvn);
63            pm.add_function_pass(ConstantFold);
64            pm.add_function_pass(ConstProp);
65            pm.add_function_pass(DeadCodeElim);
66        }
67        OptLevel::O3 => {
68            pm.add_function_pass(Mem2Reg);
69            pm.add_module_pass(Ipcp);
70            pm.add_module_pass(DeadArgElim);
71            pm.add_module_pass(Inliner {
72                size_limit: 100,
73                max_inline_depth: 16,
74                hot_loop_bonus: 100,
75            });
76            pm.add_function_pass(Gvn);
77            pm.add_function_pass(LoopUnroll {
78                factor: 8,
79                max_trip_count: 16,
80            });
81            pm.add_function_pass(ConstantFold);
82            pm.add_function_pass(ConstProp);
83            pm.add_function_pass(DeadCodeElim);
84            // Extra cleanup rounds as a placeholder for future aggressive O3.
85            pm.add_function_pass(Gvn);
86            pm.add_function_pass(ConstantFold);
87            pm.add_function_pass(ConstProp);
88            pm.add_function_pass(DeadCodeElim);
89            pm.add_function_pass(ConstantFold);
90            pm.add_function_pass(ConstProp);
91            pm.add_function_pass(DeadCodeElim);
92        }
93    }
94
95    pm
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use llvm_ir::{Builder, Context, InstrKind, Linkage, Module, ValueRef};
102    use llvm_ir_parser::parser::parse;
103
104    fn make_dead_code_fn() -> (Context, Module) {
105        let mut ctx = Context::new();
106        let mut module = Module::new("test");
107        let mut b = Builder::new(&mut ctx, &mut module);
108        b.add_function(
109            "main",
110            b.ctx.i32_ty,
111            vec![],
112            vec![],
113            false,
114            Linkage::External,
115        );
116        let entry = b.add_block("entry");
117        b.position_at_end(entry);
118
119        let c1 = b.const_int(b.ctx.i32_ty, 1);
120        let c2 = b.const_int(b.ctx.i32_ty, 2);
121        let c100 = b.const_int(b.ctx.i32_ty, 100);
122        let c7 = b.const_int(b.ctx.i32_ty, 7);
123
124        let dead1 = b.build_add("dead1", c1, c2);
125        let _dead2 = b.build_mul("dead2", dead1, c100);
126        b.build_ret(c7);
127
128        (ctx, module)
129    }
130
131    #[test]
132    fn o2_preserves_return_semantics_and_reduces_body_size_vs_o0() {
133        let (mut ctx_o0, mut m_o0) = make_dead_code_fn();
134        let (mut ctx_o2, mut m_o2) = make_dead_code_fn();
135
136        let mut pm_o0 = build_pipeline(OptLevel::O0);
137        let mut pm_o2 = build_pipeline(OptLevel::O2);
138        pm_o0.run_until_fixed_point(&mut ctx_o0, &mut m_o0, 3);
139        pm_o2.run_until_fixed_point(&mut ctx_o2, &mut m_o2, 8);
140
141        let f0 = &m_o0.functions[0];
142        let f2 = &m_o2.functions[0];
143
144        let o0_body_len = f0.blocks[0].body.len();
145        let o2_body_len = f2.blocks[0].body.len();
146        assert!(
147            o2_body_len < o0_body_len,
148            "O2 should reduce instruction count (o0={}, o2={})",
149            o0_body_len,
150            o2_body_len
151        );
152
153        let t0 = f0.blocks[0].terminator.expect("o0 terminator");
154        let t2 = f2.blocks[0].terminator.expect("o2 terminator");
155
156        match (&f0.instr(t0).kind, &f2.instr(t2).kind) {
157            (InstrKind::Ret { val: Some(v0) }, InstrKind::Ret { val: Some(v2) }) => {
158                assert_eq!(
159                    *v0,
160                    ValueRef::Constant(ctx_o0.const_int(ctx_o0.i32_ty, 7)),
161                    "o0 should still return constant 7"
162                );
163                assert_eq!(
164                    *v2,
165                    ValueRef::Constant(ctx_o2.const_int(ctx_o2.i32_ty, 7)),
166                    "o2 should return constant 7"
167                );
168            }
169            _ => panic!("expected both pipelines to end with ret i32 7"),
170        }
171    }
172
173    #[test]
174    fn parse_opt_level_variants() {
175        assert_eq!(OptLevel::parse("O0"), Some(OptLevel::O0));
176        assert_eq!(OptLevel::parse("1"), Some(OptLevel::O1));
177        assert_eq!(OptLevel::parse("o2"), Some(OptLevel::O2));
178        assert_eq!(OptLevel::parse(" 3 "), Some(OptLevel::O3));
179        assert_eq!(OptLevel::parse("Ox"), None);
180    }
181
182    #[test]
183    fn o1_folds_trivial_constant_expression() {
184        let mut ctx = Context::new();
185        let mut module = Module::new("fold");
186        let mut b = Builder::new(&mut ctx, &mut module);
187        b.add_function(
188            "main",
189            b.ctx.i32_ty,
190            vec![],
191            vec![],
192            false,
193            Linkage::External,
194        );
195        let entry = b.add_block("entry");
196        b.position_at_end(entry);
197        let c2 = b.const_int(b.ctx.i32_ty, 2);
198        let sum = b.build_add("sum", c2, c2);
199        b.build_ret(sum);
200
201        let mut pm = build_pipeline(OptLevel::O1);
202        pm.run_until_fixed_point(&mut ctx, &mut module, 4);
203        assert_eq!(module.functions[0].blocks[0].body.len(), 0);
204        let tid = module.functions[0].blocks[0].terminator.expect("ret");
205        match &module.functions[0].instr(tid).kind {
206            InstrKind::Ret {
207                val: Some(ValueRef::Constant(cid)),
208            } => match ctx.get_const(*cid) {
209                llvm_ir::ConstantData::Int { val, .. } => assert_eq!(*val, 4),
210                other => panic!("unexpected constant kind: {other:?}"),
211            },
212            other => panic!("expected constant return after O1, got {other:?}"),
213        }
214    }
215
216    fn parse_dbg_fixture() -> (Context, Module) {
217        let src = r#"
218source_filename = "pipeline_dbg.c"
219define i32 @f(i32 %a, i32 %b) {
220entry:
221  %s = add i32 %a, %b, !dbg !12
222  ret i32 %s, !dbg !13
223}
224!llvm.dbg.cu = !{!0}
225!0 = !DICompileUnit(language: DW_LANG_C99, file: !1)
226!1 = !DIFile(filename: "pipeline_dbg.c", directory: ".")
227!12 = !DILocation(line: 10, column: 2, scope: !0)
228!13 = !DILocation(line: 11, column: 3, scope: !0)
229"#;
230        parse(src).expect("parse debug fixture")
231    }
232
233    fn assert_debug_metadata_survives(module: &Module) {
234        let f = &module.functions[0];
235        assert!(
236            !f.instr_dbg_locs.is_empty(),
237            "function should retain at least one !dbg location"
238        );
239        assert!(
240            module
241                .named_metadata
242                .iter()
243                .any(|(k, _)| k == "llvm.dbg.cu"),
244            "named metadata llvm.dbg.cu must be preserved"
245        );
246
247        for loc_id in f.instr_dbg_locs.values() {
248            assert!(
249                module.debug_location(*loc_id).is_some(),
250                "missing debug location for !dbg !{}",
251                loc_id
252            );
253        }
254    }
255
256    #[test]
257    fn debug_metadata_survives_o1_o2_o3_pipelines() {
258        for level in [OptLevel::O1, OptLevel::O2, OptLevel::O3] {
259            let (mut ctx, mut module) = parse_dbg_fixture();
260            let mut pm = build_pipeline(level);
261            pm.run_until_fixed_point(&mut ctx, &mut module, 8);
262            assert_debug_metadata_survives(&module);
263        }
264    }
265}