Skip to content

← Macros, FFI and Type-Driven Design step 2 of 28

Easy Primitives

Your first macro_rules!: matchers and transcribers

Write a macro with four rules, then let a match pick which one to invoke.

macro_rules! describe { /* ... */ }

pub fn run(n: usize) -> String

run is already written. It calls describe! seven times with seven different argument lists, and your job is to make the macro answer correctly:

invocation answer
describe!() "nothing"
describe!(one) "one thing"
describe!(one, two) "two things"
anything else "something else"

The starter has the first rule and nothing else, so it does not compile.

What a macro rule actually is

A macro_rules! definition is a list of rules, and every rule has two halves separated by =>:

macro_rules! describe {
//  ┌── matcher            ┌── transcriber
    (one, two)          => { "two things" };
}

The matcher is a pattern over tokens. The transcriber is the tokens that get emitted in place of the invocation. describe!(one, two) is not a function call — nothing is passed, nothing returns. The compiler finds the first rule whose matcher accepts the tokens between the delimiters, deletes the whole invocation, and pastes the transcriber’s tokens in its place. Only then does the rest of compilation — name resolution, type checking, borrow checking — begin.

That ordering is the single most useful fact about macros. It explains why a macro can produce a struct (types are not involved yet), why the error messages point at generated code (the code you wrote is gone by then), and why describe!(one, two) can accept one and two even though no such variables exist (they are never resolved — they are just tokens that the matcher compares against literal tokens one and two).

First match wins, top to bottom

Rules are tried in source order, and the first one that matches wins. There is no “most specific rule” heuristic and no scoring. This is the rule that most often bites beginners, because the natural instinct is to write the general case first:

macro_rules! describe {
    ($($anything:tt)*) => { "something else" };   // catches EVERYTHING
    (one) => { "one thing" };                     // dead. never reached.
}

$($anything:tt)* means “zero or more token trees”, which every possible invocation satisfies. Put it first and every later rule becomes unreachable code — and by default the compiler says nothing at all. (unused_macro_rules is an allow-by-default rustc lint; item 18.13 shows how to turn it on when you suspect this.)

The test cases here are chosen so a catch-all-first ordering fails loudly.

::: question describe!(one) and describe!{one} and describe![one] — do all three work, and are they different?

All three work, and the delimiters are interchangeable. The macro’s matcher never sees the outer delimiter at all; it only sees the tokens inside. So describe!(one), describe!{one} and describe![one] are the same invocation written three ways, and one rule matches all three.

There is exactly one difference, and it is about statements, not matching. In statement position, a brace-delimited invocation needs no trailing semicolon:

my_macro! { ... }     // no semicolon needed
my_macro!( ... );     // semicolon needed

That is why vec![...] and println!(...) read the way they do while macro_rules! { ... } and thread_local! { ... } read the way they do. The convention in the community is !() for expression-like macros, ![] for collection-like ones, and !{} for item-like ones — a convention only, which clippy’s allow-by-default nonstandard_macro_braces lint can enforce if you ask it to. :::

Definition order matters

A macro_rules! macro is only in scope after its textual definition. This is a different rule from everything else in Rust, where a function defined at the bottom of a file is callable from the top. Move the macro_rules! block below run here and nothing compiles.

The reason is the same as before: macros are expanded before name resolution runs, in one pass over the token stream. There is no earlier pass that could have collected the definitions. (Item 10.10 covers #[macro_export] and pub(crate) use, which is how macros escape this rule at module boundaries.)

Your job

Add three rules to describe! so that all seven invocations in run answer correctly. Think about the order before you type.