Declarative and Procedural Macros in Depth

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"].