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}