430: Debugging Macros with cargo expand
Difficulty: 3 Level: Advanced Use `cargo expand` to see exactly what your macros generate, and `compile_error!` to emit helpful diagnostics โ the two essential tools when a macro misbehaves.The Problem This Solves
Macro errors are notoriously cryptic. The error points to the call site but the bug is in the expansion. You stare at `error[E0308]: mismatched types` in your template code and have no idea which arm matched, what tokens were captured, or what the expanded output looks like. Without tooling, debugging macros means adding `println!` calls you can't even see compile, or mentally tracing recursive expansions by hand. The other failure mode is silent: the macro runs but does the wrong thing. Maybe it matched the wrong arm, maybe a repetition produced one more or fewer item than expected, maybe `$a + sum!($($tail),*)` has a comma issue in the base case. These are impossible to diagnose without seeing the expansion. `cargo expand` solves this: it runs the macro expander and prints the fully-expanded Rust source. What you see is what the compiler compiles. You can read it, copy it, and compile it manually to isolate issues.The Intuition
`cargo expand` is a thin wrapper around the compiler's internal `-Z unpretty=expanded` flag. It produces valid Rust source with all macros expanded โ `vec![1,2,3]` becomes its full `Vec` construction, your `sum!(1,2,3)` becomes `1 + (2 + 3)`, your derive impls become fully written trait implementations. For interactive debugging, `compile_error!` is your `panic!` โ it emits a hard error with your message during expansion. `stringify!($x)` shows you exactly what tokens were captured in a fragment. Together, these let you instrument macros like you'd instrument runtime code.How It Works in Rust
// โโ Tool 1: cargo expand โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Install: cargo install cargo-expand
// Run: cargo expand (entire crate)
// cargo expand my_mod (one module)
// What you see: every macro call replaced with its full expansion.
// โโ Tool 2: stringify! to inspect captured tokens โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
macro_rules! trace_input {
($($x:tt)*) => {{
// Shows exactly what tokens were matched โ indispensable for debugging
println!("Macro received: {}", stringify!($($x)*));
}};
}
// trace_input!(1 + 2 * "foo") โ prints: Macro received: 1 + 2 * "foo"
// โโ Tool 3: compile_error! to diagnose wrong-arm matches โโโโโโโโโโโโโโโโโโโโโ
macro_rules! exactly_two {
($a:expr, $b:expr) => { ($a, $b) };
($($other:tt)*) => {
// Shows the offending tokens in the error message
compile_error!(concat!(
"exactly_two! needs exactly 2 args, got: ",
stringify!($($other)*)
))
};
}
// โโ Mentally tracing recursive expansion โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// sum!(1, 2, 3):
// โ 1 + sum!(2, 3) matches ($head, $($tail),*)
// โ 1 + (2 + sum!(3)) matches ($head, $($tail),*)
// โ 1 + (2 + 3) matches ($head)
// = 6
macro_rules! sum {
() => { 0 };
($head:expr $(, $tail:expr)*) => {
$head + sum!($($tail),*)
};
}
// โโ Nightly: trace_macros! prints each expansion step โโโโโโโโโโโโโโโโโโโโโโโโ
// #![feature(trace_macros)]
// trace_macros!(true);
// sum!(1, 2, 3); // prints each recursive step to stderr
// trace_macros!(false);
Debugging checklist when a macro fails:
1. `cargo expand` โ see the full expansion
2. `println!("arm: {}", stringify!($x))` โ check what was matched
3. `compile_error!` in unexpected arms โ force a diagnostic with context
4. Simplify the call site to the minimum failing case
What This Unlocks
- Confident macro development โ see exactly what you're generating before the compiler rejects it for a reason you can't interpret.
- Learning from others' macros โ `cargo expand` on any crate shows you what `#[derive(serde::Serialize)]` actually generates for your type.
- Better error messages โ `compile_error!` + `stringify!` lets your macros report why input was wrong, not just that it was.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| See macro expansion | `ocamlfind ocamlopt -ppx ... -dsource` or `ppxlib` dump | `cargo expand` (cargo-expand crate) |
| Runtime trace | N/A for ppx | `trace_macros!(true)` (nightly) |
| Compile-time error from macro | `Location.raise_errorf` in ppx | `compile_error!("msg")` |
| Inspect matched tokens | N/A | `stringify!($x)` prints token tree as string |
// Debugging macros with trace_macros! and cargo expand
// trace_macros!(true); // Nightly only โ prints each expansion step
// trace_macros!(false); // Turn off
// A macro with a bug we'll debug
macro_rules! buggy_example {
// Missing: what if list is empty?
($head:expr, $($tail:expr),+) => {
$head + buggy_example!($($tail),+)
};
($only:expr) => { $only }; // base case
}
// Debugging technique 1: add compile_error! to print matched arm
macro_rules! debug_which_arm {
() => { compile_error!("matched empty arm") };
($single:expr) => { $single };
($a:expr, $b:expr) => { $a + $b };
($($x:expr),+) => { "more than 2" };
}
// Debugging technique 2: stringify! to see what's being matched
macro_rules! trace_input {
($($x:tt)*) => {
{
println!("Macro received: {}", stringify!($($x)*));
}
};
}
// Debugging technique 3: step through expansion manually
// Imagine: my_macro!(1, 2, 3)
// Step 1: matches ($head:expr, $($tail:expr),+) with head=1, tail=2,3
// Step 2: expands to 1 + my_macro!(2, 3)
// Step 3: matches ($head:expr, $($tail:expr),+) with head=2, tail=3
// Step 4: expands to 1 + (2 + my_macro!(3))
// Step 5: matches ($only:expr) with only=3
// Result: 1 + (2 + 3) = 6
// Well-designed macro for comparison
macro_rules! sum {
() => { 0 };
($head:expr $(, $tail:expr)*) => {
$head + sum!($($tail),*)
};
}
// Macro that validates input count
macro_rules! exactly_two {
($a:expr, $b:expr) => { ($a, $b) };
($($other:tt)*) => {
compile_error!(concat!(
"exactly_two! requires exactly 2 args, got: ",
stringify!($($other)*)
))
};
}
fn main() {
// Working macro
println!("buggy_example!(1, 2, 3) = {}", buggy_example!(1, 2, 3));
// trace_input shows what was passed
trace_input!(hello world 42);
trace_input!(1 + 2, "foo");
// sum macro
println!("sum!() = {}", sum!());
println!("sum!(5) = {}", sum!(5));
println!("sum!(1, 2, 3, 4) = {}", sum!(1, 2, 3, 4));
// exactly_two
let (a, b) = exactly_two!(10, 20);
println!("exactly_two: {}, {}", a, b);
// Cargo expand command (not runtime, but shown for reference):
println!("
Debugging tools:");
println!(" cargo install cargo-expand");
println!(" cargo expand -- 2>&1 | head -100");
println!(" RUSTFLAGS='-Z trace-macros' cargo build (nightly)");
}
#[cfg(test)]
mod tests {
#[test]
fn test_sum() {
assert_eq!(sum!(), 0);
assert_eq!(sum!(5), 5);
assert_eq!(sum!(1, 2, 3), 6);
assert_eq!(sum!(1, 2, 3, 4, 5), 15);
}
#[test]
fn test_buggy_becomes_correct() {
assert_eq!(buggy_example!(10, 20, 30), 60);
}
}
(* Debugging macros in OCaml *)
(* OCaml macro debugging via ppx dump flags *)
(* cargo-like: ocamlfind ocamlopt -package ppx_deriving -ppx ... *)
(* Debugging technique: add Printf to macro output *)
let debug_macro_expand name value =
Printf.printf "[MACRO EXPAND] %s => %s\n" name value
(* Verify expansion produces expected results *)
let test_macro_output name actual expected =
if actual = expected then
Printf.printf "[OK] %s\n" name
else
Printf.printf "[FAIL] %s: expected %s, got %s\n" name expected actual
let () =
(* Simulate macro that we're debugging *)
debug_macro_expand "stringify!(x + y)" ""x + y"";
debug_macro_expand "concat!("a", "b")" ""ab"";
test_macro_output "simple add" (string_of_int (1 + 2)) "3";
test_macro_output "concat" ("foo" ^ "bar") "foobar"