Hygiene
Macro hygiene is a compiler property that prevents local variable identifiers introduced inside a macro's body from accidentally colliding with—or shadowing—variables of the same name at the macro's call site.
How Span Contexts Prevent Identifier Collisions
Unlike C preprocessor #define macros (which perform raw text substitution and silently clobber caller variables that happen to share the same name), Rust attaches a syntax context (part of the token's Span) to every identifier:
- An identifier written literally inside the macro transcriber (
let temp = ...;) belongs to the macro's own hygiene context (think of it as having an invisible color distinct from the caller's scope). - An expression
$e:exprpassed by the caller keeps the caller's hygiene context.
Caller Scope (Blue Context): Macro Body (Green Context):
let temp = 100; let temp = 2;
scale!(temp) ───────────> temp (Green: 2) * temp (Blue: 100) = 200
macro_rules! double_with_temp {
($user_expr:expr) => {{
// This `temp` lives in the macro's hygiene context
// and does NOT shadow the caller's `temp`!
let temp = 2;
$user_expr * temp
}};
}
fn main() {
let temp = 21;
let result = double_with_temp!(temp);
println!("Caller temp = {temp}, result = {result}");
}
Caller temp = 21, result = 42
Cannot Reference Macro-Internal Variables From Caller Code (E0425)
Similarly, a statement macro that declares a local binding let x = 10; using a literal identifier x inside the macro body does not expose x to subsequent statements at the call site:
// ❌ does not compile
macro_rules! make_hidden_x {
() => {
let x = 42;
};
}
fn main() {
make_hidden_x!();
println!("{x}"); // Error: `x` in `main` has a different hygiene context than `x` in the macro
}
error[E0425]: cannot find value `x` in this scope
--> src/main.rs:10:16
|
2 | macro_rules! make_hidden_x {
| -------------------------- similarly named local `x` defined here
...
10 | println!("{x}");
| ^ help: a local variable with a similar name exists (notice the hygiene): `x`
Intentionally Binding a Caller Identifier via $var:ident
When a macro wants to introduce a variable or function into the caller's scope, the caller must pass the identifier name explicitly as an $id:ident parameter so the identifier carries the caller's hygiene context:
macro_rules! make_caller_var {
($var_name:ident = $val:expr) => {
let $var_name = $val;
};
}
fn main() {
make_caller_var!(answer = 42);
println!("Caller can read `answer`: {answer}");
}
Caller can read `answer`: 42
Scope of Hygiene in macro_rules! (Mixed-Site Hygiene)
In macro_rules!, hygiene is mixed-site:
- Local variables and labels (
'a,let x) are fully hygienic (def-site scoped). - Type names, module paths, functions, and struct fields resolved without
$crateare looked up at the call site unless prefixed with$crate::(which always resolves to the crate that defined the macro).