The mechanics of declarative macros (macro_rules!) in Rust: matcher and transcriber syntax, every fragment specifier, repetition operators, follow-set restrictions, mixed-site hygiene, $crate, TT-munching recursion, #[macro_export] and #[macro_use] scoping, and the edition 2024 expr widening.
Cross-references: [[rust-syntax-macros-procedural]] (when to graduate to proc-macros) [[rust-syntax-edition-2024]] (edition 2024 expr widening, pat 2021 widening) [[rust-syntax-pattern-matching]] (same matcher-style mindset).
When to use this skill
- User writes a
macro_rules! my_macro { ... }and the matcher does not compile or does not match the call site - User asks "which fragment specifier should I use for X" (ident vs expr vs tt vs path)
- User writes a repetition
$( ... )*and gets E0617 ("ambiguity") or duplicate-binding errors - User writes
#[macro_export]and the macro fails to find its own helper items from a downstream crate - User implements a counter / TT-muncher and hits "recursion limit reached"
- User upgrades to edition 2024 and an
exprfragment now matches_orconst { ... }where it did not before - User asks "what is
$crate", "what is$tt", "why does my macro see my outerxand not the caller'sx" - User mixes
#[macro_use]with the 2018+ path-based import and is confused which one to use
For procedural macros (#[derive(...)], attribute macros, function-like proc-macros) see [[rust-syntax-macros-procedural]]. For edition 2024 specifics see [[rust-syntax-edition-2024]].
Quick reference: fragment specifiers
| Specifier | Matches | Follow-set (tokens that may follow $x:F) |
|---|---|---|
block |
a brace-delimited block expression { ... } |
any token |
expr |
an expression (edition 2024: also _ and const { ... } at top level) |
=>, ,, ; |
expr_2021 |
an expression excluding top-level _ and const { ... } |
=>, ,, ; |
ident |
an identifier or keyword (not _, not $crate) |
any token |
item |
a top-level item (fn, struct, impl, mod, ...) | any token |
lifetime |
a 'a style lifetime token |
any token |
literal |
a literal optionally preceded by - |
any token |
meta |
the contents of an attribute (#[meta]) |
any token |
pat |
a pattern, including top-level | (since edition 2021) |
=>, ,, =, |, if, in |
pat_param |
a pattern WITHOUT top-level | |
=>, ,, =, |, if, in |
path |
a TypePath-style path (std::vec::Vec, crate::m::T) |
=>, ,, =, |, ;, :, >, >>, [, {, as, where |
stmt |
a statement WITHOUT trailing ; (the matcher does not consume the ;) |
=>, ,, ; |
tt |
a single token tree (one token, or a matched delimiter group) | any token |
ty |
a type | =>, ,, =, |, ;, :, >, >>, [, {, as, where |
vis |
a possibly empty visibility (pub, pub(crate), pub(in ...), or nothing) |
,, an identifier, a keyword that begins an item, (, [, { |
ALWAYS pick the most specific fragment that works. NEVER reach for tt unless you genuinely need raw token capture (TT-munching, pass-through). tt defeats most safety the macro system gives you because it accepts anything but loses semantic shape.
ALWAYS match the follow-set: writing $e:expr $name:ident is rejected because no token may follow expr other than =>, ,, or ;. Insert a literal separator (e.g. ,) between fragments to make the call site unambiguous.
Source: Rust Reference, macros-by-example
Decision tree: pick a fragment specifier
Capturing a name (variable, fn, type-name token)?
├── one identifier → ident
└── a path with :: → path
Capturing a value or computation?
├── any expression → expr (or expr_2021 if you need pre-2024 semantics)
├── a block { ... } → block
└── a statement, no `;` → stmt
Capturing a type position?
└── any type → ty
Capturing a pattern?
├── allow `A | B` → pat
└── single alternative → pat_param
Capturing an attribute body?
└── meta
Capturing literals?
└── literal
Capturing lifetimes (`'a`)?
└── lifetime
Capturing visibility (pub, pub(crate), empty)?
└── vis
Capturing an item (fn, struct, impl)?
└── item
Capturing arbitrary tokens (TT-munching, pass-through)?
└── tt (last resort)
Minimal macro_rules! syntax
macro_rules! my_macro {
( $name:ident, $value:expr ) => {
let $name = $value;
};
}
fn main() {
my_macro!(x, 1 + 2);
println!("{x}");
}
Grammar (from the Rust Reference):
MacroRulesDef := ( MacroRules ) ; // or [ MacroRules ] ; or { MacroRules }
MacroRules := MacroRule ( ; MacroRule )* ;?
MacroRule := MacroMatcher => MacroTranscriber
MacroMatcher := ( MacroMatch* ) // or [ ... ] or { ... }
MacroTranscriber:= DelimTokenTree
Rules :
- Multiple
(matcher) => { transcriber }arms are allowed; first matching arm wins. - The matcher delimiter is independent of the call-site delimiter.
my_macro!(x),my_macro![x], andmy_macro!{x}all parse the same matcher. - Metavariables are written
$name:fragmentin the matcher and$namein the transcriber. - Literal tokens in the matcher MUST appear verbatim at the call site (commas, semicolons, keywords, punctuation).
Source: Rust Reference, macros-by-example
Repetition: $( ... )*, $( ... )+, $( ... )?
macro_rules! vec_of {
( $( $x:expr ),* $(,)? ) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
let v: Vec<i32> = vec_of!(1, 2, 3,);
Rules :
*= zero or more,+= one or more,?= zero or one.- A separator token (e.g.
,) goes between)and the repetition operator:$( $x:expr ),*. ?repetition MUST NOT have a separator.$(,)?is correct (one optional comma);$( $x:expr ),?is rejected.- The transcriber MUST mirror the same repetition shape:
$( v.push($x); )*mirrors the$( $x:expr ),*matcher. - Multiple metavariables inside one repetition group MUST have the same length:
$( $k:ident = $v:expr ),*binds$kand$vtogether. - Nested repetitions are allowed:
$( $( $x:expr ),* );*: outer;-separated groups of inner,-separated lists.
ALWAYS allow a trailing separator at the call site using $(,)? (or $(;)?). Users expect vec![1, 2, 3,] to parse.
Source: Rust Reference, macros-by-example
Metavariable expressions (stable Rust 1.86+)
Inside the transcriber, the following expressions read repetition state:
| Expression | Meaning |
|---|---|
${count($x)} |
total number of times $x was matched (outermost) |
${count($x, depth)} |
count at a given nesting depth |
${index()} / ${index(depth)} |
current zero-based repetition index |
${len()} / ${len(depth)} |
length of the current repetition |
${ignore($x)} |
mention $x to bind it to the surrounding repetition without expanding it |
$$ |
literal $ token (escape) |
macro_rules! enumerated {
( $( $name:ident ),* $(,)? ) => {
$( println!("{} = {}", ${index()}, stringify!($name)); )*
};
}
enumerated!(alpha, beta, gamma);
// 0 = alpha 1 = beta 2 = gamma
ALWAYS use ${count(...)} instead of hand-rolled TT-munching when you only need the count. NEVER recurse for what a metavariable expression handles directly.
Source: Little Book of Rust Macros, metavariable expressions
Hygiene: mixed-site, in plain language
Macros by example have mixed-site hygiene :
- Local variables, loop labels, and block labels resolve at the macro definition site.
- All other symbols (functions, types, modules, traits, statics) resolve at the macro invocation site.
let x = 1;
fn func() { unreachable!("definition site") }
macro_rules! check {
() => {
assert_eq!(x, 1); // `x` is the definition-site x (== 1)
func(); // `func` is whatever func() exists at the call site
};
}
fn main() {
let x = 2;
fn func() { /* called by check!() */ }
check!(); // assert passes; the call-site func() runs
}
Rules :
- NEVER assume a local variable name in the caller's scope. If the macro needs a variable, declare it inside the transcriber.
- NEVER assume a type or function is in scope at the call site. Use absolute paths via
$crate::...(see next section). - ALWAYS test the macro from a different crate AND from a different module to confirm hygiene assumptions.
Source: Rust Reference, macros-by-example § Hygiene
$crate: the absolute path to the defining crate
When a #[macro_export] macro references its own crate's items, ALWAYS use $crate::.... Plain crate::... resolves at the invocation site, not the definition site, so it breaks the moment the macro is called from another crate.
// In crate `helper`:
pub mod internals {
pub fn helper_fn() {}
}
#[macro_export]
macro_rules! call_helper {
() => { $crate::internals::helper_fn() };
}
// In a downstream crate:
use helper::call_helper;
fn main() {
call_helper!(); // expands to `helper::internals::helper_fn()` -- correct
}
Rules :
- ALWAYS write
$crate::path::to::iteminside#[macro_export]macros. $cratedoes NOT bypass visibility.helper_fnMUST still bepub(and reachable throughpub mod).$crateis itself a token, so${ignore($crate)}is illegal: treat it as a path prefix only.
Source: Rust Reference, macros-by-example § $crate
Scoping: #[macro_export] vs #[macro_use] vs path imports
| Mechanism | Scope | When to use |
|---|---|---|
#[macro_export] on the macro |
exports the macro at the crate root | crates that publish macros for downstream users |
use crate::path::my_macro; (2018+) |
bring an exported macro into scope by path | the modern way to consume macros |
#[macro_use] extern crate foo; |
textual import of every exported macro of foo |
legacy 2015 edition only |
#[macro_use] mod m; |
textual import of every macro_rules! from a child module |
discouraged; prefer pub(crate) use m::name; |
ALWAYS prefer pub use crate::path::name; (edition 2018+) for both internal and external macro re-export. #[macro_use] is order-sensitive and brittle; reserve it for crates that still ship as 2015.
A macro_rules! macro without #[macro_export] is private to the module that defined it. Re-export it with pub(crate) use my_macro; or pub use my_macro; to widen its scope without exposing it at the crate root.
Source: Rust Reference, macros § Path-Based Scope
TT-munching: recursion for tokens-as-data
TT-munching processes a token stream one token tree at a time, prepending it to an accumulator and recursing on the rest. Canonical shape :
macro_rules! count_tts {
() => { 0_usize };
( $head:tt $( $tail:tt )* ) => { 1_usize + count_tts!( $( $tail )* ) };
}
const N: usize = count_tts!(a b c d e); // 5
Rules :
- ALWAYS provide a base case (matcher with zero tokens) BEFORE the recursive case.
- ALWAYS use
:ttfor pass-through tokens. Other fragment specifiers consume too much and break further recursion. - NEVER recurse beyond what is necessary. Deep recursion hits the crate-level
recursion_limit(default 128). Raise it explicitly at the crate root:#![recursion_limit = "256"]. - PREFER the metavariable expression
${count($x)}(Rust 1.86+) over hand-rolled token counters. TT-munching for counting is now a legacy pattern. - Use
${ignore($x)}to bind a metavariable to a repetition without expanding it (e.g. when generating fresh tokens per iteration).
Source: Rust Reference, macros-by-example Source: Little Book of Rust Macros, counting
Edition 2024: expr widening
In edition 2024 the expr fragment specifier was widened to also accept :
- the underscore expression
_at the top level - the const block expression
const { ... }at the top level
Pre-2024 (expr_2021) accepted neither at the top level.
// Edition 2024 only:
macro_rules! capture {
( $e:expr ) => { /* ... */ };
}
capture!(_); // OK in 2024, rejected in 2021
capture!(const { 1 + 2 }); // OK in 2024, rejected in 2021
Migration rules :
- ALWAYS use plain
exprfor new edition-2024 macros. - Use
expr_2021ONLY when emulating the pre-2024 narrower matcher (rare; typically when a macro accepts bothexprand a follow-on_token and would now be ambiguous). - When porting a public macro to edition 2024, audit existing arms: an
exprarm may now greedily capture inputs that previously fell through to a later arm.
Source: Rust Edition Guide, expr fragment Source: Rust 1.85 release notes
Anti-patterns (summary, see references/anti-patterns.md for the full list)
- Relying on caller-defined names: macros assume the caller has a
resultvariable in scope. Mixed-site hygiene defeats this. ALWAYS bind your own variables in the transcriber. - Missing
$cratein#[macro_export]: works from the defining crate, breaks from every consumer. ALWAYS prefix internal items with$crate::. - Wrong fragment specifier (
identvsexpr):identrejectsfoo.bar,foo(),&x. Useexprwhen you accept any expression; reserveidentfor single-token names. - Hitting the recursion limit on simple counters: hand-rolled TT-munching counters blow the recursion budget. Use
${count($x)}(Rust 1.86+). - Edition 2024
exproverreach: a permissiveexprarm shadows what used to fall through to a later arm. Audit arm ordering after migration. - Three-deep macro nesting where a function would do: declarative macros are for syntactic patterns and code generation. NEVER chain macros to express runtime logic: write a
fn.
Reference files
references/methods.md: full grammar tables, every fragment specifier with edition deltas, full repetition operator semantics, full follow-set table, metavariable-expression catalog.references/examples.md: runnable examples: trivial macro,vec![...]-style builder, hashmap-literal, TT-munching counter,${count(...)}counter,$cratecross-crate macro, hygiene demo, edition 2024exprwidening demo.references/anti-patterns.md: eight failure modes with reproductions, root causes, and the deterministic fix for each.