๐ŸŽ“ A1Learner
๐Ÿ”

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?