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?