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 (OPS v2 `pow` and `saturate` included), also when only some operands are known |
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//!   A `check` becomes a plain instruction only when ir-lang's placement rule
65//!   (`can_error`) accepts it; an error edge that can be taken is never
66//!   removed.
67//! - **Statepoints are kept by construction**: no pass moves a call or
68//!   safepoint, edits a GC list, or makes a `ref` (or a value derived from one)
69//!   a constant, numbered, or hoisted, so input whose statepoints came from
70//!   `Builder::insert_statepoints` stays valid without running it again. The
71//!   tests check that running it on optimized output adds nothing.
72//! - **Bounded work**: every pass is iterative (no recursion over the input) and
73//!   draws from a [`Budget`]; a function whose budget runs out is left valid and
74//!   partly optimized.
75//!
76//! What is not done yet: no memory optimizations (loads are not value-numbered
77//! or forwarded, stores are not removed), no inlining, no SROA, no strength
78//! reduction beyond `x ** 2` to `x * x`, and `fma` is not constant-folded.
79//! See `dev/ROADMAP.md`.
80//!
81//! The IR is `ir-lang` `=2.0.0-alpha.3`; callers must use the same release.
82//!
83//! ## Features
84//!
85//! - `std` (default): the standard library; without it the crate is `#![no_std]`
86//!   and needs only `alloc`. Optimization results are identical either way
87//!   (folding implements IEEE rounding and square root itself).
88
89#![cfg_attr(not(feature = "std"), no_std)]
90#![cfg_attr(docsrs, feature(doc_cfg))]
91#![forbid(unsafe_code)]
92#![deny(missing_docs)]
93#![deny(unused_must_use)]
94#![deny(unused_results)]
95#![deny(clippy::unwrap_used)]
96#![deny(clippy::expect_used)]
97#![deny(clippy::todo)]
98#![deny(clippy::unimplemented)]
99#![deny(clippy::print_stdout)]
100#![deny(clippy::print_stderr)]
101#![deny(clippy::dbg_macro)]
102#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
103
104extern crate alloc;
105#[cfg(test)]
106extern crate std;
107
108mod analysis;
109mod budget;
110mod edit;
111mod error;
112mod float;
113mod fold;
114mod passes;
115mod pipeline;
116mod stats;
117
118pub use budget::Budget;
119pub use error::OptError;
120pub use pipeline::{
121    DEFAULT_MAX_ITERATIONS, DEFAULT_PIPELINE, DEFAULT_STEPS_PER_UNIT, Optimizer, PassKind, run_pass,
122};
123pub use stats::{PassStats, Stats};
124
125/// Optimizes every function of `module` with the default pipeline (Tier 1).
126///
127/// The same as `Optimizer::new().run(module)`: [`DEFAULT_PIPELINE`] run to a
128/// fixpoint (at most [`DEFAULT_MAX_ITERATIONS`] sweeps), with the default
129/// budget, validation between passes in debug builds, and changed functions
130/// compacted.
131///
132/// # Errors
133///
134/// [`OptError::InvalidInput`] if a function does not validate; the other
135/// variants only for a defect in a pass.
136///
137/// # Examples
138///
139/// ```
140/// use ir_lang::{Linkage, Module, Signature, Type};
141///
142/// let mut m = Module::new("m");
143/// let f = m.declare_function("f", &Signature::new(&[], &[Type::I32]), Linkage::Export)?;
144/// let mut b = m.build(f)?;
145/// let unused = b.iconst(Type::I32, 1)?;
146/// let v = b.iconst(Type::I32, 2)?;
147/// b.ret(&[v])?;
148/// # let _ = unused;
149/// let stats = opt_lang::optimize(&mut m)?;
150/// assert_eq!(stats.insts_after(), 1);
151/// m.validate()?;
152/// # Ok::<(), Box<dyn std::error::Error>>(())
153/// ```
154pub fn optimize(module: &mut ir_lang::Module) -> Result<Stats, OptError> {
155    Optimizer::new().run(module)
156}
157
158/// Runs the README's examples as doctests, so its claims stay true.
159#[cfg(doctest)]
160#[doc = include_str!("../README.md")]
161struct ReadmeDoctests;
162
163/// Runs the API reference's examples as doctests.
164#[cfg(doctest)]
165#[doc = include_str!("../docs/API.md")]
166struct ApiDoctests;