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}