Macro Expansion Stages
Macro expansion is the compile-time phase in which rustc iteratively resolves and expands declarative (macro_rules!) and procedural (#[derive], attribute, and function-like) macro invocations into concrete Abstract Syntax Tree (AST) nodes before type checking, borrow checking, and MIR lowering begin.
Where Macro Expansion Fits in the rustc Pipeline
Unlike C's text-based preprocessor (cpp), which performs blind string substitution before lexing, Rust expands macros after lexical analysis and initial parsing on structured token trees and AST nodes:
1. Source Text (.rs)
|
v [ Lexer ]
2. Token Stream & Token Trees (Balanced `()`, `[]`, `{}` verified here!)
|
v [ Parser + Iterative Name Resolution & Macro Expansion Loop ]
3. Unexpanded AST
+-- Evaluates `#[cfg(...)]` and `#[cfg_attr(...)]` (strips disabled branches)
+-- Resolves macro paths (`use`, `#[macro_use]`)
+-- Expands `#[attr]` procedural macros, `#[derive(...)]`, and `macro!()` calls
+-- Repeats until zero macro invocations remain in the AST!
|
v [ AST Validation & Lowering ]
4. High-Level IR (HIR) ---> Type Checking ---> MIR ---> LLVM IR ---> Machine Code
Outside-In (Lazy) Expansion Order
By default, Rust expands macros outside-in (lazily): when an outer macro outer!(inner!(x)) is invoked, outer! receives the literal tokens inner ! ( x ) as its input before inner!(x) is expanded! Only after outer! finishes expanding and emits inner!(x) into the resulting AST does the compiler expand inner!(x):
macro_rules! inner_value {
() => {
10 + 32
};
}
macro_rules! inspect_tokens {
// Matches the unexpanded token sequence passed into `inspect_tokens!`:
($($tt:tt)*) => {
println!(
"raw tokens seen by outer macro: {:?} -> evaluated: {}",
stringify!($($tt)*),
$($tt)*
);
};
}
fn main() {
// Because expansion is outside-in, `inspect_tokens!` sees `inner_value! ()`
// as raw tokens before `inner_value!()` is expanded for the arithmetic evaluation!
inspect_tokens!(inner_value!());
}
raw tokens seen by outer macro: "inner_value! ()" -> evaluated: 42
Exception: Eager Expansion in Built-In Compiler Macros
A small set of built-in compiler macros that require a resolved string literal at compile time—such as include!, include_str!, include_bytes!, concat!, env!, and format_args!—expand their macro arguments eagerly (inside-out) so you can write concat!("v", env!("CARGO_PKG_VERSION")) or #[doc = concat!("...", stringify!(...))].
Syntactic Positions Where Macros Can Expand
The parser determines what kind of AST node a macro expansion must produce based on the syntactic position of the macro call:
| Syntactic Position | Example | Allowed Macro Output |
|---|---|---|
| Item position (module/crate/impl body) | generate_struct!(User); |
Zero or more items (fn, struct, impl, const, use) |
| Statement position | log_event!("boot"); |
Zero or more statements or an expression |
| Expression position | let x = vec![1, 2]; |
A single expression (wrap multiple statements in a { ... } block!) |
| Type position | let y: TupleOf!(u32, bool); |
A single valid type |
| Pattern position | match v { Pat!(10) => ... } |
A single pattern |
Recursion Limit (#![recursion_limit = "..."])
To prevent infinite macro expansion loops from freezing the compiler, rustc caps nested macro expansions at a default depth of 128. If a deeply recursive macro exceeds this depth, compilation fails with error: recursion limit reached while expanding ..., which can be raised at the crate root via #![recursion_limit = "256"].