Declarative and Procedural Macros in Depth

Macro Hygiene

Macro hygiene is the compiler mechanism that attaches invisible lexical syntax context metadata (part of each token's Span) to every identifier inside a macro expansion so that local variables introduced inside a macro never accidentally shadow or collide with variables of the same name at the call site.

How Mixed-Site Hygiene Works for macro_rules!

In macro_rules!, local variable identifiers (let temp = ...;), loop labels ('outer: loop), and lifetime names introduced literally inside the macro body have def-site hygiene (scoped to the macro definition), whereas identifiers passed into the macro as arguments ($var:ident) retain their call-site hygiene (scoped to the caller).

Caller Scope (`CallSite #0`):
  let acc = 100;
  let result = add_with_internal_acc!(acc, 25);

Expanded AST with Syntax Context Tags:
  let acc#0 = 100;
  let result#0 = {
      let acc#1 = 10;          // `acc#1` (macro's internal local) is completely distinct from `acc#0`!
      acc#0 + 25 + acc#1       // `$user_expr` resolves to `acc#0` (100), while `acc` resolves to `acc#1` (10)!
  };
macro_rules! add_with_internal_acc {
    ($user_expr:expr, $delta:expr) => {{
        // This `acc` lives in the macro's syntax context and CANNOT collide
        // with the caller's variable `acc`!
        let acc = 10;
        $user_expr + $delta + acc
    }};
}

fn main() {
    let acc = 100;
    let total = add_with_internal_acc!(acc * 2, 5);
    println!("caller acc = {}, total = {}", acc, total);
}
caller acc = 100, total = 215

Accessing Caller Variables Intentionally Requires Passing an :ident

Because a literal identifier written inside a macro body (x) has def-site hygiene, a macro cannot read or mutate a variable named x in the caller's scope unless the caller passes x explicitly as a $var:ident (or $expr:expr) argument:

// ❌ does not compile
macro_rules! read_implicit_x {
    () => {
        println!("x = {}", x);
    };
}

fn main() {
    let x = 42;
    read_implicit_x!();
}
error[E0425]: cannot find value `x` in this scope
  --> src/main.rs:4:28
   |
 4 |         println!("x = {}", x);
   |                            ^ not found in this scope
...
10 |     read_implicit_x!();
   |     ------------------ in this macro invocation
   |
help: an identifier with the same name exists, but is not accessible due to macro hygiene
  --> src/main.rs:9:9
   |
 9 |     let x = 42;
   |         ^

What Is Hygienic vs. Unhygienic in macro_rules!

While local variables (let) and labels are strictly hygienic, module-level items (fn, struct), type names, and trait methods resolve at the call site unless qualified with $crate:

Construct Inside macro_rules! Hygiene Behavior Best Practice
Local variables (let temp = ...;) Strictly hygienic (never collides with caller locals) Safe to name freely
Loop labels ('label: loop) Strictly hygienic Safe to name freely
Items in own crate ($crate::Helper) Resolved via $crate metavariable Always prefix helper functions/types with $crate::
Standard library items (::core::option::Option::Some) Unqualified Some or Result uses caller's imports! Use ::std::... or ::core::... in exported library macros
Struct field names & self in methods Resolved at call site / requires passing self as $self:ident if referenced inside helper macro Pass $self:ident when generating method bodies