Skip to main content

ir_assert/
lib.rs

1#![allow(clippy::test_attr_in_doctest)]
2#![doc = include_str!("../README.md")]
3
4mod build;
5pub mod env;
6mod ir;
7pub mod predicate;
8
9use env::EnvSpec;
10pub use ir::{BasicBlockIr, FunctionIr};
11use std::collections::HashMap;
12use std::fmt::Display;
13use std::path::PathBuf;
14
15#[doc(hidden)]
16pub use ir_assert_macro::__assert_ir_impl;
17#[doc(hidden)]
18pub use ir_assert_macro::__debug_assert_ir_impl;
19
20/// Trait for all predicate types that can be evaluated against a function's IR.
21pub trait Predicate: Display {
22    /// Evaluate the predicate against a function's IR.
23    fn evaluate(
24        &self,
25        fn_name: &str,
26        functions: &HashMap<String, FunctionIr>,
27        env: &crate::env::EnvSpec,
28    ) -> Result<(), String>;
29
30    /// Collect all environment specifications referenced by this predicate tree.
31    fn collect_environments(&self, envs: &mut Vec<crate::env::EnvSpec>) {
32        envs.push(EnvSpec::DEFAULT);
33    }
34}
35
36/// Assert properties of LLVM IR for given functions/closures.
37///
38/// Use inside `#[test]` functions:
39///
40/// ```rust
41/// use ir_assert::assert_ir;
42///
43/// #[test]
44/// fn test_optimized() {
45///     assert_ir!(basic_blocks.len().eq(1) & calls.len().eq(0), my_fn);
46/// }
47/// ```
48///
49/// Predicate syntax is provided by the [`crate::predicate`] module.
50///
51/// The predicate expression can use:
52/// - `basic_blocks.len().eq(1)`, `calls.len().eq(0)`, etc. with `.eq()`, `.ne()`,
53///   `.lt()`, `.le()`, `.gt()`, `.ge()` comparison methods
54/// - `&`, `|`, `!` — logical combinators
55/// - `basic_blocks.all(|bb| ...)`, `basic_blocks.any(|bb| ...)` — quantifiers
56/// - `basic_blocks.at(N).prop().len().eq(X)` — indexed block access
57/// - `rustc("1.86") & ir_pred` — evaluate ir_pred against IR from rustc 1.86
58/// - `target("triple") & ir_pred` — evaluate ir_pred against IR for target
59///
60/// Target example:
61///
62/// ```rust
63/// use ir_assert::assert_ir;
64///
65/// fn add(a: u64, b: u64) -> u64 { a + b }
66///
67/// #[test]
68/// fn test_target_specific() {
69///     assert_ir!(
70///         target("x86_64-unknown-linux-gnu")
71///             & basic_blocks.len().eq(1),
72///         add
73///     );
74/// }
75/// ```
76///
77/// Target example with generic function and closure:
78///
79/// ```rust
80/// use ir_assert::assert_ir;
81///
82/// fn id<T>(x: T) -> T { x }
83///
84/// #[test]
85/// fn test_target_with_generic_and_closure() {
86///     assert_ir!(
87///         target("x86_64-unknown-linux-gnu")
88///             & basic_blocks.len().eq(1)
89///             & calls.len().eq(0),
90///         id::<u64>,
91///         |a: usize, b: usize| a + b
92///     );
93/// }
94/// ```
95#[macro_export]
96macro_rules! assert_ir {
97    ($($tt:tt)*) => {
98        $crate::__assert_ir_impl!($crate, $($tt)*);
99    };
100}
101
102/// Like [`assert_ir!`], but only performs the IR assertion when `cfg(debug_assertions)` is
103/// enabled (i.e. debug builds).
104///
105/// With `cfg(debug_assertions)` on, this delegates entirely to [`assert_ir!`] and supports
106/// any number of targets.
107///
108/// With `cfg(debug_assertions)` off, the predicate is skipped:
109/// - A single target expression is evaluated and returned as-is (passthru).
110/// - Multiple target expressions are a **compile error** — use [`assert_ir!`] directly if you
111///   need multi-target assertions in release mode.
112///
113/// # Examples
114///
115/// ```rust
116/// use ir_assert::debug_assert_ir;
117///
118/// fn add(a: u64, b: u64) -> u64 { a + b }
119///
120/// #[test]
121/// fn test_add_optimized_in_debug() {
122///     // In debug builds this checks IR; in release builds it's a no-op passthru.
123///     let f = debug_assert_ir!(basic_blocks.len().eq(1) & calls.len().eq(0), add);
124///     assert_eq!(f(1, 2), 3);
125/// }
126/// ```
127#[macro_export]
128macro_rules! debug_assert_ir {
129    ($($tt:tt)*) => {
130        $crate::__debug_assert_ir_impl!($crate, $($tt)*)
131    };
132}
133
134#[doc(hidden)]
135#[track_caller]
136#[allow(clippy::too_many_arguments)]
137pub fn __macro_internal(
138    cargo: &str,
139    rustup: &str,
140    manifest_dir: &str,
141    crate_name: &str,
142    is_test: bool,
143    symbol: &str,
144    pred: &dyn Predicate,
145    pred_str: &str,
146    target_names: &[&str],
147) {
148    // Prefer Cargo's configured target dir to keep outputs in a writable/project-local location.
149    // This also avoids deriving `/ir-assert...` when invoked from doctests.
150    let cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR")
151        .map(PathBuf::from)
152        .unwrap_or_else(|| PathBuf::from(manifest_dir).join("target"));
153    let ir_target_dir = cargo_target_dir.join("ir-assert");
154
155    // Acquire the build lock, which also carries the per-process IR cache.
156    // Only one thread compiles at a time; already-built envs are served from the cache.
157    let mut cache = build::BUILD_LOCK
158        .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
159        .lock()
160        .unwrap_or_else(|e| e.into_inner());
161
162    let ctx = build::BuildContext { cargo, rustup, manifest_dir, crate_name, is_test };
163
164    // 1. Collect all environments from the predicate tree
165    let mut envs = Vec::new();
166    pred.collect_environments(&mut envs);
167
168    // Deduplicate
169    envs.sort_by(|a, b| format!("{:?}", a).cmp(&format!("{:?}", b)));
170    envs.dedup();
171
172    // 2. Build IR for all environments (cached: compiler is invoked at most once per env)
173    let all_env_functions = build::load_all_envs(&ctx, &envs, &ir_target_dir, &mut cache)
174    .unwrap_or_else(|errors| {
175        let mut msg = String::from("ir-assert: none of the environments are available\n");
176        for (env, e) in &errors {
177            msg.push_str(&format!("\n  [{}]\n", env));
178            for line in e.lines() {
179                msg.push_str(&format!("    {}\n", line));
180            }
181        }
182        panic!("{}", msg);
183    });
184
185    // 3. Find the container function in default IR
186    let default_functions = all_env_functions
187        .values()
188        .next()
189        .unwrap_or_else(|| panic!("ir-assert: none of the environments are available"));
190    let container = default_functions
191        .iter()
192        .find(|f| f.name == symbol)
193        .unwrap_or_else(|| {
194            panic!(
195                "ir-assert: container function '{}' not found in IR. ",
196                symbol,
197            )
198        });
199
200    // 4. Find functions referenced by the container
201    let referenced = build::find_referenced_functions(container);
202
203    if referenced.is_empty() {
204        panic!(
205            "ir-assert: no function references found in container '{}'. \
206             Make sure target functions are referenced via inline asm.",
207            symbol
208        );
209    }
210
211    // 5. For each referenced function, build per-env FunctionIr list and evaluate
212    for (idx, func_name) in referenced.iter().enumerate() {
213        // Check if the function exists in default IR and has blocks
214        let default_func = default_functions.iter().find(|f| f.name == *func_name);
215        if let Some(f) = default_func {
216            if f.blocks.is_empty() {
217                continue;
218            }
219        }
220
221        // Build the per-env mapping for this target function
222        let mut env_function_irs: Vec<(crate::env::EnvSpec, ir::FunctionIr)> = Vec::new();
223
224        for (env, functions) in &all_env_functions {
225            if let Some(env_container) = functions.iter().find(|f| f.name == symbol) {
226                let env_referenced = build::find_referenced_functions(env_container);
227                if let Some(env_func_name) = env_referenced.get(idx) {
228                    if let Some(func) = functions.iter().find(|f| f.name == *env_func_name) {
229                        if !func.blocks.is_empty() {
230                            env_function_irs.push((env.clone(), func.clone()));
231                        }
232                    }
233                }
234            }
235        }
236
237        // Evaluate on each available environment.
238        let mut evaluated_any = false;
239        for (env, func_ir) in &env_function_irs {
240            let env_functions: HashMap<String, FunctionIr> = all_env_functions
241                .get(env)
242                .map(|funcs| {
243                    funcs
244                        .iter()
245                        .cloned()
246                        .map(|f| (f.name.clone(), f))
247                        .collect::<HashMap<_, _>>()
248                })
249                .unwrap_or_default();
250            evaluated_any = true;
251            if let Err(reason) = pred.evaluate(&func_ir.name, &env_functions, env) {
252                let target_label = target_names.get(idx).copied().unwrap_or(func_name.as_str());
253                let indented_reason = reason.replace('\n', "\n    ");
254
255                let raw_ir = &func_ir.raw;
256
257                panic!(
258                    "ir-assert: assertion failed\n  \
259                     predicate: {}\n  \
260                     target: {}\n  \
261                     environment: {}\n  \
262                     reason:\n    {}\n\n{}",
263                    pred_str, target_label, env, indented_reason, raw_ir
264                );
265            }
266        }
267        if !evaluated_any {
268            let target_label = target_names.get(idx).copied().unwrap_or(func_name.as_str());
269            let available_envs: Vec<_> = env_function_irs
270                .iter()
271                .map(|(e, _)| format!("{}", e))
272                .collect();
273            panic!(
274                "ir-assert: none of the environments matched the predicate\n  \
275                 predicate: {}\n  \
276                 target: {}\n  \
277                 available environments: [{}]",
278                pred_str,
279                target_label,
280                available_envs.join(", ")
281            );
282        }
283    }
284}