| name | rust-syntax-macros-declarative |
| description | Use when the user writes a `macro_rules!`, debugs a macro hygiene issue, counts tokens via TT-munching, picks fragment specifiers, handles repetitions, or uses `$crate` for cross-crate macro references. Prevents fragment-specifier mismatches, identifier hygiene mistakes, missing `$crate` in exported macros, and naive recursion limits. Covers: `macro_rules!` declaration, all fragment specifiers (ident, expr, ty, pat, path, block, stmt, item, tt, meta, vis, literal, lifetime), repetition `$(...)+` `$(...)*` with separators, hygiene rules, `$crate`, TT-munching recursion, edition-2024 `expr` fragment widening, `macro_rules!` import scoping. Keywords: macro_rules, "declarative macro", "macro by example", fragment specifier, "ident expr ty pat path block stmt item tt meta vis literal lifetime", repetition, "$()*", "$()+", hygiene, "$crate", "TT munching", "recursion limit", "expr fragment 2024", "macro export", "macro_export", "macro_use", "what is $tt".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-syntax-macros-declarative
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
expr fragment now matches _ or const { ... } where it did not before
- User asks "what is
$crate", "what is $tt", "why does my macro see my outer x and not the caller's x"
- 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], and my_macro!{x} all parse the same matcher.
- Metavariables are written
$name:fragment in the matcher and $name in 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 $k and $v together.
- 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);
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);
func();
};
}
fn main() {
let x = 2;
fn func() { }
check!();
}
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.
pub mod internals {
pub fn helper_fn() {}
}
#[macro_export]
macro_rules! call_helper {
() => { $crate::internals::helper_fn() };
}
use helper::call_helper;
fn main() {
call_helper!();
}
Rules :
- ALWAYS write
$crate::path::to::item inside #[macro_export] macros.
$crate does NOT bypass visibility. helper_fn MUST still be pub (and reachable through pub mod).
$crate is 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);
Rules :
- ALWAYS provide a base case (matcher with zero tokens) BEFORE the recursive case.
- ALWAYS use
:tt for 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.
macro_rules! capture {
( $e:expr ) => { };
}
capture!(_);
capture!(const { 1 + 2 });
Migration rules :
- ALWAYS use plain
expr for new edition-2024 macros.
- Use
expr_2021 ONLY when emulating the pre-2024 narrower matcher (rare; typically when a macro accepts both expr and a follow-on _ token and would now be ambiguous).
- When porting a public macro to edition 2024, audit existing arms: an
expr arm 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
result variable in scope. Mixed-site hygiene defeats this. ALWAYS bind your own variables in the transcriber.
- Missing
$crate in #[macro_export]: works from the defining crate, breaks from every consumer. ALWAYS prefix internal items with $crate::.
- Wrong fragment specifier (
ident vs expr): ident rejects foo.bar, foo(), &x. Use expr when you accept any expression; reserve ident for 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
expr overreach: a permissive expr arm 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, $crate cross-crate macro, hygiene demo, edition 2024 expr widening demo.
references/anti-patterns.md: eight failure modes with reproductions, root causes, and the deterministic fix for each.