🎓 A1Learner
🔍
0 of 149 lessons completed

Rust Declarative Macros

A declarative macro is a macro defined by a list of patterns. When a call fits a pattern, the macro writes the code that goes with it.

The name has no !

You define greet — the ! appears only when you call the macro:

macro_rules! greet {
    () => { println!("hi") };
}
// call it as: greet!()

One arm: matcher => transcriber

The matcher (left) describes the shape of the call. The transcriber (right) is the code that replaces it. $x holds the captured code itself, not a run-time value:

macro_rules! double {
    ($x:expr) => { $x + $x };
}
println!("double = {}", double!(21));

Captures: a name, a word, an expression

A matcher may hold plain words that must appear exactly. Here is sits between two captures. One is an ident capture, which can stand where a name is born. The other is an expr capture:

macro_rules! let_be {
    ($n:ident is $v:expr) => { let $n = $v; };
}
let_be!(score is 9);
println!("score = {score}");

Repeat with a separator

This is the pattern the standard vec! is built with. $( $x:expr ),* reads a comma-separated list. The same $( ... )* shape in the transcriber writes one copy per piece.

macro_rules! total {
    ($($x:expr),*) => {{ let mut s = 0; $(s += $x;)* s }};
}
println!("total = {}", total!(1, 2, 3));

Arms are tried in order

Like match arms, the first fitting arm wins — so the literal arm must come before the general one:

macro_rules! classify {
    (stop) => { "halt" };
    ($x:expr) => { "go" };
}
println!("{} / {}", classify!(stop), classify!(42));

A call that fits no arm

double! takes one expression. A two-value call fits no arm, and the compiler says so at the first unexpected token:

double!(1, 2);
// error: no rules expected `,`

Example

macro_rules! make_list {
    ($($x:expr),*) => {
        {
            let mut v = Vec::new();
            $(v.push($x);)*
            v
        }
    };
}
macro_rules! shout {
    ($word:expr) => { println!("{}!", $word.to_uppercase()) };
}
fn main() {
    let nums = make_list!(1, 2, 3);
    println!("nums = {:?}", nums);
    println!("count = {}", make_list!("a", "b").len());
    shout!("hello");
}

Key points

  • More capture kinds exist: stmt, block, item, literal, path, tt (one token tree).
  • The Rust Reference lists them all.
  • expr, ident and ty cover most macros you will write.
  • Use $( ... )* to repeat: * means zero or more, + means one or more, ? means zero or one (with no separator)
  • A declarative macro writes code by matching a call against its patterns.
  • The macro sees the shape of your code before types are checked.
  • That is why make_list! below can hold numbers in one call and words in the next.

In this example, make_list! copies the vec! idea. Its matcher, ($($x:expr),*), reads a comma-separated list of expressions and names each one $x. The transcriber first makes an empty Vec. Then one push is written per captured expression — three pushes for 1, 2, 3. main prints nums = [1, 2, 3], then reuses the same macro with words and prints count = 2. Last, shout! shows a second small macro: it captures one expression and prints it in capitals, so "hello" becomes HELLO!. The Macro Designators lesson lists the full designator catalogue — vis, literal, tt and friends, with stringify!.

Note

No generic function signature could promise both so simply.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does $( $x:expr ),* match?