Rust 75 🦀 Declarative Macros (macro_rules!) Syntax
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.
| Specifier | Captures | Example |
|---|---|---|
expr | An expression | $x:expr matches 1 + 2 |
ident | An identifier or keyword | $name:ident matches foo |
ty | A type | $t:ty matches Vec<String> |
path | A type path | $p:path matches std::vec::Vec |
pat | A pattern | $p:pat matches Some(x) |
stmt | A statement without the trailing semicolon | $s:stmt matches let x = 1 |
block | A block expression | $b:block matches { ... } |
item | An item | $i:item matches fn foo() {} |
literal | A literal expression | $l:literal matches 42 or "hello" |
meta | An attribute body | $m:meta matches derive(Debug) |
vis | A visibility qualifier (possibly empty) | $v:vis matches pub or nothing |
tt | A 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
| Component | Syntax | Purpose |
|---|---|---|
| Name | macro_rules! name | Define a macro (no ! in the name) |
| Rule | (matcher) => { transcriber }; | One accepted syntax shape |
| Metavariable | $name:fragment | Capture 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
| Specifier | Captures | Follow-set allowed after |
|---|---|---|
expr | Expression | =>, ,, ; |
ident | Identifier | Any |
ty | Type | =>, ,, =, |, ;, :, >, >>, [, {, as, where |
path | Type path | Same as ty |
pat | Pattern | =>, ,, =, |, if, in |
stmt | Statement | =>, ,, ; |
tt | Token tree | Any |
literal | Literal | Any |
vis | Visibility | ,, identifier, type-start token |
Rules
| Rule | Consequence |
|---|---|
| First matching rule wins | Order specific before general |
| No backtracking on invalid expansion | A 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 matcher | Same 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| “Missing fragment specifier” | Wrote $name without :fragment | Add :ident, :expr, etc. |
| “No rules expected this token” | Invocation does not match any matcher | Check the token grammar; add a catch-all arm |
| “Attempted to repeat an expression containing no syntax variables” | Metavariable not inside repetition in transcriber | Wrap it in $( ... )* |
| Trailing comma rejected | No $(,)? in matcher | Add $(,)? at the end |
| Exported macro cannot find helper | Used 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
| Item | Value |
|---|---|
| Definition | macro_rules! name { (matcher) => { transcriber }; } |
| Invocation | name!(...) |
| Metavariable | $name:fragment |
| Repetition | $( ... )*, $( ... )+, $( ... )? |
| Separator | $( ... ),* |
| Trailing comma | $(,)? |
| Specificity | First matching rule wins |
| Backtracking | None on expansion failure |
| Hygiene | Mixed-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.
exprcaptures expressions,identcaptures identifiers,tycaptures types, andttcaptures 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.
exprandstmtcan only be followed by=>,,, or;. A matcher that puts another expression after$x:expris 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
ttspecifier 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.
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!