ExamplesBy LevelBy TopicLearning Paths
428 Fundamental

428: Macro Hygiene

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "428: Macro Hygiene" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. C preprocessor macros are famously dangerous because they operate via text substitution: `#define DOUBLE(x) x * 2` expands `DOUBLE(a + b)` to `a + b * 2` — not `(a + b) * 2`. Key difference from OCaml: 1. **Automatic hygiene**: Rust `macro_rules!` is automatically hygienic for introduced variables; OCaml PPX requires explicit fresh identifier generation.

Tutorial

The Problem

C preprocessor macros are famously dangerous because they operate via text substitution: #define DOUBLE(x) x * 2 expands DOUBLE(a + b) to a + b * 2 — not (a + b) * 2. Variable name collisions are another hazard: a macro using int result = ... conflicts with any result variable in the expansion scope. Rust's macro_rules! is hygienic: identifiers introduced inside a macro expansion live in a separate scope from the call site. let result = $val inside a macro doesn't shadow result outside it.

Hygiene is what makes Rust's macros safe to use in large codebases without name collision nightmares — it's a fundamental property that distinguishes macro_rules! from C preprocessor macros.

🎯 Learning Outcomes

  • • Understand what macro hygiene means: each macro expansion gets its own identifier scope
  • • Learn how let result = $val inside a macro doesn't capture the caller's result
  • • See the with_counter!(|c| { c += 1; }) pattern for intentional hygiene-breaking
  • • Understand when hygiene can be intentionally bypassed (passing identifier arguments)
  • • Learn the difference between hygienic macro_rules! and non-hygienic proc macros
  • Code Example

    #![allow(clippy::all)]
    //! Macro Hygiene
    //!
    //! How macros avoid name collisions.
    
    /// Macros create fresh identifiers by default.
    /// This prevents accidental shadowing.
    
    #[macro_export]
    macro_rules! hygienic_example {
        ($val:expr) => {{
            let result = $val; // 'result' is hygienic
            result * 2
        }};
    }
    
    /// Demonstrate that macro vars don't leak.
    pub fn test_hygiene() -> i32 {
        let result = 10; // Outer 'result'
        let doubled = hygienic_example!(5); // Inner 'result' is separate
        result + doubled // 10 + 10 = 20
    }
    
    /// Non-hygienic when you want shared names.
    #[macro_export]
    macro_rules! with_counter {
        (|$c:ident| $body:block) => {{
            let mut $c = 0;
            $body
        }};
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_hygienic_example() {
            let result = 100; // This 'result' is separate
            assert_eq!(hygienic_example!(5), 10);
            assert_eq!(result, 100); // Unchanged
        }
    
        #[test]
        fn test_hygiene_function() {
            assert_eq!(test_hygiene(), 20);
        }
    
        #[test]
        fn test_nested_hygiene() {
            let x = hygienic_example!(hygienic_example!(3));
            assert_eq!(x, 12); // ((3 * 2) * 2)
        }
    
        #[test]
        fn test_with_counter() {
            let v = with_counter!(|counter| {
                counter += 1;
                counter += 1;
                counter
            });
            assert_eq!(v, 2);
        }
    
        #[test]
        fn test_multiple_calls() {
            let a = hygienic_example!(1);
            let b = hygienic_example!(2);
            assert_eq!(a + b, 6);
        }
    }

    Key Differences

  • Automatic hygiene: Rust macro_rules! is automatically hygienic for introduced variables; OCaml PPX requires explicit fresh identifier generation.
  • Span-based: Rust's hygiene is based on Span — identifiers have a "context" indicating which expansion created them; OCaml has no equivalent span-based hygiene.
  • Proc macro hygiene: Rust's proc macros are not hygienic by default (they use Span::call_site()) but can opt into hygiene with Span::def_site().
  • Fragment capture: Rust's $expr:expr captured fragments maintain their own hygiene context; OCaml's AST captures are transparent.
  • OCaml Approach

    OCaml's PPX extensions are not hygienic in the way Rust's macro_rules! is. Generated code identifiers can conflict with surrounding code. PPX authors must use Ast_builder.gen_symbol or fresh_var utilities to generate unique names. This is the same problem Rust's macro_rules! solves automatically. OCaml's let open Module in scoping provides some protection, but not systematic hygiene.

    Full Source

    #![allow(clippy::all)]
    //! Macro Hygiene
    //!
    //! How macros avoid name collisions.
    
    /// Macros create fresh identifiers by default.
    /// This prevents accidental shadowing.
    
    #[macro_export]
    macro_rules! hygienic_example {
        ($val:expr) => {{
            let result = $val; // 'result' is hygienic
            result * 2
        }};
    }
    
    /// Demonstrate that macro vars don't leak.
    pub fn test_hygiene() -> i32 {
        let result = 10; // Outer 'result'
        let doubled = hygienic_example!(5); // Inner 'result' is separate
        result + doubled // 10 + 10 = 20
    }
    
    /// Non-hygienic when you want shared names.
    #[macro_export]
    macro_rules! with_counter {
        (|$c:ident| $body:block) => {{
            let mut $c = 0;
            $body
        }};
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_hygienic_example() {
            let result = 100; // This 'result' is separate
            assert_eq!(hygienic_example!(5), 10);
            assert_eq!(result, 100); // Unchanged
        }
    
        #[test]
        fn test_hygiene_function() {
            assert_eq!(test_hygiene(), 20);
        }
    
        #[test]
        fn test_nested_hygiene() {
            let x = hygienic_example!(hygienic_example!(3));
            assert_eq!(x, 12); // ((3 * 2) * 2)
        }
    
        #[test]
        fn test_with_counter() {
            let v = with_counter!(|counter| {
                counter += 1;
                counter += 1;
                counter
            });
            assert_eq!(v, 2);
        }
    
        #[test]
        fn test_multiple_calls() {
            let a = hygienic_example!(1);
            let b = hygienic_example!(2);
            assert_eq!(a + b, 6);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_hygienic_example() {
            let result = 100; // This 'result' is separate
            assert_eq!(hygienic_example!(5), 10);
            assert_eq!(result, 100); // Unchanged
        }
    
        #[test]
        fn test_hygiene_function() {
            assert_eq!(test_hygiene(), 20);
        }
    
        #[test]
        fn test_nested_hygiene() {
            let x = hygienic_example!(hygienic_example!(3));
            assert_eq!(x, 12); // ((3 * 2) * 2)
        }
    
        #[test]
        fn test_with_counter() {
            let v = with_counter!(|counter| {
                counter += 1;
                counter += 1;
                counter
            });
            assert_eq!(v, 2);
        }
    
        #[test]
        fn test_multiple_calls() {
            let a = hygienic_example!(1);
            let b = hygienic_example!(2);
            assert_eq!(a + b, 6);
        }
    }

    Deep Comparison

    OCaml vs Rust: macro hygiene

    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

  • Hygiene test: Write a macro swap!(a, b) that swaps two variables using a temporary. Verify that the temporary doesn't conflict with variables named tmp or temp in the calling scope.
  • Intentional capture: Implement with_err!(|e| { ... }) where e is a user-named error variable bound inside the macro. Show that the caller controls the name and can use with_err!(|my_err| { handle(my_err) }).
  • Demonstrate non-hygiene: Write a C-style macro alternative using string manipulation (as a thought exercise) and explain how three different variable naming conflicts would occur in C but not in Rust's hygienic macro_rules!.
  • Open Source Repos