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 |