Declarative and Procedural Macros in Depth

Macro Matching

Macro matching is the top-down pattern-matching algorithm used by macro_rules! to test an invocation's input token stream against each rule's matcher pattern (($pattern) => { $expansion };) and bind matched grammar fragments to metavariables ($name:designator).

Top-Down Rule Evaluation and No-Backtracking Within Fragment Specifiers

When a macro_rules! macro is invoked:

  1. The expander tests each rule in top-to-bottom declaration order and selects the first rule whose matcher completely consumes the input token stream.
  2. When matching a typed fragment specifier ($e:expr, $t:ty, $p:pat, $s:stmt, $i:item), the parser parses the fragment greedily without backtracking.

Because typed fragment parsers do not backtrack, Rust enforces follow-set restrictions: the token immediately following a fragment specifier must belong to that specifier's allowed follow set, or the macro definition itself is rejected at compile time!

Complete Reference of Fragment Specifiers and Their Follow Sets (Rust 2024 Edition)

Specifier Matches Allowed Following Tokens (Follow Set)
expr Any valid Rust expression (including _ and const {} in 2024) =>, ,, ; (plus closing ), ], })
expr_2021 Pre-2024 expression (excludes _ and const {} expressions) =>, ,, ;
stmt A statement without its trailing semicolon =>, ,, ;
ty Any type (u32, Vec<T>, &'a str, impl Trait) =>, ,, ;, =, |, :, >, >>, [, {, as, where, or a block
pat Any pattern (including top-level | or-patterns: A | B) =>, ,, =, if, in
pat_param Single pattern without top-level | =>, ,, =, |, if, in, :
path A TypeStyle path (std::mem::replace, Vec::<u8>) Same as ty
ident An identifier or keyword (foo, self, r#type) Any token
lifetime A lifetime token ('a, 'static, '_) Any token
literal A literal (42, -10, "hi", 'c', true) Any token
block A { ... } block expression Any token
item A module-level item (fn, struct, impl, use) Any token
meta The contents of an attribute (derive(Debug)) Any token
vis An optional visibility qualifier (pub, pub(crate), or empty!) Almost any token starting an item or ident
tt A single token tree (single token or balanced ()/[]/{}) Any token

Follow-Set Compile Error ($a:expr $b:expr)

Attempting to follow $a:expr with whitespace or an arbitrary operator like -> (which could be ambiguous or outside expr's follow set =>, ,, ;) fails when compiling the macro definition:

// ❌ does not compile
macro_rules! bad_arrow {
    // Error: `$lhs:expr` can only be followed by `=>`, `,`, or `;` (not `->`)!
    ($lhs:expr -> $rhs:expr) => {
        ($lhs, $rhs)
    };
}

fn main() {
    let _ = bad_arrow!(1 -> 2);
}
error: `$lhs:expr` is followed by `->`, which is not allowed for `expr` fragments
 --> src/main.rs:4:16
  |
4 |     ($lhs:expr -> $rhs:expr) => {
  |                ^^ not allowed after `expr` fragments
  |
  = note: allowed there are: `=>`, `,` or `;`

Using => (which is in the follow set of expr) or wrapping $lhs:expr inside delimiters ($lhs:expr) -> ($rhs:expr) resolves the ambiguity cleanly.

Ordering Rules from Most Specific to Most General

Because macro_rules! selects the first matching rule from top to bottom, place specific literal/keyword rules above general catch-all $e:expr or $($tt:tt)* rules:

macro_rules! route {
    // Specific literal/keyword rules first:
    (GET $path:literal => $handler:ident) => {
        println!("registered GET route {} -> {}", $path, stringify!($handler))
    };
    (POST $path:literal => $handler:ident) => {
        println!("registered POST route {} -> {}", $path, stringify!($handler))
    };
    // General fallback rule last:
    ($method:ident $path:literal => $handler:ident) => {
        println!(
            "registered custom {} route {} -> {}",
            stringify!($method),
            $path,
            stringify!($handler)
        )
    };
}

fn main() {
    route!(GET "/health" => check_health);
    route!(POST "/users" => create_user);
    route!(DELETE "/users/1" => delete_user);
}
registered GET route /health -> check_health
registered POST route /users -> create_user
registered custom DELETE route /users/1 -> delete_user