Rust Debugging Macros
Debugging macros means reading the code a macro actually wrote. A macro error points at the call, not at that hidden code.
Watch every expansion (nightly)
Turn tracing on around the call you are studying. The compiler prints each invocation before expanding it:
// Needs nightly Rust.
#![feature(trace_macros)]
trace_macros!(true);
each_word!(a b c);
trace_macros!(false);Print tokens from inside a macro (nightly)
A muncher can report each token as it eats it:
// Needs nightly Rust.
#![feature(log_syntax)]
($tt:tt $($rest:tt)*) => { log_syntax!($tt); /* keep munching */ };Read the finished code
cargo expand shows the whole crate after expansion, on one screen:
$ cargo install cargo-expand
$ cargo expandPrint tokens from a proc macro
A procedural macro runs during the build. Its eprintln! output lands in the build messages. It never reaches the finished program:
#[proc_macro]
pub fn dump_tokens(input: TokenStream) -> TokenStream {
eprintln!("dump_tokens saw: {input}");
TokenStream::new()
}Read the dump in the build output
Call the macro once in the user crate. Then build the crate. The tokens print just as the macro received them. Look at the group. It hugs the word before it. No space sits between them:
dump_tokens!(alpha beta (gamma delta));
// build output: dump_tokens saw: alpha beta(gamma delta)Example
macro_rules! each_word {
() => {};
($word:tt $($rest:tt)*) => {
println!("saw: {}", stringify!($word));
each_word!($($rest)*);
};
}
fn main() {
each_word!(macros write code);
}Key points
- First move when a macro fails: read the expanded code — the error lives there, not in the call you wrote
- Use cargo expand (install it with cargo install cargo-expand) to print your crate with every macro already expanded
- trace_macros!(true) makes the compiler print every macro_rules! invocation just before it expands
- trace_macros!(false) turns the printing off
- This is invaluable for recursive macros
- log_syntax!(...) prints the tokens it is given during compilation, so a macro can report what it just matched
- trace_macros! and log_syntax! are nightly-only: they need a nightly compiler and #![feature(trace_macros)] or #![feature(log_syntax)] at the top of the crate
- On stable Rust, shrink the problem instead
- Copy the macro into a tiny test crate
- Cut the input down until the failing step is the only step left
- Use eprintln! inside a procedural macro to inspect tokens at compile time
- It shows the exact input before you reach for heavier tools
In this example, each_word is a small muncher. It prints one word, then calls itself on the rest. Its real output is the three saw: lines. Now imagine its middle arm had a mistake. The program failed with "no rules expected" at the call in main. The call looks innocent — the fault is in a step you cannot see. Turning on trace_macros! (on nightly) would print each invocation in turn. First each_word!(macros write code), then (write code), then (code), then (). And the step where the trail stops or goes wrong is the broken one.
Note
Keep the debugging lines out of published macros
A stray log_syntax! makes every user's build print noise
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A recursive macro fails with "no rules expected" at the call site. What is the most direct way to see which step went wrong?