Skip to main content

opt_lang/
lib.rs

1//! # opt_lang
2//!
3//! SSA optimizations for [`ir_lang`] 2.0: the IR-to-IR stage of the `-lang`
4//! family's native compiler path, between the front end's lowering and
5//! instruction selection. It makes a program smaller and faster without
6//! changing what it computes, traps with, raises, or writes to memory.
7//!
8//! ## Quick start
9//!
10//! One call optimizes every function of a module:
11//!
12//! ```
13//! use ir_lang::{Linkage, Module, Overflow, Signature, Type};
14//!
15//! // fn f(x: i64) -> i64 { let a = 2 + 3; let b = x * a; let c = x * a; b + c }
16//! let mut m = Module::new("demo");
17//! let f = m.declare_function("f", &Signature::new(&[Type::I64], &[Type::I64]), Linkage::Export)?;
18//! let mut b = m.build(f)?;
19//! let x = b.param(0)?;
20//! let (two, three) = (b.iconst(Type::I64, 2)?, b.iconst(Type::I64, 3)?);
21//! let a = b.add(two, three, Overflow::Wrap)?;
22//! let p = b.mul(x, a, Overflow::Wrap)?;
23//! let q = b.mul(x, a, Overflow::Wrap)?;
24//! let r = b.add(p, q, Overflow::Wrap)?;
25//! b.ret(&[r])?;
26//!
27//! let stats = opt_lang::optimize(&mut m)?;
28//! assert_eq!((stats.insts_before(), stats.insts_after()), (6, 3));
29//! let text = m.display_function(f).to_string();
30//! assert!(text.contains("v1: i64 = const 5"), "{text}");
31//! assert!(text.contains("v2: i64 = mul<overflow=wrap> v0, v1"), "{text}");
32//! assert!(text.contains("v3: i64 = add<overflow=wrap> v2, v2"), "{text}");
33//! # Ok::<(), Box<dyn std::error::Error>>(())
34//! ```
35//!
36//! ## The passes
37//!
38//! | Pass | What it does |
39//! |---|---|
40//! | [`PassKind::SimplifyCfg`] | folds constant and trivial branches, removes unreachable blocks, threads jumps through empty blocks, merges straight-line blocks |
41//! | [`PassKind::Sccp`] | sparse conditional constant propagation over block parameters, `switch`, and `check` edges, folding with the exact OPS policies |
42//! | [`PassKind::CopyProp`] | replaces block parameters that always receive one value, and removes them |
43//! | [`PassKind::Gvn`] | dominator-based value numbering of pure operations |
44//! | [`PassKind::Licm`] | hoists pure loop-invariant operations into preheaders |
45//! | [`PassKind::Dce`] | removes dead instructions and dead block parameters, keeping every side effect |
46//!
47//! [`optimize`] runs [`DEFAULT_PIPELINE`] to a fixpoint. [`Optimizer`] chooses
48//! the passes, the number of sweeps, the budget, and validation; [`run_pass`]
49//! runs one pass on a [`Builder`](ir_lang::Builder) for callers with their own
50//! pipeline.
51//!
52//! ## Guarantees
53//!
54//! - **Semantics are preserved**: return values, traps and their codes, `check`
55//!   errors and their codes, exceptions, and memory effects (stores, atomics,
56//!   volatile accesses, calls, and their order) are unchanged. Constant folding
57//!   follows `specs/OPS.md` exactly, and an operation whose policy would raise at
58//!   run time is never folded into a value. Checked by differential tests that
59//!   run random programs before and after optimization in the ir-lang reference
60//!   interpreter.
61//! - **The IR stays valid**: every pass leaves the function valid under
62//!   [`ir_lang::Module::validate`], including the safepoint rules for GC
63//!   references. The validator runs after every pass in debug builds and tests.
64//! - **Bounded work**: every pass is iterative (no recursion over the input) and
65//!   draws from a [`Budget`]; a function whose budget runs out is left valid and
66//!   partly optimized.
67//!
68//! What is not done yet: no memory optimizations (loads are not value-numbered
69//! or forwarded, stores are not removed), no inlining, no SROA, no strength
70//! reduction, and `fma` is not constant-folded. See `dev/ROADMAP.md`.
71//!
72//! ## Features
73//!
74//! - `std` (default): the standard library; without it the crate is `#![no_std]`
75//!   and needs only `alloc`. Optimization results are identical either way
76//!   (folding implements IEEE rounding and square root itself).
77
78#![cfg_attr(not(feature = "std"), no_std)]
79#![cfg_attr(docsrs, feature(doc_cfg))]
80#![forbid(unsafe_code)]
81#![deny(missing_docs)]
82#![deny(unused_must_use)]
83#![deny(unused_results)]
84#![deny(clippy::unwrap_used)]
85#![deny(clippy::expect_used)]
86#![deny(clippy::todo)]
87#![deny(clippy::unimplemented)]
88#![deny(clippy::print_stdout)]
89#![deny(clippy::print_stderr)]
90#![deny(clippy::dbg_macro)]
91#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
92
93extern crate alloc;
94#[cfg(test)]
95extern crate std;
96
97mod analysis;
98mod budget;
99mod edit;
100mod error;
101mod float;
102mod fold;
103mod passes;
104mod pipeline;
105mod stats;
106
107pub use budget::Budget;
108pub use error::OptError;
109pub use pipeline::{
110    DEFAULT_MAX_ITERATIONS, DEFAULT_PIPELINE, DEFAULT_STEPS_PER_UNIT, Optimizer, PassKind, run_pass,
111};
112pub use stats::{PassStats, Stats};
113
114/// Optimizes every function of `module` with the default pipeline (Tier 1).
115///
116/// The same as `Optimizer::new().run(module)`: [`DEFAULT_PIPELINE`] run to a
117/// fixpoint (at most [`DEFAULT_MAX_ITERATIONS`] sweeps), with the default
118/// budget, validation between passes in debug builds, and changed functions
119/// compacted.
120///
121/// # Errors
122///
123/// [`OptError::InvalidInput`] if a function does not validate; the other
124/// variants only for a defect in a pass.
125///
126/// # Examples
127///
128/// ```
129/// use ir_lang::{Linkage, Module, Signature, Type};
130///
131/// let mut m = Module::new("m");
132/// let f = m.declare_function("f", &Signature::new(&[], &[Type::I32]), Linkage::Export)?;
133/// let mut b = m.build(f)?;
134/// let unused = b.iconst(Type::I32, 1)?;
135/// let v = b.iconst(Type::I32, 2)?;
136/// b.ret(&[v])?;
137/// # let _ = unused;
138/// let stats = opt_lang::optimize(&mut m)?;
139/// assert_eq!(stats.insts_after(), 1);
140/// m.validate()?;
141/// # Ok::<(), Box<dyn std::error::Error>>(())
142/// ```
143pub fn optimize(module: &mut ir_lang::Module) -> Result<Stats, OptError> {
144    Optimizer::new().run(module)
145}
146
147/// Runs the README's examples as doctests, so its claims stay true.
148#[cfg(doctest)]
149#[doc = include_str!("../README.md")]
150struct ReadmeDoctests;
151
152/// Runs the API reference's examples as doctests.
153#[cfg(doctest)]
154#[doc = include_str!("../docs/API.md")]
155struct ApiDoctests;