ExamplesBy LevelBy TopicLearning Paths
430 Fundamental

430: Macro Debugging Techniques

Functional Programming

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

  • • Learn how cargo-expand (cargo expand) shows the fully expanded macro output
  • • Understand how trace_macros!(true) enables step-by-step expansion tracing (nightly only)
  • • See how compile_error!(stringify!($tokens)) reveals what tokens a macro is receiving
  • • Learn how dbg! and eprintln! in macro bodies help debug runtime behavior
  • • Understand how to write failing expansion tests with trybuild
  • Code 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

  • Tooling: cargo expand is a widely-used Rust tool with IDE integration; OCaml's equivalent requires more manual invocation.
  • Compile-time inspection: Rust's compile_error!(stringify!(...)) provides in-source debugging; OCaml requires external tool invocation.
  • Step-by-step: Rust's trace_macros! (nightly) shows each expansion step; OCaml has no equivalent interactive tracing.
  • Editor integration: 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Trace a complex macro: Take the 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 debugging: Use the 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.
  • trybuild test: Set up a 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.
  • Open Source Repos