Two Halves of a Rule: Matchers, Metavariables, and Literal Transcription
A macro_rules! definition looks unlike any other item in Rust, but it does exactly one job: take tokens in, put different tokens out. Expansion runs while the compiler is still assembling the syntax tree — before name resolution finishes for the generated code, long before type checking — so a macro matches syntax, never values. Read every definition as a two-part transformation: the matcher left of => decides which token shapes the macro accepts, and the transcriber right of => is the template pasted into the call site with metavariables substituted.
macro_rules! square {
($x:expr) => { ($x) * ($x) };
}
let n = square!(5 + 1); // the tokens (5 + 1) * (5 + 1)
assert_eq!(n, 36);
Five details in that snippet carry the fundamentals:
$x:exprdeclares a metavariable: bind the namexto one syntactic expression. The fragment specifier (expr) tells the matcher where the construct ends — it consumes a balanced, valid expression rather than an arbitrary run of tokens.- Rules are tried top to bottom, first match wins, exactly like
match. rustc looks up the macro by name, tries each rule in turn, and transcribes the first successful match; if transcription itself errors, later rules are not tried. - Every rule has the shape
MacroMatcher => MacroTranscriber, and both halves must sit inside(),[], or{}. The outer delimiters of the matcher match any pair of delimiters, so a matcher written(())matches the invocation{()}but not{{}}. - The transcriber is literal token substitution. Nothing runs during expansion, and grouping is not inserted for you: if the template says
$x * $x, thensquare!(5 + 1)expands to5 + 1 * 5 + 1and evaluates to 11, not 36. Missing parentheses are the single most common macro_rules bug. - A metavariable may be transcribed zero times, once, or many times. Writing
$xtwice duplicates the tokens —5 + 1is written out and re-parsed twice — so unlike a function parameter there is no single evaluation whose result both uses share.
| Specifier | Matches | What may legally follow it |
|---|---|---|
expr / stmt |
one expression / one statement (no trailing ;) |
only =>, ,, ; |
ty / path |
one type / one type path | =>, ,, =, ` |
ident |
an identifier or keyword — not _, not a raw r#… identifier |
anything |
tt |
one token tree: a single token or one delimited group | anything |
block, item, pat, literal, lifetime, vis, meta |
the obvious syntactic unit | essentially unrestricted |
The second column of that table explains errors that puzzle newcomers. Writing ($a:expr + $b:expr) fails with expected ,: an expression may only be followed by =>, ,, or ;, because [ can legally continue an expression (x[0] is an index), so the parser refuses to place an expression next to a token it cannot unambiguously separate. Split the operands into ($a:expr, $b:expr) and let the template write $a + $b.
Two consequences shape every macro you design. First, matchers work on token trees rather than on Rust's grammar, so you can describe shapes the language has no syntax for — ($name:ident : $t:ty) or ($($k:ident),+ => $v:expr) — which is how logging macros, format_args!-style formatting, and small embedded DSLs are built. Second, a captured fragment forwarded into another macro arrives as an opaque AST: in ($l:expr) => { other!($l) }, the callee cannot match $l against the literal token 3; only ident, lifetime, and tt fragments remain matchable by literal tokens downstream. Capture as tt when the next macro must discriminate on the value itself.
Delimiters at the call site are interchangeable for matching: square!(5), square![5], and square!{5} all hit the same rule. Choosing between them is style, plus one grammar rule about semicolons covered in the final chapter. For contrast, Feature Flags gave you #[cfg(feature = "...")] — conditional compilation over code that already parses. macro_rules! is the generative counterpart: new code that did not exist before expansion. Next you will add repetition over lists and the hygiene rules that stop generated identifiers from colliding with caller names in Repetition, Hygiene, and Macro Patterns.
Expansion Runs Before Types Exist: Where Macro Errors Come From
A macro is expanded during parsing, which means everything it emits is type-checked as ordinary caller code after the fact. The pipeline is roughly: parse the crate, expand macro invocations (possibly producing new invocations to expand), resolve names, then check types. Two habits fall out of that ordering.
First, errors report at the call site, not at the line inside your definition. Given:
macro_rules! shout {
($msg:expr) => { println!("{}!!", $msg.to_uppercase()) };
}
fn main() {
shout!(42);
}
the failure — no method named \to_uppercase` found for type `{integer}`— is reported on theshout!(42)line with anin this macro invocation` note, because that is where the tokens that carry a call-site span live. When you see an error pointing at a perfectly ordinary-looking invocation, the actual mistake is often inside the macro's template; the invocation is only where the compiler could pin the blame.
Second, a macro cannot ask about types. Fragment specifiers (expr, ty, pat) are purely syntactic — they tell the matcher where a construct ends, not what it means. There is no $x:type that resolves to a concrete type, no way to branch on "is this a String or a &str". Generated code must be polymorphic instead: call .clone(), Into::into(...), or let inference work backwards from how the result is used. If a design needs type-directed behavior, you want a trait method or a procedural macro that emits trait-bounded generic code — not more macro_rules! arms.
When the template itself emits nonsense, the diagnostics get cryptic. The escape hatch is compile_error!, which expands to an error carrying your message at the invocation:
macro_rules! my_vec {
() => { compile_error!("my_vec! requires at least one element") };
($($x:expr),+ $(,)?) => { vec![$($x),+] };
}
A well-built macro spends its arms distinguishing caller mistakes and turns each into a compile_error! with a sentence naming the fix, instead of letting a deep follow-set or type error surface three layers down. You will use this heavily when writing repeaters.
Three failure modes are worth recognizing by name:
error: recursion limit reached while expanding \count!`` — a rule that expands to itself (the muncher pattern of the next topic) blew the default#![recursion_limit = "128"]. Raise it at the crate root only after confirming the recursion is meant to terminate.local ambiguity: multiple parsing options— the matcher parser consumes one token at a time with no lookahead or backtracking.($($i:ident)* $j:ident)invoked asm!(error)cannot decide whethererrorextends the repetition or binds$j, and instead of guessing it refuses.expected \,`inside amacro_rules!` definition — a follow-set violation: a fragment is followed by a token the grammar forbids after it (see the table in the previous chapter).
Because errors describe the expansion rather than your intent, inspecting the expansion is the fastest debugging tool. cargo install cargo-expand once, then cargo expand prints the crate with every macro already substituted — the compiler's own -Zunpretty=expanded output — so you can read the tokens that will actually be type-checked and see exactly which of your arms fired. Use it when an error message points into generated code you do not recognize; the expansion usually makes the mismatch obvious in seconds. The alternative to reading expansion blind is writing macros whose failure modes you pre-empted with compile_error!, which the Repetition, Hygiene, and Macro Patterns topic puts into practice.
Expression, Statement, or Item: What the Semicolon Decides
The same m!(...) call means three different things depending on where it appears, and the semicolon after it is what settles the question. The grammar recognizes three positions, and each one constrains what your transcriber may legally produce.
| Position | Example call | The expansion must be |
|---|---|---|
| Expression | let v = vec![1, 2, 3]; |
exactly one expression |
| Statement | debug_locals!(); inside a block |
zero or more statements |
| Item | lazy_static! { ... } at module level or in a block |
items (fn, struct, impl, static…) |
In expression position the template must collapse to a single expression. Put a semicolon on it and you no longer have an expression — you have an expression statement: the value is computed and dropped.
fn names() -> Vec<String> {
let v = vec!["a".into()];
v; // deliberate semicolon
}
// error[E0308]: mismatched types
// expected `Vec<String>`, found `()`
The function now returns unit, because the block's final statement evaluates to (). The same trap fires on must_use results: vec![1, 2, 3]; as a statement warns unused \Vec` that must be used` while compiling perfectly. When a macro expands to something with a value, the semicolon is a decision about whether you keep that value — and it is checked only after expansion, which is why the mistake shows up as a type error far from your mental model of the call.
In statement position, the expansion may be several statements, and this is where multi-statement macros live:
macro_rules! debug_locals {
() => {
let a = 1;
let b = 2;
println!("a={a} b={b}");
};
}
fn main() {
debug_locals!(); // OK: statement position swallows the three statements
// let x = debug_locals!(); // rejected: this position demands one expression
}
The trailing semicolon in statement position is part of the statement syntax, not part of the macro call; braced invocations debug_locals! {} work too. What matters is that nothing here can be spliced into an expression context — the tokens exist as siblings in the block, not as a value.
Item position carries the strictest punctuation rule: an invocation using (...) or [...] delimiters must be terminated by a semicolon, while {...} needs none. That is the entire reason macros like lazy_static!, bitflags!, and cfg_if! are written with braces and no trailing ;, and why include!("schema.sql"); always has one. Inside a function body, item-position expansions are legal too — items are valid statements in a block — so a macro that generates a static or an impl works in both places.
Bracket choice then becomes documentation: f!(...) reads like a function call, vec![...] like indexing a collection, thread_local! { ... } like a block declaration. The delimiter never changes matching (the matcher's outer delimiters match any pair); it changes only how the call is terminated and how it reads.
Finally, the classic interview question — why not just write a function? — is answered by position and phase together. A function is parsed and type-checked once at its definition, takes a fixed number of typed arguments, and evaluates at runtime. A macro runs before type checking, accepts any arity and unparseable-as-values shapes (vec![a, b] where each element is written out again, println!("{}-{}", a, b) with one argument per placeholder), and expands to whatever position it sits in — all at zero runtime cost, because by the time codegen runs, only the expansion remains. Reach for a function whenever the inputs are already values; reach for macro_rules! only when you need the syntax.
Quiz
1. You define `macro_rules! add { ($a:expr + $b:expr) => { $a + $b }; }` and it fails to compile. What is the error and the reason?
2. `macro_rules! square { ($x:expr) => { $x * $x }; }` — what does `square!(2 + 3)` evaluate to?
3. A macro-generated expression calls `.to_uppercase()` on an `i32`. Where does rustc report the failure?
4. You need a macro that generates a `static CONFIG: Config` and you want to invoke it at module level and inside a function body. Which invocation syntax is correct?
Flashcards
Question
What are the two halves of a `macro_rules!` rule, and when does each run?
Click to reveal answer
Answer
The matcher (left of `=>`) declares the accepted token shape with `$name:fragment` bindings; the transcriber (right of `=>`) is the token template substituted at the call site. Both run at expansion time — during parsing, before name resolution and type checking — so everything is syntax, and no value is evaluated until the expanded code runs normally.
Question
Why does `($a:expr + $b:expr)` fail to compile while `($a:expr, $b:expr)` does not?
Click to reveal answer
Answer
Follow-set restriction: an `expr` (and `stmt`) fragment may only be followed by `=>`, `,`, or `;`. The parser works one token at a time without lookahead and refuses placements that could later be ambiguous — an operator like `+` after an expression. Put the operator in the transcriber instead.
Question
Why can a macro's `($x:expr) => { $x * $x }` produce a wrong arithmetic result, and what is the fix?
Click to reveal answer
Answer
Transcription duplicates tokens without adding grouping, so `square!(5 + 1)` becomes `5 + 1 * 5 + 1` = 11, not 36 — there is no 'capture once' evaluation as with function parameters. Fix: parenthesize in the template, `($x) * ($x)`.
Question
Name the three syntactic positions an invocation can occupy and what the expansion must be in each.
Click to reveal answer
Answer
Expression position (`let v = m!()`) — must expand to exactly one expression; statement position (`m!();` in a block) — may expand to multiple statements; item position (module level or in a block) — must expand to items, and requires a trailing `;` when delimited by `()` or `[]` (braces need none).
Question
How do you see the actual token stream a macro produced, and what two errors indicate a matcher problem?
Click to reveal answer
Answer
`cargo install cargo-expand` once, then `cargo expand` prints the crate with all macros substituted (the `-Zunpretty=expanded` view). Matcher problems show up as `expected \`,\`` (follow-set violation) and `local ambiguity: multiple parsing options` (no lookahead — restructure the rules so one token path is unambiguous).
Revision Notes
Key Takeaways
- 1. A rule is `matcher => transcriber`: first matching arm wins, transcription is literal token substitution, and no parentheses or single evaluation are inserted for you
- 2. Fragment specifiers are syntactic and carry follow-set restrictions — `expr`/`stmt` only before `=>`, `,`, `;` — which is where most definition-site "expected `,`" errors come from
- 3. Expansion runs before name resolution and type checking, so macro errors point at the invocation with an `in this macro invocation` note and macros can never query types
- 4. Position decides what an expansion may be: expression (one expression), statement (several statements), item (items; `;` required unless braced)
- 5. Debug with `cargo expand` to read real tokens and with `compile_error!` to make caller mistakes fail loudly at the call site
Interview Tips
- • "Why not write this as a function?" — a function is parsed and type-checked once with fixed arity and typed args; a macro runs before type checking, matches syntax, allows any arity and repeated code, and costs nothing at runtime. Say you default to functions and use macros only for syntax (variadic formatting, collection literals, codegen)
- • "What does `$crate` do?" — it names the crate that defined the macro so exported macros can call their own helpers regardless of how the consumer renamed the dependency; the topic's sibling, hygiene, covers it in depth
- • "Explain what happens when `square!(5 + 1)` runs" — walk the pipeline: match `expr`, bind tokens `5 + 1`, transcribe `5 + 1 * 5 + 1`, precedence gives 11; then give the parenthesized fix
- • "How do you debug a macro that produces garbage?" — `cargo expand` to see the substituted stream, `compile_error!` to convert silent follow-set/type failures into readable messages, and the recursion-limit/ambiguity errors named from memory
Cheat Sheet
macro_rules! Fundamentals
Rule shape: macro_rules! name { (matcher) => {transcriber}; ... }
- Rules tried top-down, first match wins; a transcription error is not retried.
- Outer delimiters
(),[],{}match any pair;$starts a metavariable (never literal). - Matcher/transcriber are pure tokens: no grouping added, no evaluation — always write
($x) * ($x).
Fragment specifiers and follow sets:
| Spec | Matches | May be followed by |
|---|---|---|
expr, stmt |
expression / statement | =>, ,, ; |
ty, path |
type / path | =>, ,, =, ` |
pat |
pattern | =>, ,, =, if, in |
ident, tt, literal, lifetime, vis, meta, block, item |
that unit | unrestricted |
Forwarding: a fragment passed to another macro is an opaque AST of its kind — only ident, lifetime, tt can be matched by literal tokens downstream. Capture tt when the callee must discriminate.
Positions:
let v = vec![1, 2, 3]; // expression: one expression
m!(); // statement: may expand to several statements
lazy_static! { ... } // item: braces; or m!(...); with a semicolon
Semicolon after an expression = expression statement → value dropped, must_use warns, tail position returns ().
Expansion pipeline & debugging:
parse -> expand macros -> resolve names -> type check
- Errors surface at the call site (
in this macro invocation). cargo expandshows post-expansion tokens;compile_error!("msg")turns caller mistakes into readable errors.recursion limit reached while expanding→#;local ambiguity→ matcher needs lookahead you cannot give it.