Just added: Algorithms you can run and practice
Updated Jun 22, 2026 Edit on GitHub

Macros & Metaprogramming - Code That Writes Code

You've been using macros since Phase 1, every time you typed println!. That ! has probably sat there as a small unexplained mystery - punctuation you copy without knowing why. This phase clears it up, and the answer turns out to be one of the more elegant ideas in Rust.

Macros intimidate people, with a reputation for arcane wizard stuff. The truth is gentler: a macro is a tool that writes ordinary Rust code for you, before your program is compiled. Once you have that model, the mystery dissolves - and you'll realize you'll spend far more time using macros than ever writing them.

📝 Macro - a piece of code that runs at compile time and expands into ordinary Rust source. By the time the compiler actually compiles your program, every macro call has been replaced by the plain code it generated. A macro is not a function; it's a code generator.

Why the !

Every ! you've typed marks a macro call, not a function call: println!, vec!, panic!, format!, assert!. The bang is Rust's way of saying "this isn't a function - it expands into code before compilation." That distinction is the whole point of this phase.

Why can't these be plain functions? Two reasons, and println! shows both:

fn main() {
    let name = "Ada";
    let count = 3;

    println!("hello");                          // zero extra args
    println!("hello {}", name);                 // one
    println!("{} sent {} messages", name, count); // two
}
$ cargo run
hello
hello Ada
Ada sent 3 messages

What just happened: The same println! accepted zero, one, and two trailing arguments. A normal Rust function has a fixed number of parameters with fixed types - there's no way to write one fn that takes "a format string plus however many values you feel like." A macro can, because instead of a signature it has patterns it matches against whatever you hand it.

The more impressive reason: println! checks your format string at compile time. Forget an argument and the program won't even build:

fn main() {
    println!("{} and {}", "only one");  // string has two {} but one value
}
$ cargo build
error: 2 positional arguments in format string, but there is 1 argument
 --> src/main.rs:2:15

What just happened: The macro read your literal "{} and {}" during compilation, counted two placeholders, saw one value, and refused to build. A regular function receives its arguments at runtime and can't see inside a string literal like that. Because a macro runs at compile time with your source code in hand, it catches the mistake before your program ever runs - the superpower the ! is announcing.

Declarative macros (macro_rules!)

The most common kind of macro you can write yourself is the declarative macro, built with macro_rules!. The mental model: a tiny pattern-matching engine, a lot like match from Phase 9 - except instead of matching on values, it matches on the shape of code you pass it, and instead of returning a value, it produces new code.

A small example: max! takes two expressions and expands into an if that picks the larger:

macro_rules! max {
    ($a:expr, $b:expr) => {
        if $a > $b { $a } else { $b }
    };
}

fn main() {
    let biggest = max!(3 + 1, 10);
    println!("{}", biggest);
}
$ cargo run
10

What just happened: Read the macro as a rule. The left side, ($a:expr, $b:expr), is the matcher - "I expect two things, each an expression; call them $a and $b." :expr is a fragment specifier telling Rust what kind of code to capture. The right side, between => and ;, is the expansion - the code to generate, with $a and $b slotted in. So max!(3 + 1, 10) expanded, at compile time, into if 3 + 1 > 10 { 3 + 1 } else { 10 }, and the compiler compiled that ordinary code.

Now the feature that makes macros genuinely powerful: repetition. A matcher can say "zero or more of these," which is how vec! accepts any number of elements. Let's build our own:

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

fn main() {
    let nums = my_vec![10, 20, 30];
    println!("{:?}", nums);
}
$ cargo run
[10, 20, 30]

What just happened: The matcher $( $x:expr ),* reads as "a comma-separated list of expressions" - $( ... ) wraps the repeating part, , is the separator, * means "zero or more." For my_vec![10, 20, 30], that captured $x three times. In the expansion, $( v.push($x); )* repeats the v.push($x); line once per captured expression, so the macro generated a fresh Vec, pushed 10, 20, and 30, and handed it back - essentially how the real vec! works.

💡 Key point. A macro_rules! matcher captures fragments of code ($x:expr) and the expansion stamps them into a template, optionally repeating with $( ... )*. Because this all happens before compilation, the generated code is just as fast as if you'd written it by hand - zero runtime cost.

When a declarative macro earns its keep

A fair question after writing max!: why not a function? For max!, you absolutely should use a function (or std::cmp::max) - it'd be clearer. Macros earn their keep only when ordinary tools can't do the job. The classic case is generating genuinely repetitive code that functions and generics can't express - most often, implementing the same trait across many types:

trait Describe {
    fn describe(&self) -> String;
}

macro_rules! impl_describe {
    ($($t:ty),*) => {
        $(
            impl Describe for $t {
                fn describe(&self) -> String {
                    format!("a {} with value {}", stringify!($t), self)
                }
            }
        )*
    };
}

impl_describe!(i32, f64, bool);

fn main() {
    println!("{}", 42.describe());
    println!("{}", 3.5.describe());
    println!("{}", true.describe());
}
$ cargo run
a i32 with value 42
a f64 with value 3.5
a bool with value true

What just happened: impl_describe!(i32, f64, bool) matched a comma-separated list of types ($t:ty), and $( ... )* stamped out three separate impl Describe for ... blocks - one per type - at compile time. Writing those by hand would be tedious and easy to get out of sync; the macro keeps them in lockstep. (stringify! is itself a macro that turns the token i32 into the string "i32".) This is the sweet spot: boilerplate that varies only by type, which a plain generic function can't generate because each impl is a separate language construct.

⚠️ Gotcha - macros are harder to read and debug than functions. That max! evaluates $a and $b twice in the expansion, so max!(expensive(), 0) would call expensive() twice - a subtle bug a function would never have. Macro errors point at the expanded code, not your source, which can be baffling. Reach for a macro only when ordinary code genuinely can't do the job; when in doubt, write the function.

Derive macros

Here's the macro feature you'll use constantly - and you've already met it. Remember #[derive(Debug)] from way back? That's a macro - specifically a procedural macro: a code generator the compiler runs on your type to write impl blocks for you.

📝 Declarative vs procedural. A declarative macro (macro_rules!) is pattern-based - you write matchers and templates, as above. A procedural macro is a small Rust program that receives your code as input and computes the output code with ordinary Rust logic. #[derive(...)] is the most common procedural macro.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 1, y: 2 };
    let b = a.clone();              // Clone gave us .clone()
    println!("{:?}", a);           // Debug gave us {:?}
    println!("equal? {}", a == b); // PartialEq gave us ==
}
$ cargo run
Point { x: 1, y: 2 }
equal? true

What just happened: That one line, #[derive(Debug, Clone, PartialEq)], ran three procedural macros at compile time, each generating a full impl block for Point: Debug wrote the code that prints Point { x: 1, y: 2 }, Clone wrote the field-by-field .clone(), and PartialEq wrote the == comparison. You'd have written dozens of lines by hand; derive generated them from the struct definition alone.

Procedural macros, briefly

Custom #[derive(...)] is one of three kinds of procedural macro. You don't need to write any of these to be productive, but you'll meet them constantly in libraries:

  • Custom derive - #[derive(Serialize)] from the serde crate generates JSON (de)serialization code for your struct. This is how serialization in Rust feels effortless.
  • Attribute macros - #[tokio::main] on your main function rewrites it to set up an async runtime, and a web framework's #[rocket::get("/")] turns a plain function into a route handler. They wrap or transform the item they're attached to.
  • Function-like macros - they look like macro_rules! calls (name!(...)) but are backed by a full Rust program; sqlx::query! checks your SQL against a real database at compile time.

All three operate on the token stream - the raw sequence of tokens making up your code - using proc_macro machinery. They're powerful enough to inspect and rewrite arbitrary code, which is why they must live in their own dedicated crate. Writing one is an advanced topic involving crates like syn and quote, a deep dive we won't take here.

💡 You'll use far more macros than you write. The macros powering serde, tokio, clap, and sqlx represent enormous engineering effort, and you get all of it from a single #[derive(...)] or #[tokio::main] line. For the vast majority of Rust you'll write, "knowing macros" means knowing which ones to reach for, not authoring your own. That's a feature, not a gap.

Recap

  1. Every ! (println!, vec!, panic!) marks a macro call. Macros run at compile time and expand into ordinary Rust code before your program is compiled - that's why they can take any number of arguments and check format strings before the program runs.
  2. Declarative macros (macro_rules!) pattern-match on the shape of code: matchers capture fragments like $x:expr or $t:ty, and $( ... )* repeats the expansion once per captured item - exactly how vec! accepts any number of elements.
  3. A declarative macro earns its keep only when functions and generics can't, such as implementing one trait across many types. ⚠️ Macros are harder to read and debug, so prefer a plain function when one will do.
  4. Derive macros like #[derive(Debug, Clone, PartialEq)] are procedural macros that generate whole impl blocks from your type - the macro feature you'll use on nearly every struct.
  5. Procedural macros come in three flavors (custom derive, attribute, function-like), operate on the token stream, live in their own crate, and power libraries like serde and tokio. You'll use them far more than you'll ever write them.

Quick check

One quick pass to lock in the core idea - that a macro is compile-time code generation:

[
  {
    "q": "Why does `println!` have a `!`, and why can't it be a plain function?",
    "choices": [
      "The `!` marks it as a macro that expands at compile time, letting it take any number of arguments and check the format string before the program runs",
      "The `!` means the function can panic, which regular functions are forbidden from doing",
      "The `!` is just Rust's required syntax for any function that prints to the screen",
      "The `!` makes the call faster by skipping argument type checks"
    ],
    "answer": 0,
    "explain": "The `!` marks a macro call. Because a macro expands into code at compile time rather than being called at runtime, it can accept a variable number of arguments and inspect the format-string literal to catch mistakes before the program ever runs - neither of which a fixed-signature function can do."
  },
  {
    "q": "In `macro_rules! my_vec { ( $( $x:expr ),* ) => { ... } }`, what does `$( $x:expr ),*` match?",
    "choices": [
      "A comma-separated list of zero or more expressions, captured as `$x`",
      "Exactly one expression named `$x`",
      "A single string literal split on commas",
      "Two expressions separated by a comma, no more and no fewer"
    ],
    "answer": 0,
    "explain": "The `$( ... ),*` is a repetition: `$( )` wraps the repeating part, `,` is the separator, and `*` means zero or more. So it captures a comma-separated list of expressions, each bound to `$x`, which the expansion then stamps out one at a time."
  },
  {
    "q": "What is `#[derive(Debug, Clone, PartialEq)]` actually doing to your struct?",
    "choices": [
      "Running procedural macros at compile time that generate full `impl` blocks (for `{:?}`, `.clone()`, and `==`) from the struct definition",
      "Importing three traits from the standard library at runtime",
      "Marking the struct as one whose fields can never change",
      "Telling the compiler to skip type-checking those three traits"
    ],
    "answer": 0,
    "explain": "`derive` invokes procedural macros that read your struct and generate the corresponding trait `impl` blocks at compile time - `Debug` produces the `{:?}` formatting code, `Clone` the `.clone()` method, and `PartialEq` the `==` comparison. It's the everyday face of metaprogramming in Rust."
  }
]

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Why does `println!` have a `!`, and why can't it be a plain function?

2. In `macro_rules! my_vec { ( $( $x:expr ),* ) => { ... } }`, what does `$( $x:expr ),*` match?

3. What is `#[derive(Debug, Clone, PartialEq)]` actually doing to your struct?