Ergonomic API for constructing MIR bodies in tests. Use for testing and benchmarking MIR passes without manual structure boilerplate.
Source: libs/@local/hashql/mir/src/builder/
The body! macro does not support all MIR constructs. If you need a feature that is not supported, do not work around it manually - instead, stop and request that the feature be added to the macro.
For advanced cases not supported by the macro, see mir-fluent-builder.md.
use hashql_core::{heap::Heap, r#type::environment::Environment};
use hashql_mir::{builder::body, intern::Interner};
let heap = Heap::new();
let interner = Interner::new(&heap);
let env = Environment::new(&heap);
let body = body!(interner, env; fn@0/1 -> Int {
decl x: Int, cond: Bool;
bb0() {
cond = load true;
if cond then bb1() else bb2();
},
bb1() {
goto bb3(1);
},
bb2() {
goto bb3(2);
},
bb3(x) {
return x;
}
});body!(interner, env; <source> @ <id> / <arity> -> <return_type> {
decl <local>: <type>, ...;
<block>(<params>...) {
<statements>...
},
...
})
Important: Only a single decl statement is supported. Declare all locals in one comma-separated list:
// ✅ Correct - single decl with all locals
decl env: (), vertex: Entity, x: Int, y: Int, result: Bool;
// ❌ Wrong - multiple decl statements will not compile
decl env: (), vertex: Entity;
decl x: Int, y: Int;
decl result: Bool;| Component | Description | Example |
|---|---|---|
<source> |
Body source type | fn, thunk, [ctor expr], intrinsic |
<id> |
DefId (literal or variable) | 0, 42, my_def_id |
<arity> |
Number of function arguments | 0, 1, 2 |
<return_type> |
Return type | Int, Bool, (Int, Bool) |
The <id> can be a numeric literal (0, 1, 42) or a variable identifier (callee_id, my_def_id). When using a variable, it must be a DefId in scope.
Source types:
| Syntax | Maps to | Use case |
|---|---|---|
fn |
Source::Closure |
Regular closures/functions |
thunk |
Source::Thunk |
Thunk bodies (zero-arg delayed computations) |
[ctor sym::path] |
Source::Ctor(sym) |
Constructor bodies (always inlined) |
[graph::read::filter] |
Source::GraphReadFilter |
Graph read filter bodies (never inlined) |
intrinsic |
Source::Intrinsic |
Intrinsic bodies (never inlined) |
| Syntax | Description | Example |
|---|---|---|
Int |
Integer type | Int |
Num |
Number (float) type | Num |
Bool |
Boolean type | Bool |
Null |
Null type | Null |
? |
Unknown type (dynamic) | ? |
(T1, T2, ...) |
Tuple types | (Int, Bool, Int) |
(T,) |
Single-element tuple | (Int,) |
(a: T1, b: T2) |
Struct types | (a: Int, b: Bool) |
[List T] |
List type (intrinsic) | [List Int], [List (Int, Bool)] |
[fn(T1, T2) -> R] |
Closure types | [fn(Int) -> Int], [fn() -> Bool] |
[Opaque path; T] |
Opaque type with symbol path | [Opaque sym::path::Entity; ?] |
|types| types.custom() |
Custom type expression | |t| t.null() |
Declare field projections after decl to access struct/tuple fields as places:
@proj <name> = <base>.<field>: <type>, ...;
Field access modes:
- Numeric index (e.g.,
tup.0) →ProjectionKind::Field - Named field (e.g.,
entity.metadata) →ProjectionKind::FieldByName
Each @proj declaration supports only ONE field after the base. For deeper paths, chain through intermediate declarations:
let body = body!(interner, env; fn@0/0 -> Int {
decl tup: ((Int, Int), Int), result: Int;
// inner uses tup as base, inner_1 uses inner as base
@proj inner = tup.0: (Int, Int), inner_1 = inner.1: Int;
bb0() {
result = load inner_1;
return result;
}
});Named field projections for opaque types:
use hashql_core::symbol::sym;
let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool {
decl env: (), vertex: [Opaque sym::path::Entity; ?];
// Chain: vertex -> metadata -> archived
@proj metadata = vertex.metadata: ?, archived = metadata.archived: Bool;
bb0() {
return archived;
}
});| Syntax | Description | MIR Equivalent |
|---|---|---|
let x; |
Mark storage live | StorageLive(x) |
drop x; |
Mark storage dead | StorageDead(x) |
x = load <operand>; |
Load value | Assign(x, Load(operand)) |
x = apply <func>; |
Call with no args | Assign(x, Apply(func, [])) |
x = apply <func>, <a1>, <a2>; |
Call with args | Assign(x, Apply(func, [a1, a2])) |
x = tuple <a>, <b>; |
Create tuple | Assign(x, Aggregate(Tuple, [a, b])) |
x = struct a: <v1>, b: <v2>; |
Create struct | Assign(x, Aggregate(Struct, [v1, v2])) |
x = closure <def> <env>; |
Create closure | Assign(x, Aggregate(Closure, [def, env])) |
x = opaque (<name>), <value>; |
Create opaque wrapper | Assign(x, Aggregate(Opaque(name), [value])) |
x = bin.<op> <lhs> <rhs>; |
Binary operation | Assign(x, Binary(lhs, op, rhs)) |
x = un.<op> <operand>; |
Unary operation | Assign(x, Unary(op, operand)) |
x = input.load! "name"; |
Load required input | Assign(x, Input(Load { required: true }, "name")) |
x = input.load "name"; |
Load optional input | Assign(x, Input(Load { required: false }, "name")) |
x = input.exists "name"; |
Check if input exists | Assign(x, Input(Exists, "name")) |
| Syntax | Description |
|---|---|
return <operand>; |
Return from function |
goto <block>(<args>...); |
Unconditional jump with args |
if <cond> then <tb>(<ta>) else <eb>(<ea>); |
Conditional branch |
switch <discr> [<val> => <block>(<args>), ...]; |
Switch (no otherwise) |
switch <discr> [<val> => <block>(), _ => <block>()]; |
Switch with otherwise |
unreachable; |
Mark block as unreachable |
| Syntax | Description |
|---|---|
x, cond |
Place (local variable or projection) |
42, -5 |
Integer literal (i64) |
3.14 |
Float literal (f64) |
true, false |
Boolean literal |
() |
Unit |
null |
Null |
def_id |
DefId variable (for function pointers) |
Binary (bin.<op>): ==, !=, <, <=, >, >=, &, |, +, -, *, /
Unary (un.<op>): !, neg
let body = body!(interner, env; fn@0/0 -> Int {
decl x: Int, cond: Bool;
bb0() {
cond = load true;
if cond then bb1() else bb2();
},
bb1() {
goto bb3(1);
},
bb2() {
goto bb3(2);
},
bb3(x) {
return x;
}
});let body = body!(interner, env; fn@0/0 -> Int {
decl x: Int, cond: Bool;
bb0() {
x = load 0;
goto bb1();
},
bb1() {
cond = bin.< x 10;
x = bin.+ x 1;
if cond then bb1() else bb2();
},
bb2() {
return x;
}
});let body = body!(interner, env; fn@0/0 -> Null {
decl selector: Int;
bb0() {
selector = load 0;
switch selector [0 => bb1(), 1 => bb2(), _ => bb3()];
},
bb1() {
return null;
},
bb2() {
return null;
},
bb3() {
return null;
}
});Filter bodies for graph traversal. The first two declared locals become the function arguments (_0 = env tuple, _1 = vertex):
let body = body!(interner, env; [graph::read::filter]@0/2 -> Bool {
decl env: (Int,), vertex: (Int, Int), result: Bool;
@proj vertex_field = vertex.0: Int;
bb0() {
result = bin.== vertex_field 42;
return result;
}
});Construct opaque-wrapped values with opaque (<name>), <value>. The name must
be wrapped in parentheses because it is a multi-token path.
use hashql_core::symbol::sym;
let body = body!(interner, env; fn@0/0 -> Int {
decl inner: (x: Int, y: Int), wrapped: [Opaque sym::path::Entity; ?], result: Int;
@proj y_field = wrapped.y: Int;
bb0() {
inner = struct x: 100, y: 200;
wrapped = opaque (sym::path::Entity), inner;
result = load y_field;
return result;
}
});Use a DefId variable directly:
let callee_id = DefId::new(1);
let body = body!(interner, env; fn@0/0 -> Int {
decl result: Int;
bb0() {
result = apply callee_id;
return result;
}
});Load a DefId into a local, then apply the local:
let callee_id = DefId::new(1);
let body = body!(interner, env; fn@0/0 -> Int {
decl func: [fn(Int) -> Int], result: Int;
bb0() {
func = load callee_id;
result = apply func, 1;
return result;
}
});When creating multiple bodies that reference each other:
let callee_id = DefId::new(1);
let caller_id = DefId::new(0);
let caller = body!(interner, env; fn@caller_id/0 -> Int {
decl result: Int;
bb0() {
result = apply callee_id;
return result;
}
});
let callee = body!(interner, env; fn@callee_id/0 -> Int {
decl ret: Int;
bb0() {
return ret;
}
});let body = body!(interner, env; fn@0/0 -> (a: Int, b: Bool) {
decl result: (a: Int, b: Bool);
bb0() {
result = struct a: 42, b: true;
return result;
}
});// body0: function that takes captured env and returns it
let body0 = body!(interner, env; fn@0/1 -> Int {
decl env_arg: Int, result: Int;
bb0() {
result = load env_arg;
return result;
}
});
// body1: creates closure, calls it via projections
let body1 = body!(interner, env; fn@1/0 -> Int {
decl captured: Int, closure: [fn(Int) -> Int], result: Int;
@proj closure_fn = closure.0: [fn(Int) -> Int], closure_env = closure.1: Int;
bb0() {
captured = load 55;
closure = closure (body0.id) captured;
result = apply closure_fn, closure_env;
return result;
}
});Projected places can be used as operands in terminators:
let body = body!(interner, env; fn@0/0 -> Int {
decl tup: (Int, Int), result: Int;
@proj tup_0 = tup.0: Int, tup_1 = tup.1: Int;
bb0() {
tup = tuple 1, 2;
if tup_0 then bb1(tup_0) else bb2(tup_1);
},
bb1(result) {
return result;
},
bb2(result) {
return result;
}
});Standard pattern used across transform pass tests:
use std::{io::Write as _, path::PathBuf};
use bstr::ByteVec as _;
use hashql_core::{
heap::Heap,
pretty::Formatter,
r#type::{TypeFormatter, TypeFormatterOptions, environment::Environment},
};
use hashql_diagnostics::DiagnosticIssues;
use insta::{Settings, assert_snapshot};
use crate::{
builder::body,
context::MirContext,
def::DefIdSlice,
intern::Interner,
pass::TransformPass as _,
pretty::TextFormat,
};
#[track_caller]
fn assert_pass<'heap>(
name: &'static str,
body: Body<'heap>,
context: &mut MirContext<'_, 'heap>,
) {
let formatter = Formatter::new(context.heap);
let mut formatter = TypeFormatter::new(
&formatter,
context.env,
TypeFormatterOptions::terse().with_qualified_opaque_names(true),
);
let mut text_format = TextFormat {
writer: Vec::new(),
indent: 4,
sources: (),
types: &mut formatter,
};
let mut bodies = [body];
// Format before
text_format
.format(DefIdSlice::from_raw(&bodies), &[])
.expect("should be able to write bodies");
// Run the pass and capture change status
let changed = YourPass::new().run(context, &mut bodies[0]);
// Include Changed value in snapshot
write!(
text_format.writer,
"\n\n{:=^50}\n\n",
format!(" Changed: {changed:?} ")
).expect("infallible");
// Format after
text_format
.format(DefIdSlice::from_raw(&bodies), &[])
.expect("should be able to write bodies");
// Snapshot configuration
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut settings = Settings::clone_current();
settings.set_snapshot_path(dir.join("tests/ui/pass/your_pass"));
settings.set_prepend_module_to_snapshot(false);
let _drop = settings.bind_to_scope();
let value = text_format.writer.into_string_lossy();
assert_snapshot!(name, value);
}
#[test]
fn test_case() {
let heap = Heap::new();
let interner = Interner::new(&heap);
let env = Environment::new(&heap);
let body = body!(interner, env; fn@0/0 -> Int {
decl x: Int;
bb0() {
x = load 42;
return x;
}
});
assert_pass(
"test_case",
body,
&mut MirContext {
heap: &heap,
env: &env,
interner: &interner,
diagnostics: DiagnosticIssues::new(),
},
);
}Real test examples in libs/@local/hashql/mir/src/pass/:
Transform passes (transform/):
administrative_reduction/tests.rsdse/tests.rs- Dead Store Eliminationssa_repair/tests.rs- SSA Repaircfg_simplify/tests.rs- CFG Simplificationdbe/tests.rs- Dead Block Eliminationcp/tests.rs- Constant Propagationdle/tests.rs- Dead Local Eliminationinst_simplify/tests.rs- Instruction Simplification
Analysis passes (analysis/):
callgraph/tests.rs- Call graph analysisdata_dependency/tests.rs- Data dependency analysisdataflow/liveness/tests.rs- Liveness analysis