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

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)

Pro Tips

📋 Quick Reference — Macro Syntax

SyntaxMeaning
$x:exprCapture one expression
$n:identCapture an identifier/name
$( ... ),*Zero or more, comma-separated
$( ... ),+One or more, comma-separated
$(,)?Optional trailing comma

🎉 Lesson Complete!

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

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.