| |

Rust 75 🦀 Declarative Macros (macro_rules!) Syntax

⏱️ Estimated read: 14 min | |

Declarative macros are Rust’s pattern-matching system for generating code at compile time. A macro_rules! definition is a named list of rules, each with a matcher on the left and a transcriber on the right. At the invocation site, the macro expander tries each rule in order, and the first matcher that succeeds produces the output . The system is called “macros by example” because the matcher is a literal example of the syntax the macro accepts, and the transcriber is the template for what it generates.

The mechanism is fundamentally different from a function. A function receives values after type checking. A macro receives raw tokens before parsing and type checking, which means it can generate items, types, patterns, and expressions that a function cannot . It can accept a variable number of arguments, repeat code for each element in a list, and define entirely new syntax. The cost is that the macro author must specify the token grammar explicitly, and the compiler’s error messages point at the invocation rather than the definition when the matcher fails.

Key point: A macro_rules! macro is a list of (matcher) => { transcriber } rules; the matcher uses $name:fragment to capture syntax fragments, $(...)* or $(...)+ to repeat patterns, and the transcriber substitutes the captured metavariables. Rules are tried in source order, and the first matching rule wins with no backtracking if its expansion later fails to compile.


Why declarative macros exist

The boilerplate problem. The standard library implements From<i8> for f64, From<i16> for f64, From<i32> for f64, and the same for every numeric combination. Writing these by hand is tedious and error-prone. A macro with a repetition pattern generates all of them from a single list . The same pattern appears in vec!, println!, and assert_eq!—each is a macro because the number of arguments varies and the generated code depends on the argument shapes.

The syntax extension problem. A function cannot introduce new items into a module. A macro can. define_flags!(READ = 1, WRITE = 2, EXECUTE = 4) can generate three pub const declarations with the correct values . This is the capability that makes macros useful for domain-specific languages, test case generation, and builder pattern implementations.

The compile-time validation problem. A macro matcher is a grammar specification. If the caller writes syntax the matcher does not accept, the compiler rejects it before any code is generated. This is how println! enforces that the format string is a literal and the arguments match the placeholders—a level of validation that a function signature cannot express.

The hygiene problem. Macro expansion inserts code into the caller’s scope. Without hygiene, a local variable named x in the macro body could collide with a caller’s x. Rust’s macro_rules! uses mixed-site hygiene: local variables and labels introduced by the expansion are resolved at the definition site, so they cannot be accidentally captured by the caller, while item paths (functions, types, modules) are resolved at the invocation site . This is why a macro that generates a local let temp = ... can be invoked inside a function that already has a temp variable without conflict, but a macro that calls helper() without a path may resolve to the caller’s helper instead of the intended one.


a. The basic syntax: matchers and transcribers

A macro_rules! definition has a name (written without ! in the definition, with ! at the call site), a body of rules, and each rule has a matcher in parentheses and a transcriber in braces.

// Define a macro named make_getter
// The name is without ! in the definition
macro_rules! make_getter {
    // The matcher captures an identifier and a type
    // $field:ident is the field name
    // $ty:ty is the field type
    ($field:ident, $ty:ty) => {
        // The transcriber generates a method
        // $field and $ty are substituted with the captured values
        fn $field(&self) -> &$ty {
            // The method returns a reference to the field
            &self.$field
        }
    };
}

struct User {
    name: String,
}

impl User {
    // Invoke the macro inside the impl block
    // It generates: fn name(&self) -> &String { &self.name }
    make_getter!(name, String);
}

fn main() {
    let user = User {
        name: String::from("Ferris"),
    };
    // The generated method is callable like any other method
    assert_eq!(user.name(), "Ferris");
}

The $field:ident and $ty:ty are metavariables with fragment specifiers. The fragment specifier tells the matcher what kind of Rust syntax the metavariable can capture. ident captures an identifier, ty captures a type, expr captures an expression, and tt captures a single token tree . The choice of fragment specifier controls both what the caller may write and what tokens may legally follow the capture in the matcher .

The matcher delimiter is independent of the call-site delimiter. A matcher written with parentheses can be invoked with {}, [], or (). This is why vec![1, 2, 3] and vec!(1, 2, 3) both work even though the macro is defined with parentheses .

Rules are tried in source order. The first rule whose matcher succeeds is used, and if its expansion produces invalid code, the compiler does not backtrack to try a later rule . This means specific rules must come before general ones. A rule for ($name:ident) must come before a catch-all ($($tokens:tt)*) if the specific rule should take precedence.


b. Fragment specifiers: what each metavariable can capture

The fragment specifier is the parser category for the metavariable. It determines what the caller can put in that position and what punctuation can follow it in the matcher.

SpecifierCapturesExample
exprAn expression$x:expr matches 1 + 2
identAn identifier or keyword$name:ident matches foo
tyA type$t:ty matches Vec<String>
pathA type path$p:path matches std::vec::Vec
patA pattern$p:pat matches Some(x)
stmtA statement without the trailing semicolon$s:stmt matches let x = 1
blockA block expression$b:block matches { ... }
itemAn item$i:item matches fn foo() {}
literalA literal expression$l:literal matches 42 or "hello"
metaAn attribute body$m:meta matches derive(Debug)
visA visibility qualifier (possibly empty)$v:vis matches pub or nothing
ttA single token tree$t:tt matches any single token or delimited group

The follow-set rules are the constraint that determines what can come after a fragment in the matcher. expr and stmt may only be followed by =>, ,, or ;. ty and path have a longer list but still exclude many tokens. A matcher like $x:expr $y:expr is rejected because no token is allowed between the two expressions . The separator must be a literal token like , or ;.

The tt specifier is the escape hatch. It captures a single token tree—either one token or a delimited group—and has no follow-set restrictions. It is used for forwarding to another macro or for writing a catch-all rule, but it gives up the parser guidance and diagnostics of a more specific fragment .

Edition 2024 changed the expr fragment. In a macro defined in the 2024 edition, expr also matches top-level _ and const { ... } expressions. The expr_2021 specifier preserves the pre-2024 behavior for compatibility . The edition that matters is the edition of the crate where the macro is defined, not the edition of the crate that invokes it.


c. Repetition: generating code for each element

The repetition syntax is $( ... ) followed by an operator: * for zero or more, + for one or more, or ? for zero or one. A separator token can appear between the closing parenthesis and the operator, and that separator is inserted between each repetition in the transcriber .

// A macro that creates a Vec from a list of expressions
macro_rules! vec_of {
    // The matcher captures zero or more comma-separated expressions
    // $(,)? allows an optional trailing comma
    ( $( $x:expr ),* $(,)? ) => {{
        // The transcriber repeats the push for each captured expression
        let mut v = Vec::new();
        // $( v.push($x); )* expands once for each $x
        $( v.push($x); )*
        v
    }};
}

fn main() {
    // Invoke with three expressions and a trailing comma
    // The trailing comma is matched by $(,)?
    let v: Vec<i32> = vec_of!(1, 2, 3,);
    assert_eq!(v, vec![1, 2, 3]);

    // Invoke with no expressions
    // The * allows zero repetitions
    let empty: Vec<i32> = vec_of!();
    assert!(empty.is_empty());
}

The transcriber must mirror the repetition shape of the matcher. If the matcher uses $( $x:expr ),*, the transcriber must use $( ... )* with the same separator if it wants to repeat the same number of times. A metavariable that was captured inside a repetition must be used inside a repetition in the transcriber, or the compiler reports “attempted to repeat an expression containing no syntax variables matched as repeating at this depth” .

The $(,)? pattern is the standard way to allow an optional trailing comma. The ? operator means zero or one repetition, and the comma inside the parentheses is the separator. This pattern appears in vec!, println!, and most list-like macros because users expect to be able to write a trailing comma after the last element .

Nested repetitions are allowed. A matcher like $( $( $x:expr ),* );* captures semicolon-separated groups of comma-separated expressions. The transcriber repeats the outer group and, inside each, repeats the inner group. The nesting depth must match between matcher and transcriber, and each level can have its own separator .


Complete Example Session

The following session builds a macro that defines a struct with named fields and generates a constructor, using repetition, fragment specifiers, and a trailing comma.

// ============================================
// PART 1: THE MACRO DEFINITION
// ============================================
macro_rules! make_struct {
    // The matcher captures:
    // - $name:ident for the struct name
    // - $( $field:ident : $ty:ty ),* for the field list
    // - $(,)? for an optional trailing comma
    (
        $name:ident {
            $( $field:ident : $ty:ty ),* $(,)?
        }
    ) => {
        // Generate the struct declaration
        #[derive(Debug, Default)]
        struct $name {
            // Repeat the field declaration for each captured field
            $( $field: $ty, )*
        }

        impl $name {
            // Generate a constructor that takes each field as an argument
            // The parameters repeat for each field
            fn new( $( $field: $ty ),* ) -> Self {
                // Construct the struct with the given values
                Self {
                    // Repeat the field initialization for each field
                    $( $field, )*
                }
            }
        }
    };
}

// ============================================
// PART 2: INVOKING THE MACRO
// ============================================
// The macro generates the struct and the constructor
// The trailing comma after `bool` is allowed by $(,)?
make_struct!(Config {
    port: u16,
    timeout: u32,
    debug: bool,
});

// ============================================
// PART 3: USING THE GENERATED CODE
// ============================================
fn main() {
    // The constructor is generated by the macro
    let config = Config::new(8080, 30, true);
    // The struct derives Debug, so it can be printed
    println!("{:?}", config);
    // Output: Config { port: 8080, timeout: 30, debug: true }

    // The Default trait is also derived
    let default_config = Config::default();
    assert_eq!(default_config.port, 0);
}

The three parts cover the macro definition with repetition and a trailing comma, the invocation that generates a Config struct with three fields, and the usage that calls the generated constructor and the derived Default implementation. What the session demonstrates is that the repetition in the matcher captures the field list, and the repetition in the transcriber generates the struct fields, the constructor parameters, and the field initializers—all from the same captured list.


Quick Reference

Macro Definition Structure

ComponentSyntaxPurpose
Namemacro_rules! nameDefine a macro (no ! in the name)
Rule(matcher) => { transcriber };One accepted syntax shape
Metavariable$name:fragmentCapture a syntax fragment
Repetition$( ... )*Zero or more
Repetition$( ... )+One or more
Repetition$( ... )?Zero or one
Separator$( ... ),*Comma between repetitions
Optional trailing$(,)?Allow a trailing comma

Common Fragment Specifiers

SpecifierCapturesFollow-set allowed after
exprExpression=>, ,, ;
identIdentifierAny
tyType=>, ,, =, |, ;, :, >, >>, [, {, as, where
pathType pathSame as ty
patPattern=>, ,, =, |, if, in
stmtStatement=>, ,, ;
ttToken treeAny
literalLiteralAny
visVisibility,, identifier, type-start token

Rules

RuleConsequence
First matching rule winsOrder specific before general
No backtracking on invalid expansionA matched rule that produces bad code fails the macro
Matcher delimiter is flexible(), [], {} all match the same matcher at the outer level
Transcriber repetition must mirror matcherSame shape, same separator

Best Practices

✅ Do This:

// Put specific rules before general ones
macro_rules! m {
    ($x:expr) => { ... }; // specific
    ($($t:tt)*) => { ... }; // general
}

// Allow trailing commas with $(,)?
($( $x:expr ),* $(,)?) => { ... }

// Use absolute paths in exported macros
::std::collections::HashMap::new()

// Use $crate for helpers in exported macros
$crate::helper!()

❌ Don’t Do This:

// Missing fragment specifier
($name) => { ... } // error: missing fragment specifier

// Illegal follow token
($x:expr $y:expr) => { ... } // error: no token may follow expr

// Forget to repeat a metavariable in the transcriber
($($x:expr),*) => { println!("{}", $x); } // error: not repeating

// Use bare crate in an exported macro
crate::helper() // resolves to caller's crate, not definition

Common Pitfalls

PitfallWhy It HappensFix
“Missing fragment specifier”Wrote $name without :fragmentAdd :ident, :expr, etc.
“No rules expected this token”Invocation does not match any matcherCheck the token grammar; add a catch-all arm
“Attempted to repeat an expression containing no syntax variables”Metavariable not inside repetition in transcriberWrap it in $( ... )*
Trailing comma rejectedNo $(,)? in matcherAdd $(,)? at the end
Exported macro cannot find helperUsed bare name, not $crate::Use $crate::helper!()

Real-World Examples

1. Simple getter generation

macro_rules! getter {
    ($field:ident) => {
        fn $field(&self) -> &str { &self.$field }
    };
}

2. Vec-like macro

macro_rules! my_vec {
    ($($x:expr),* $(,)?) => {{
        let mut v = Vec::new();
        $( v.push($x); )*
        v
    }};
}

3. HashMap literal

macro_rules! hashmap {
    ($($k:expr => $v:expr),* $(,)?) => {{
        let mut m = ::std::collections::HashMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}

4. Test case generation

macro_rules! test_cases {
    ($($name:ident: $input:expr => $expected:expr),* $(,)?) => {
        $( #[test] fn $name() { assert_eq!(parse($input), $expected); } )*
    };
}

5. Flag constants

macro_rules! flags {
    ($($name:ident = $value:expr),* $(,)?) => {
        $( pub const $name: u32 = 1 << $value; )*
    };
}

6. Struct with constructor

macro_rules! make {
    ($name:ident { $($f:ident: $t:ty),* $(,)? }) => {
        struct $name { $($f: $t,)* }
        impl $name { fn new($($f: $t),*) -> Self { Self { $($f,)* } } }
    };
}

7. Trailing comma

vec![1, 2, 3,] // works because of $(,)?

8. Catch-all for debugging

macro_rules! debug_tokens {
    ($($t:tt)*) => { compile_error!(stringify!($($t)*)); };
}

9. $crate for exported helpers

#[macro_export]
macro_rules! log {
    ($($t:tt)*) => { $crate::logger::log(format!($($t)*)); };
}

10. Nested repetition

macro_rules! matrix {
    ($($($x:expr),*);*) => { vec![$(vec![$($x),*]),*] };
}

Visual

Matcher and Transcriber

┌──────────────────────────────────────────────┐
│  macro_rules! name {                         │
│    (matcher) => { transcriber };             │
│  }                                           │
│                                              │
│  MATCHER                                     │
│  $name:ident  → captures an identifier       │
│  $( $x:expr ),* → captures a comma list      │
│                                              │
│  TRANSCRIBER                                 │
│  $name → substitutes the captured identifier │
│  $( $x ),* → repeats for each expression     │
└──────────────────────────────────────────────┘

Repetition

┌──────────────────────────────────────────────┐
│  MATCHER: $( $x:expr ),*                     │
│  → matches: a, b, c                          │
│  → captures: $x = [a, b, c]                  │
│                                              │
│  TRANSCRIBER: $( v.push($x); )*              │
│  → expands to:                               │
│    v.push(a);                                │
│    v.push(b);                                │
│    v.push(c);                                │
└──────────────────────────────────────────────┘

Follow-Set Restriction

┌──────────────────────────────────────────────┐
│  $x:expr $y:expr                             │
│  → error: no token may follow expr           │
│                                              │
│  $x:expr, $y:expr                            │
│  → OK: comma is a legal follow token         │
│                                              │
│  Legal after expr: =>, ,, ;                  │
└──────────────────────────────────────────────┘

Hygiene

┌──────────────────────────────────────────────┐
│  MACRO BODY: let x = 1;                      │
│  CALLER: let x = 2;                          │
│         │                                    │
│         ▼                                    │
│  Mixed-site hygiene:                         │
│  - local `x` in macro resolves at definition │
│  - caller's `x` is not affected              │
│  - no collision                              │
│                                              │
│  ITEM PATHS: helper()                        │
│  - resolves at invocation site               │
│  - use $crate::helper() to avoid collisions  │
└──────────────────────────────────────────────┘

Summary

ItemValue
Definitionmacro_rules! name { (matcher) => { transcriber }; }
Invocationname!(...)
Metavariable$name:fragment
Repetition$( ... )*, $( ... )+, $( ... )?
Separator$( ... ),*
Trailing comma$(,)?
SpecificityFirst matching rule wins
BacktrackingNone on expansion failure
HygieneMixed-site (locals definition, items invocation)
Exported helpers$crate::helper!()

Key takeaways:

  • A macro is a list of rules. Each rule has a matcher and a transcriber. The first rule whose matcher succeeds produces the output, and if that output fails to compile, the compiler does not try later rules .
  • Fragment specifiers control what a metavariable can capture. expr captures expressions, ident captures identifiers, ty captures types, and tt captures any token tree. The choice determines the caller’s allowed syntax and the follow-set restrictions .
  • Repetition generates code for each element. $( ... )* matches zero or more, $( ... )+ matches one or more, and the transcriber must mirror the repetition shape. The $(,)? pattern allows a trailing comma .
  • Follow-set rules constrain matchers. expr and stmt can only be followed by =>, ,, or ;. A matcher that puts another expression after $x:expr is rejected. Use a literal separator between fragments .
  • Hygiene protects locals but not items. Local variables introduced by the expansion are resolved at the definition site, so they cannot collide with the caller’s variables. Item paths are resolved at the invocation site, so exported macros should use $crate:: for their own helpers .
  • Specific rules must come before general ones. A catch-all ($($t:tt)*) rule placed first would match everything, and the more specific rules below it would never be tried.
  • The tt specifier is the escape hatch. It captures a single token tree with no follow-set restrictions, which makes it useful for forwarding to another macro or writing a catch-all. It gives up the parser guidance of a more specific fragment .

Remember: macro_rules! is a pattern-matching system for token syntax. The matcher is a grammar example, the transcriber is a template, and the metavariables are the slots that connect them. The fragment specifiers tell the parser what kind of syntax goes in each slot. The repetition operators generate code for lists of any length. The follow-set rules keep the grammar unambiguous. And hygiene keeps the generated locals from colliding with the caller’s names. The macro is not a function—it runs before type checking, generates items and syntax, and is the reason vec!, println!, and assert_eq! can accept a variable number of arguments of varying types. Writing a macro means writing a small grammar, and the compiler enforces that grammar at every call site.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!