430: Macro Debugging Techniques
Tutorial Video
Text description (accessibility)
This video demonstrates the "430: Macro Debugging Techniques" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Macro expansion errors can be opaque — the compiler shows the expanded output but not always why a specific expansion fails. Key difference from OCaml: 1. **Tooling**: `cargo expand` is a widely
Tutorial
The Problem
Macro expansion errors can be opaque — the compiler shows the expanded output but not always why a specific expansion fails. A macro that works for some inputs but fails for others requires tools to inspect what tokens are being matched and what code is being generated. cargo-expand shows the full expanded source, trace_macros! shows each expansion step, stringify! converts tokens to strings for inspection, and strategic compile_error! can reveal what the macro is seeing at a specific expansion point.
Debugging macros is an essential skill: complex macro_rules! patterns and proc macros are hard to reason about without tooling.
🎯 Learning Outcomes
cargo-expand (cargo expand) shows the fully expanded macro outputtrace_macros!(true) enables step-by-step expansion tracing (nightly only)compile_error!(stringify!($tokens)) reveals what tokens a macro is receivingdbg! and eprintln! in macro bodies help debug runtime behaviortrybuildCode Example
#![allow(clippy::all)]
//! Macro Debugging
//!
//! Tools for debugging macros.
/// Use cargo expand to see macro expansion.
/// Use trace_macros! for step-by-step.
#[macro_export]
macro_rules! debug_sum {
($a:expr, $b:expr) => {{
let a = $a;
let b = $b;
eprintln!("debug_sum: {} + {} = {}", a, b, a + b);
a + b
}};
}
/// Macro that shows its expansion.
#[macro_export]
macro_rules! show_expansion {
($($t:tt)*) => {
compile_error!(concat!("Tokens: ", stringify!($($t)*)));
};
}
/// Helper to stringify macro args.
#[macro_export]
macro_rules! stringify_args {
($($arg:expr),*) => {
vec![$(stringify!($arg)),*]
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_debug_sum() {
let result = debug_sum!(2, 3);
assert_eq!(result, 5);
}
#[test]
fn test_stringify_args() {
let args = stringify_args!(x, y + z, foo());
assert_eq!(args.len(), 3);
assert_eq!(args[0], "x");
}
#[test]
fn test_nested_debug() {
let result = debug_sum!(debug_sum!(1, 2), 3);
assert_eq!(result, 6);
}
#[test]
fn test_stringify_preserves() {
let args = stringify_args!(1 + 2, 3 * 4);
assert!(args[0].contains("+"));
assert!(args[1].contains("*"));
}
#[test]
fn test_empty_stringify() {
let args: Vec<&str> = stringify_args!();
assert!(args.is_empty());
}
}Key Differences
cargo expand is a widely-used Rust tool with IDE integration; OCaml's equivalent requires more manual invocation.compile_error!(stringify!(...)) provides in-source debugging; OCaml requires external tool invocation.trace_macros! (nightly) shows each expansion step; OCaml has no equivalent interactive tracing.rust-analyzer shows macro expansions inline; OCaml editors have limited PPX expansion visualization.OCaml Approach
OCaml PPX debugging uses -ppx flag with manual invocation to see transformed output. ocamlfind ppx_deriving/show.ppx file.ml -impl shows the PPX output. The Ppx_tools library provides Ppx_tools.Genlex for parsing and Ppx_tools.Ppx_coptions for debugging. OCaml doesn't have a direct equivalent of cargo expand but dune describe pp file.ml shows the preprocessed output.
Full Source
#![allow(clippy::all)]
//! Macro Debugging
//!
//! Tools for debugging macros.
/// Use cargo expand to see macro expansion.
/// Use trace_macros! for step-by-step.
#[macro_export]
macro_rules! debug_sum {
($a:expr, $b:expr) => {{
let a = $a;
let b = $b;
eprintln!("debug_sum: {} + {} = {}", a, b, a + b);
a + b
}};
}
/// Macro that shows its expansion.
#[macro_export]
macro_rules! show_expansion {
($($t:tt)*) => {
compile_error!(concat!("Tokens: ", stringify!($($t)*)));
};
}
/// Helper to stringify macro args.
#[macro_export]
macro_rules! stringify_args {
($($arg:expr),*) => {
vec![$(stringify!($arg)),*]
};
}
#[cfg(test)]
mod tests {
#[test]
fn test_debug_sum() {
let result = debug_sum!(2, 3);
assert_eq!(result, 5);
}
#[test]
fn test_stringify_args() {
let args = stringify_args!(x, y + z, foo());
assert_eq!(args.len(), 3);
assert_eq!(args[0], "x");
}
#[test]
fn test_nested_debug() {
let result = debug_sum!(debug_sum!(1, 2), 3);
assert_eq!(result, 6);
}
#[test]
fn test_stringify_preserves() {
let args = stringify_args!(1 + 2, 3 * 4);
assert!(args[0].contains("+"));
assert!(args[1].contains("*"));
}
#[test]
fn test_empty_stringify() {
let args: Vec<&str> = stringify_args!();
assert!(args.is_empty());
}
}#[cfg(test)]
mod tests {
#[test]
fn test_debug_sum() {
let result = debug_sum!(2, 3);
assert_eq!(result, 5);
}
#[test]
fn test_stringify_args() {
let args = stringify_args!(x, y + z, foo());
assert_eq!(args.len(), 3);
assert_eq!(args[0], "x");
}
#[test]
fn test_nested_debug() {
let result = debug_sum!(debug_sum!(1, 2), 3);
assert_eq!(result, 6);
}
#[test]
fn test_stringify_preserves() {
let args = stringify_args!(1 + 2, 3 * 4);
assert!(args[0].contains("+"));
assert!(args[1].contains("*"));
}
#[test]
fn test_empty_stringify() {
let args: Vec<&str> = stringify_args!();
assert!(args.is_empty());
}
}
Deep Comparison
OCaml vs Rust: macro debugging
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
min_of! macro from example 414 and instrument it with eprintln! calls to print each recursive step. Verify that min_of!(5, 3, 8, 1, 4) correctly traces through the recursion.show_expansion! technique to understand what tokens a complex macro is receiving. Create a macro that sometimes fails and use compile_error!(stringify!(...)) to reveal the exact input at the failing arm.tests/ui/ directory with a failing macro invocation. Write a tests/macro_tests.rs using trybuild::TestCases that verifies the expected compile error message appears when the macro is misused.