Declarative Macros (macro_rules!)
Reviewed & published by Brayan K
A declarative macro is a compile-time pattern matcher, defined with macro_rules!, that takes the tokens you pass it and expands them into real Rust code before the program is compiled.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn macro matcher syntax, fragment specifiers like $x:expr, repetition with $(...)*, hygiene, and you'll build your own vec!-style and hashmap! macros.
What You'll Learn in This Lesson
- How macro_rules! matches patterns of tokens
- Fragment specifiers like $x:expr and $name:ident
- Repetition with $(...),* and $(...),+
- Writing a macro with several rules
- Why macro hygiene keeps names from clashing
- Declarative vs procedural macros
1️⃣ Your First macro_rules!
A macro definition looks like macro_rules! name { ... }. Inside, each rule has the shape (matcher) => { expansion }. The matcher describes the tokens the macro accepts; $x:expr captures an expression and binds it to $x, which you then splice into the expansion. Invoke a macro with a trailing !, as in square!(4).
// A declarative macro defined with macro_rules!.
// It matches the patterns on the left and expands to the code on the right.
macro_rules! say_hello {
// No arguments: () matches an empty invocation.
() => {
println!("Hello from a macro!");
};
}
macro_rules! square {
// $x:expr captures ONE expression and binds it to $x.
($x:expr) => {
$x * $x
};
}
fn main() {
say_hello!(); // expands to the println!
let n = square!(4); // expands to 4 * 4
println!("square = {}", n); // -> 16
// $x:expr captures 2 + 1 as a SINGLE expression unit,
// so the expansion behaves like (2 + 1) * (2 + 1).
let m = square!(2 + 1);
println!("careful = {}", m); // -> 9, not 5
}Notice that square!(2 + 1) gives 9, not 5. A :expr fragment is captured as one complete expression unit, so the expansion behaves like (2 + 1) * (2 + 1) rather than naive text substitution.
2️⃣ Repetition — Building a vec!-like Macro
The real power of macros is repetition. The matcher $( $item:expr ),* means "zero or more expressions separated by commas." In the expansion, wrapping code in $( ... )* repeats it once per captured item. Adding $(,)? at the end allows an optional trailing comma — exactly how the standard vec! works.
// Repetition: $(...),* matches zero or more comma-separated items.
// This is a simplified version of the real vec! macro.
macro_rules! my_vec {
// Match a comma-separated list of expressions (trailing comma optional).
( $( $item:expr ),* $(,)? ) => {
{
#[allow(unused_mut)]
let mut v = Vec::new();
// The $( ... )* on the right repeats once per matched $item.
$(
v.push($item);
)*
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec![];
let nums = my_vec![10, 20, 30];
let trailing = my_vec![1, 2, 3,]; // trailing comma allowed
println!("empty: {:?}", empty);
println!("nums: {:?}", nums);
println!("trailing: {:?}", trailing);
println!("sum = {}", nums.iter().sum::<i32>());
}Use * for "zero or more" and + for "one or more." The separator (here a comma) goes between the closing ) and the repetition operator in the matcher.
3️⃣ Multiple Rules, Recursion & Hygiene
A macro can list several rules; the compiler tries them top to bottom and uses the first that matches. Macros may even call themselves recursively. And thanks to hygiene, any local variable a macro introduces (like a and b below) is kept separate from variables of the same name at the call site — no accidental clashes.
// A macro can have MULTIPLE rules; the first matching pattern wins.
macro_rules! max {
// Base case: a single expression is its own max.
($a:expr) => { $a };
// Recursive case: max of the first item and the max of the rest.
($a:expr, $($rest:expr),+) => {
{
let a = $a;
let b = max!($($rest),+);
if a > b { a } else { b }
}
};
}
fn main() {
println!("{}", max!(3)); // -> 3
println!("{}", max!(3, 7)); // -> 7
println!("{}", max!(3, 7, 2, 9, 5)); // -> 9
// Hygiene: the macro's own a/b never clash with our a here.
let a = 100;
let biggest = max!(1, 2, 3);
println!("our a is still {}, biggest = {}", a, biggest);
}The single-argument rule is the base case; the multi-argument rule peels off the first value and recurses on $($rest),+. Even though both the macro and main use a variable named a, our value 100 is untouched.
Your turn. Fill in the blank marked ___, then run it.
// 🎯 YOUR TURN — finish the macro so it greets a name, then run it.
macro_rules! greet {
// Capture a single expression as the name to greet.
($name:expr) => {
// Print: Hello, <name>!
println!("Hello, {}!", ___); // 👉 the captured variable
};
}
fn main() {
greet!("Ada");
greet!("Grace");
}
// ✅ Expected output:
// Hello, Ada!
// Hello, Grace!Mini-Challenge: A hashmap! Macro
Combine repetition with a custom separator (=>) to build a macro that constructs a HashMap. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: build a hashmap! macro.
//
// Write a macro_rules! macro named hashmap! that accepts pairs written
// as key => value , separated by commas (allow a trailing comma), and
// expands to a populated std::collections::HashMap.
//
// Hint: the matcher is $( $key:expr => $val:expr ),* $(,)?
// and the body inserts each pair into a fresh map.
//
// Then build a map of two names to ages, collect it into a Vec, sort it,
// and print it.
//
// ✅ Example output:
// [("Alice", 30), ("Bob", 25)]
macro_rules! hashmap {
// your matcher and expansion here
}
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ Forgetting the ! when invoking. ✅ Macros are always called with a trailing bang: my_vec!(1, 2), never my_vec(1, 2).
- ❌ Wrong separator in the repetition. ✅ The separator in $(...),* must match how callers write the list — comma here, but it could be ; or =>.
- ❌ Picking the wrong fragment specifier. ✅ Use :ident for names, :ty for types, :expr for expressions — a mismatch gives a confusing parse error.
- ❌ Defining the macro after you use it. ✅ A macro_rules! macro must be defined (textually) before its first use in the same module.
Pro Tips
- 💡 Prefer functions first. Reach for a macro only when you need variadic input or compile-time code generation.
- 💡 Allow a trailing comma with $(,)? — it makes multi-line invocations much nicer to edit.
- 💡 Use cargo expand (a community tool) to see exactly what your macro expands into while debugging.
📋 Quick Reference — Macro Syntax
| Syntax | Meaning |
|---|---|
| $x:expr | Capture one expression |
| $n:ident | Capture an identifier/name |
| $( ... ),* | Zero or more, comma-separated |
| $( ... ),+ | One or more, comma-separated |
| $(,)? | Optional trailing comma |
🎉 Lesson Complete!
- ✅ macro_rules! matches token patterns and expands to code at compile time
- ✅ Fragment specifiers like $x:expr capture pieces of syntax
- ✅ $(...),* repetition powers variadic macros like vec!
- ✅ Hygiene keeps macro-internal names from clashing with caller names
Practice quiz
Which keyword defines a declarative macro?
- macro
- define!
- macro_rules!
- fn!
Answer: macro_rules!. Declarative macros are defined with macro_rules! name { ... }.
When does a macro do its work?
- At compile time
- At runtime
- During linking
- Never
Answer: At compile time. Macros expand into code at compile time, before type-checking.
What does the fragment specifier :expr capture?
- An identifier
- A type
- A whole block only
- An expression
Answer: An expression. :expr captures one complete expression as a single unit.
Why does square!(2 + 1) give 9 rather than 5?
- Macros add a bonus
- 2 + 1 is captured as one expression, like (2+1)*(2+1)
- It rounds up
- It uses integer overflow
Answer: 2 + 1 is captured as one expression, like (2+1)*(2+1). An :expr fragment is one unit, so the expansion behaves like (2+1)*(2+1).
What does the matcher $( $item:expr ),* mean?
- Zero or more comma-separated expressions
- Exactly one expression
- One or more, semicolon-separated
- A single identifier
Answer: Zero or more comma-separated expressions. $(...),* matches zero or more items separated by commas.
What is the difference between * and + in repetition?
- No difference
- * is one or more, + is zero or more
- * is zero or more, + is one or more
- Both mean exactly one
Answer: * is zero or more, + is one or more. * means zero or more matches; + means one or more.
What does $(,)? at the end of a matcher allow?
- A required comma
- An optional trailing comma
- A semicolon
- Two commas
Answer: An optional trailing comma. $(,)? permits an optional trailing comma, like the real vec! macro.
What does macro hygiene prevent?
- Slow macros
- Compile errors
- Recursion
- Macro-internal names colliding with names at the call site
Answer: Macro-internal names colliding with names at the call site. Hygiene keeps a macro's own variables separate from the caller's.
When a macro has multiple rules, which one is used?
- A random one
- The first rule that matches
- The last rule
- All of them
Answer: The first rule that matches. The compiler tries rules top to bottom and uses the first match.
How are declarative and procedural macros different?
- They are identical
- Procedural are simpler
- Declarative match token patterns; procedural run code on a TokenStream
- Declarative run at runtime
Answer: Declarative match token patterns; procedural run code on a TokenStream. macro_rules! matches patterns; procedural macros transform a TokenStream with code.
Continue this course
- Previous: More Collections: VecDeque, BTreeMap & BinaryHeap
- Next: The Drop Trait & RAII — Run cleanup automatically at scope end — Rust's no-GC resource model
- Quick reference: Rust cheat sheet
Frequently asked questions
What is the difference between a function and a macro in Rust?
A function runs at runtime and takes values of fixed types; a macro runs at compile time and operates on the syntax (tokens) of your code, expanding into more code before the program is even type-checked. Because macros work on tokens, they can do things functions cannot: accept a variable number of arguments (like println! and vec!), take whole expressions or types as input, and generate repetitive code. The trade-off is that macros are harder to write and read, so prefer a function unless you genuinely need compile-time code generation.
What do the fragment specifiers like :expr and :ident mean?
When you write a matcher such as $x:expr, the part after the colon is a fragment specifier that tells the macro what kind of syntax to capture. Common ones are :expr (an expression), :ident (an identifier or name), :ty (a type), :pat (a pattern), :stmt (a statement), :block (a braced block), :literal (a literal value), and :tt (a single token tree, the most flexible). Choosing the right specifier lets the compiler parse your input correctly and gives better error messages.
What does macro hygiene mean?
Hygiene means that identifiers a macro introduces internally — like a temporary variable named a inside the expansion — do not accidentally collide with identifiers of the same name at the call site. In the max! example, the macro uses its own a and b, yet a variable named a in the caller stays untouched. This is a major safety feature compared to C-style text-substitution macros, where such name clashes are a constant source of bugs. Hygiene is not perfect for every category of identifier, but for local variables it just works.
What is the difference between declarative and procedural macros?
Declarative macros, written with macro_rules!, match patterns of tokens and expand to a template of code — they are essentially compile-time pattern matching, and that is what this lesson covers. Procedural macros are more powerful: they are actual Rust functions that receive a TokenStream and return a TokenStream, letting you run arbitrary code to generate output. Procedural macros power things like #[derive(Debug)], custom attribute macros, and function-like macros in crates such as serde. They must live in their own crate and are more advanced, but they start from the same idea of transforming code at compile time.