SuperAPI AI Directory

rust-syntax-macros-declarative

Coding & development

View repository

Updated 10-16 06:18

About this skill

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 :

  1. Multiple (matcher) => { transcriber } arms are allowed; first matching arm wins.
  2. 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.
  3. Metavariables are written $name:fragment in the matcher and $name in the transcriber.
  4. 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 :

  1. * = zero or more, + = one or more, ? = zero or one.
  2. A separator token (e.g. ,) goes between ) and the repetition operator: $( $x:expr ),*.
  3. ? repetition MUST NOT have a separator. $(,)? is correct (one optional comma); $( $x:expr ),? is rejected.
  4. The transcriber MUST mirror the same repetition shape: $( v.push($x); )* mirrors the $( $x:expr ),* matcher.
  5. Multiple metavariables inside one repetition group MUST have the same length: $( $k:ident = $v:expr ),* binds $k and $v together.
  6. 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 :

  1. NEVER assume a local variable name in the caller's scope. If the macro needs a variable, declare it inside the transcriber.
  2. NEVER assume a type or function is in scope at the call site. Use absolute paths via $crate::... (see next section).
  3. 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 :

  1. ALWAYS write $crate::path::to::item inside #[macro_export] macros.
  2. $crate does NOT bypass visibility. helper_fn MUST still be pub (and reachable through pub mod).
  3. $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); // 5

Rules :

  1. ALWAYS provide a base case (matcher with zero tokens) BEFORE the recursive case.
  2. ALWAYS use :tt for pass-through tokens. Other fragment specifiers consume too much and break further recursion.
  3. 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"].
  4. PREFER the metavariable expression ${count($x)} (Rust 1.86+) over hand-rolled token counters. TT-munching for counting is now a legacy pattern.
  5. 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 :

  1. ALWAYS use plain expr for new edition-2024 macros.
  2. 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).
  3. 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)

  1. 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.
  2. Missing $crate in #[macro_export]: works from the defining crate, breaks from every consumer. ALWAYS prefix internal items with $crate::.
  3. 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.
  4. Hitting the recursion limit on simple counters: hand-rolled TT-munching counters blow the recursion budget. Use ${count($x)} (Rust 1.86+).
  5. Edition 2024 expr overreach: a permissive expr arm shadows what used to fall through to a later arm. Audit arm ordering after migration.
  6. 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.