428: Macro Hygiene
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
let result = $val inside a macro doesn't capture the caller's resultwith_counter!(|c| { c += 1; }) pattern for intentional hygiene-breakingmacro_rules! and non-hygienic proc macrosCode 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
macro_rules! is automatically hygienic for introduced variables; OCaml PPX requires explicit fresh identifier generation.Span — identifiers have a "context" indicating which expansion created them; OCaml has no equivalent span-based hygiene.Span::call_site()) but can opt into hygiene with Span::def_site().$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);
}
}#[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
Exercises
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.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) }).macro_rules!.