Skip to main content

opt_lang/
pipeline.rs

1//! The pass pipeline: which passes run, in what order, how often, and with what
2//! checks between them.
3//!
4//! The pipeline is a [`pass_lang::PassManager`] over one function at a time,
5//! run to a fixpoint (bounded by [`Optimizer::max_iterations`]). pass-lang's
6//! [`PassError`] carries only a message, so the typed [`OptError`] of a failing
7//! pass travels beside it in the session and is returned instead.
8
9use alloc::vec::Vec;
10use core::fmt;
11
12use ir_lang::{Builder, FuncId, Module};
13use pass_lang::{Outcome, Pass, PassError, PassManager};
14
15use crate::analysis::size;
16use crate::passes::{copy_prop, dce, gvn, licm, sccp, simplify_cfg};
17use crate::{Budget, OptError, Stats};
18
19/// One optimization pass.
20///
21/// Every pass leaves the function valid, terminates within a bound linear in
22/// the function (plus the budgeted parts named below), and reports whether it
23/// changed anything.
24///
25/// # Examples
26///
27/// ```
28/// use opt_lang::PassKind;
29///
30/// assert_eq!(PassKind::Sccp.name(), "sccp");
31/// assert_eq!(PassKind::from_name("simplify-cfg"), Some(PassKind::SimplifyCfg));
32/// assert_eq!(PassKind::ALL.len(), 6);
33/// ```
34#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
35#[non_exhaustive]
36pub enum PassKind {
37    /// CFG simplification: fold branches on constants and identical edges,
38    /// remove unreachable blocks, thread jumps through empty blocks, merge
39    /// straight-line blocks. Linear per round, at most 8 rounds per run.
40    SimplifyCfg,
41    /// Sparse conditional constant propagation over block parameters, including
42    /// `switch` and `check` edges. Linear.
43    Sccp,
44    /// Copy propagation and block-parameter elimination (a parameter that
45    /// always receives the same value is replaced by it). Linear, plus budgeted
46    /// re-examination.
47    CopyProp,
48    /// Dominator-based global value numbering of pure operations, with a few
49    /// exact integer identities. Linear.
50    Gvn,
51    /// Loop-invariant code motion of pure operations into preheaders. Linear
52    /// plus the (budgeted) sizes of nested loop bodies.
53    Licm,
54    /// Dead-code elimination, side-effect aware, including dead block
55    /// parameters. Linear.
56    Dce,
57}
58
59impl PassKind {
60    /// Every pass, in a fixed order.
61    pub const ALL: [PassKind; 6] = [
62        PassKind::SimplifyCfg,
63        PassKind::Sccp,
64        PassKind::CopyProp,
65        PassKind::Gvn,
66        PassKind::Licm,
67        PassKind::Dce,
68    ];
69
70    /// The pass's name (`simplify-cfg`, `sccp`, `copy-prop`, `gvn`, `licm`,
71    /// `dce`).
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// assert_eq!(opt_lang::PassKind::CopyProp.name(), "copy-prop");
77    /// ```
78    #[must_use]
79    pub const fn name(self) -> &'static str {
80        match self {
81            PassKind::SimplifyCfg => "simplify-cfg",
82            PassKind::Sccp => "sccp",
83            PassKind::CopyProp => "copy-prop",
84            PassKind::Gvn => "gvn",
85            PassKind::Licm => "licm",
86            PassKind::Dce => "dce",
87        }
88    }
89
90    /// Looks a pass up by its name.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// assert_eq!(opt_lang::PassKind::from_name("gvn"), Some(opt_lang::PassKind::Gvn));
96    /// assert_eq!(opt_lang::PassKind::from_name("inline"), None);
97    /// ```
98    #[must_use]
99    pub fn from_name(name: &str) -> Option<PassKind> {
100        PassKind::ALL.into_iter().find(|p| p.name() == name)
101    }
102}
103
104impl fmt::Display for PassKind {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.write_str(self.name())
107    }
108}
109
110/// The default pipeline, run to a fixpoint: simplify the CFG, propagate
111/// constants, propagate copies, number values, hoist invariants, delete dead
112/// code, and simplify the CFG again.
113///
114/// # Examples
115///
116/// ```
117/// use opt_lang::{DEFAULT_PIPELINE, PassKind};
118///
119/// assert_eq!(DEFAULT_PIPELINE.len(), 7);
120/// assert_eq!(DEFAULT_PIPELINE[1], PassKind::Sccp);
121/// ```
122pub const DEFAULT_PIPELINE: &[PassKind] = &[
123    PassKind::SimplifyCfg,
124    PassKind::Sccp,
125    PassKind::CopyProp,
126    PassKind::Gvn,
127    PassKind::Licm,
128    PassKind::Dce,
129    PassKind::SimplifyCfg,
130];
131
132/// The default number of pipeline sweeps per function.
133///
134/// # Examples
135///
136/// ```
137/// assert_eq!(opt_lang::DEFAULT_MAX_ITERATIONS, 4);
138/// ```
139pub const DEFAULT_MAX_ITERATIONS: usize = 4;
140
141/// The default budget per function, in steps per unit of size (instructions +
142/// blocks + values). Far above what the linear passes need; it only binds on
143/// inputs built to trigger the non-linear parts.
144///
145/// # Examples
146///
147/// ```
148/// assert_eq!(opt_lang::DEFAULT_STEPS_PER_UNIT, 256);
149/// ```
150pub const DEFAULT_STEPS_PER_UNIT: u64 = 256;
151
152fn default_budget(func: &ir_lang::Function) -> u64 {
153    let units = (func.inst_count() + func.block_count() + func.value_count()) as u64;
154    units
155        .saturating_mul(DEFAULT_STEPS_PER_UNIT)
156        .saturating_add(1 << 16)
157}
158
159fn execute(b: &mut Builder<'_>, pass: PassKind, budget: &mut Budget) -> Result<bool, OptError> {
160    // Operands are read through forwarding; flattening it first keeps every
161    // read O(1) (a no-op scan when there is none).
162    b.resolve_aliases();
163    let r = match pass {
164        PassKind::SimplifyCfg => simplify_cfg::run(b, budget),
165        PassKind::Sccp => sccp::run(b, budget),
166        PassKind::CopyProp => copy_prop::run(b, budget),
167        PassKind::Gvn => gvn::run(b, budget),
168        PassKind::Licm => licm::run(b, budget),
169        PassKind::Dce => dce::run(b, budget),
170    };
171    r.map_err(|error| OptError::Build {
172        func: b.id(),
173        pass,
174        error,
175    })
176}
177
178/// Runs one pass on the function being edited, for callers that build their
179/// own pipeline (Tier 3).
180///
181/// The function is validated first (passes require valid input); after the
182/// pass it is validated again in debug builds. Returns whether the pass changed
183/// anything. A pass the budget cannot pay for does nothing and returns
184/// `Ok(false)`, with [`Budget::is_exhausted`] set.
185///
186/// # Errors
187///
188/// [`OptError::InvalidInput`] if the function does not validate (nothing is
189/// changed); [`OptError::InvalidOutput`] or [`OptError::Build`] for a defect in
190/// a pass.
191///
192/// # Examples
193///
194/// ```
195/// use ir_lang::{Linkage, Module, Overflow, Signature, Type};
196/// use opt_lang::{Budget, PassKind};
197///
198/// let mut m = Module::new("m");
199/// let f = m.declare_function("f", &Signature::new(&[Type::I64], &[Type::I64]), Linkage::Export)?;
200/// let mut b = m.build(f)?;
201/// let x = b.param(0)?;
202/// let a = b.add(x, x, Overflow::Wrap)?;
203/// let again = b.add(x, x, Overflow::Wrap)?; // the same value
204/// let y = b.mul(a, again, Overflow::Wrap)?;
205/// b.ret(&[y])?;
206///
207/// let mut budget = Budget::unlimited();
208/// assert!(opt_lang::run_pass(&mut m.edit(f)?, PassKind::Gvn, &mut budget)?);
209/// assert!(!opt_lang::run_pass(&mut m.edit(f)?, PassKind::Gvn, &mut budget)?);
210/// assert!(m.to_string().contains("mul<overflow=wrap> v1, v1"));
211/// # Ok::<(), Box<dyn std::error::Error>>(())
212/// ```
213pub fn run_pass(
214    b: &mut Builder<'_>,
215    pass: PassKind,
216    budget: &mut Budget,
217) -> Result<bool, OptError> {
218    b.validate().map_err(|error| OptError::InvalidInput {
219        func: b.id(),
220        error,
221    })?;
222    let changed = execute(b, pass, budget)?;
223    if changed && cfg!(debug_assertions) {
224        b.validate().map_err(|error| OptError::InvalidOutput {
225            func: b.id(),
226            pass,
227            error,
228        })?;
229    }
230    Ok(changed)
231}
232
233/// The unit the pass manager runs on: the module, the function being
234/// optimized, and the state passes share.
235struct Session<'m> {
236    module: &'m mut Module,
237    func: FuncId,
238    budget: Budget,
239    validate: bool,
240    /// Whether handles may be renumbered (the caller asked for compaction).
241    compact: bool,
242    failure: Option<OptError>,
243    stats: Stats,
244}
245
246/// Compacts the function when most of its arena is tombstones, so later passes
247/// walk dense tables. Only when the caller allowed renumbering.
248fn maybe_compact(b: &mut Builder<'_>) {
249    let func = b.func();
250    let (insts, blocks) = size(func);
251    if func.inst_count() > 2 * insts + 64 || func.block_count() > 2 * blocks + 64 {
252        let _ = b.compact();
253    }
254}
255
256/// A pipeline step: one pass, wrapped for pass-lang, with validation after it.
257struct Step(PassKind);
258
259impl Pass<Session<'_>> for Step {
260    fn name(&self) -> &'static str {
261        self.0.name()
262    }
263
264    fn run(&mut self, s: &mut Session<'_>) -> Result<Outcome, PassError> {
265        let pass = self.0;
266        let func = s.func;
267        let result = match s.module.edit(func) {
268            Ok(mut b) => execute(&mut b, pass, &mut s.budget).and_then(|changed| {
269                if changed && s.validate {
270                    b.validate()
271                        .map_err(|error| OptError::InvalidOutput { func, pass, error })?;
272                }
273                if changed && s.compact {
274                    maybe_compact(&mut b);
275                }
276                Ok(changed)
277            }),
278            Err(e) => Err(OptError::Module(e)),
279        };
280        match result {
281            Ok(changed) => {
282                s.stats.record(pass, changed);
283                Ok(Outcome::from_changed(changed))
284            }
285            Err(e) => {
286                s.failure = Some(e);
287                Err(PassError::new("see the session's typed error"))
288            }
289        }
290    }
291}
292
293/// The configured optimizer (Tier 2): which passes, how many sweeps, what
294/// budget, and whether to validate between passes.
295///
296/// # Examples
297///
298/// ```
299/// use ir_lang::{Linkage, Module, Overflow, Signature, Type};
300/// use opt_lang::{Optimizer, PassKind};
301///
302/// let mut m = Module::new("m");
303/// let f = m.declare_function("f", &Signature::new(&[Type::I64], &[Type::I64]), Linkage::Export)?;
304/// let mut b = m.build(f)?;
305/// let x = b.param(0)?;
306/// let unused = b.add(x, x, Overflow::Wrap)?;
307/// b.ret(&[x])?;
308/// # let _ = unused;
309///
310/// let stats = Optimizer::new()
311///     .passes(&[PassKind::Dce])
312///     .max_iterations(2)
313///     .validate(true)
314///     .run(&mut m)?;
315/// assert_eq!((stats.insts_before(), stats.insts_after()), (1, 0));
316/// # Ok::<(), Box<dyn std::error::Error>>(())
317/// ```
318#[derive(Clone, PartialEq, Eq, Debug)]
319pub struct Optimizer {
320    passes: Vec<PassKind>,
321    max_iterations: usize,
322    budget: Option<u64>,
323    validate: bool,
324    compact: bool,
325}
326
327impl Default for Optimizer {
328    fn default() -> Self {
329        Optimizer::new()
330    }
331}
332
333impl Optimizer {
334    /// The default configuration: [`DEFAULT_PIPELINE`],
335    /// [`DEFAULT_MAX_ITERATIONS`] sweeps, a budget of
336    /// [`DEFAULT_STEPS_PER_UNIT`] steps per unit of function size, validation
337    /// between passes in debug builds only, and compaction of every changed
338    /// function.
339    ///
340    /// # Examples
341    ///
342    /// ```
343    /// let opt = opt_lang::Optimizer::new();
344    /// # let _ = opt;
345    /// ```
346    #[must_use]
347    pub fn new() -> Optimizer {
348        Optimizer {
349            passes: DEFAULT_PIPELINE.to_vec(),
350            max_iterations: DEFAULT_MAX_ITERATIONS,
351            budget: None,
352            validate: cfg!(debug_assertions),
353            compact: true,
354        }
355    }
356
357    /// The passes to run, in order, each sweep. An empty list does nothing.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use opt_lang::{Optimizer, PassKind};
363    ///
364    /// let cleanup = Optimizer::new().passes(&[PassKind::Dce, PassKind::SimplifyCfg]);
365    /// # let _ = cleanup;
366    /// ```
367    #[must_use]
368    pub fn passes(mut self, passes: &[PassKind]) -> Optimizer {
369        self.passes = passes.to_vec();
370        self
371    }
372
373    /// The most sweeps per function; the pipeline stops earlier at its fixpoint
374    /// (a sweep that changes nothing). `0` runs nothing.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use ir_lang::{Linkage, Module, Signature};
380    ///
381    /// let mut m = Module::new("m");
382    /// let f = m.declare_function("f", &Signature::new(&[], &[]), Linkage::Export)?;
383    /// m.build(f)?.ret(&[])?;
384    /// let stats = opt_lang::Optimizer::new().max_iterations(0).run(&mut m)?;
385    /// assert_eq!(stats.iterations(), 0);
386    /// # Ok::<(), Box<dyn std::error::Error>>(())
387    /// ```
388    #[must_use]
389    pub fn max_iterations(mut self, n: usize) -> Optimizer {
390        self.max_iterations = n;
391        self
392    }
393
394    /// A fixed budget of `steps` per function, instead of the default
395    /// size-proportional one.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use ir_lang::{Linkage, Module, Signature};
401    ///
402    /// let mut m = Module::new("m");
403    /// let f = m.declare_function("f", &Signature::new(&[], &[]), Linkage::Export)?;
404    /// m.build(f)?.ret(&[])?;
405    /// let stats = opt_lang::Optimizer::new().budget(0).run(&mut m)?;
406    /// assert_eq!(stats.budget_exhausted(), 1);
407    /// # Ok::<(), Box<dyn std::error::Error>>(())
408    /// ```
409    #[must_use]
410    pub fn budget(mut self, steps: u64) -> Optimizer {
411        self.budget = Some(steps);
412        self
413    }
414
415    /// Whether to run the ir-lang validator after every pass that changed the
416    /// function (default: on in debug builds, off in release builds). The input
417    /// is always validated.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// let checked = opt_lang::Optimizer::new().validate(true);
423    /// assert_ne!(checked, opt_lang::Optimizer::new().validate(false));
424    /// ```
425    #[must_use]
426    pub fn validate(mut self, on: bool) -> Optimizer {
427        self.validate = on;
428        self
429    }
430
431    /// Whether to compact every changed function at the end (default on):
432    /// renumber values, blocks, and instructions densely and drop removed
433    /// entities. With it off, handles stay stable and removed entities remain as
434    /// tombstones.
435    ///
436    /// # Examples
437    ///
438    /// ```
439    /// let opt = opt_lang::Optimizer::new().compact(false);
440    /// # let _ = opt;
441    /// ```
442    #[must_use]
443    pub fn compact(mut self, on: bool) -> Optimizer {
444        self.compact = on;
445        self
446    }
447
448    /// Optimizes every function with a body.
449    ///
450    /// # Errors
451    ///
452    /// [`OptError::InvalidInput`] if a function does not validate (functions
453    /// before it are already optimized; it and the ones after are untouched);
454    /// [`OptError::InvalidOutput`] or [`OptError::Build`] for a defect in a
455    /// pass.
456    ///
457    /// See [`Optimizer`] for an example.
458    pub fn run(&self, module: &mut Module) -> Result<Stats, OptError> {
459        let funcs: Vec<FuncId> = module
460            .functions()
461            .filter(|&f| module.function(f).is_some())
462            .collect();
463        self.run_on(module, &funcs)
464    }
465
466    /// Optimizes one function.
467    ///
468    /// # Errors
469    ///
470    /// [`OptError::Module`] if `func` is unknown or has no body; otherwise as
471    /// [`run`](Optimizer::run).
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use ir_lang::{Linkage, Module, Signature, Type};
477    ///
478    /// let mut m = Module::new("m");
479    /// let f = m.declare_function("f", &Signature::new(&[], &[Type::U8]), Linkage::Export)?;
480    /// let mut b = m.build(f)?;
481    /// let v = b.iconst(Type::U8, 7)?;
482    /// let _dead = b.iconst(Type::U8, 8)?;
483    /// b.ret(&[v])?;
484    /// let stats = opt_lang::Optimizer::new().run_function(&mut m, f)?;
485    /// assert_eq!(stats.insts_after(), 1);
486    /// # Ok::<(), Box<dyn std::error::Error>>(())
487    /// ```
488    pub fn run_function(&self, module: &mut Module, func: FuncId) -> Result<Stats, OptError> {
489        if module.function(func).is_none() {
490            let _ = module.edit(func)?;
491        }
492        self.run_on(module, &[func])
493    }
494
495    fn run_on(&self, module: &mut Module, funcs: &[FuncId]) -> Result<Stats, OptError> {
496        let mut total = Stats::default();
497        let first = match funcs.first() {
498            Some(&f) => f,
499            None => return Ok(total),
500        };
501        let mut pm: PassManager<Session<'_>> = PassManager::new();
502        for &p in &self.passes {
503            let _ = pm.add(Step(p));
504        }
505        let mut s = Session {
506            module,
507            func: first,
508            budget: Budget::unlimited(),
509            validate: self.validate,
510            compact: self.compact,
511            failure: None,
512            stats: Stats::default(),
513        };
514        for &f in funcs {
515            s.module
516                .validate_function(f)
517                .map_err(|error| OptError::InvalidInput { func: f, error })?;
518            let Some(body) = s.module.function(f) else {
519                continue;
520            };
521            let (insts, blocks) = size(body);
522            let steps = self.budget.unwrap_or_else(|| default_budget(body));
523            s.func = f;
524            s.budget = Budget::new(steps);
525            s.stats = Stats::default();
526            let report = match pm.run_to_fixpoint(&mut s, self.max_iterations) {
527                Ok(r) => r,
528                Err(e) => {
529                    // A step records its typed error before failing, so the
530                    // fallback (naming the failing pass) is never used.
531                    return Err(s.failure.take().unwrap_or(OptError::Build {
532                        func: f,
533                        pass: PassKind::from_name(e.pass()).unwrap_or(PassKind::Dce),
534                        error: ir_lang::BuildError::UnknownBlock {
535                            block: ir_lang::Block::from_u32(u32::MAX),
536                        },
537                    }));
538                }
539            };
540            if self.compact && report.changes() > 0 {
541                let _ = s.module.edit(f)?.compact();
542            }
543            let (insts_after, blocks_after) = s.module.function(f).map_or((0, 0), size);
544            let mut st = core::mem::take(&mut s.stats);
545            st.functions = 1;
546            st.insts_before = insts;
547            st.blocks_before = blocks;
548            st.insts_after = insts_after;
549            st.blocks_after = blocks_after;
550            st.iterations = report.iterations();
551            st.converged = usize::from(report.converged());
552            st.budget_exhausted = usize::from(s.budget.is_exhausted());
553            total.merge(&st);
554        }
555        Ok(total)
556    }
557}