When a single pattern isn't enough
You are writing a macro to generate a logging function. You want to pass a format string and any number of arguments. Or you are defining a configuration struct and you want the macro to generate a get method for every field. In Python, you would write a function that loops over a list. In JavaScript, you might use map or a for...of loop.
Rust macros do not execute code. They generate code. You cannot write a for loop inside a macro expansion to iterate over the input arguments, because the macro runs at compile time and the input is a list of tokens, not a runtime collection. You need a way to tell the compiler: "Take this pattern, match it against the input list, and for every match, generate this block of code."
That is what repetition syntax does. It turns your macro from a static template into a dynamic generator that scales with the input.
The repetition syntax
Repetition uses the $( ... ) grouping combined with a quantifier operator. The $ marks the start of a repetition group. The parentheses enclose the pattern you want to repeat. The operator at the end controls how many times the pattern matches and expands.
There are three operators:
*matches zero or more times.+matches one or more times.?matches zero or one time.
The syntax looks like $( pattern )*. If you need a separator between items, such as a comma, you place it inside the parentheses, right before the operator. The separator is part of the repetition structure. The compiler expects the separator between every matched item.
Convention aside: Always put the separator inside the repetition group. Writing $( $x:expr )*, is a syntax error. The comma must be $( $x:expr ),*. This keeps the repetition self-contained and makes the pattern easier to read.
Minimal example
Here is a macro that takes a list of expressions and prints each one.
macro_rules! print_items {
// Match zero or more expressions separated by commas.
// The comma is inside the parentheses, before the *.
($($item:expr),*) => {
// Expand the println block for every matched item.
// The repetition operator * must match the matcher *.
$(
println!("Item: {}", $item);
)*
};
}
fn main() {
// Expands to three println! calls.
print_items!(1, 2, 3);
// Expands to zero println! calls. This is valid because of *.
print_items!();
}
The matcher $($item:expr),* captures each expression into the variable $item. The expander $( ... )* uses $item to generate code. The repetition operator in the expander must match the operator in the matcher. If you match with *, you expand with *. If you match with +, you expand with +. The count must align.
Treat the repetition operator as a contract. If the matcher promises a list, the expander must deliver a list.
How the compiler expands repetitions
When you call print_items!(1, 2, 3), the compiler parses the input tokens. It sees 1, then ,, then 2, then ,, then 3.
The matcher $($item:expr),* steps through the input:
- It matches
1as an expression and captures it into$item. - It sees the comma separator and consumes it.
- It matches
2and captures it into$item. - It sees the comma and consumes it.
- It matches
3and captures it into$item. - There are no more tokens, so the repetition ends.
The variable $item now holds a sequence of three values: 1, 2, and 3. The expander runs the block three times, substituting $item with each value in turn. The result is three println! invocations.
If you call print_items!(), the matcher sees no tokens. The * operator allows zero matches. The sequence is empty. The expander runs zero times. The macro expands to nothing.
If you change the operator to +, the behavior changes. The + operator requires at least one match. Calling print_items!() with + triggers a compile error. The compiler rejects the call because the input does not satisfy the "one or more" requirement.
Realistic example: Multi-assert
Repetitions shine when you need to generate repetitive boilerplate. Here is a macro that asserts multiple conditions and reports which one failed.
macro_rules! assert_all {
// Match one or more expressions separated by commas.
// We use + because asserting zero conditions is useless.
($($cond:expr),+) => {
// Generate an if-check for each condition.
// stringify! captures the source code of the expression.
$(
if !$cond {
panic!("Assertion failed: {}", stringify!($cond));
}
)+
};
}
fn main() {
let x = 10;
let y = 20;
// Expands to two if-blocks.
// If the first fails, it panics with "x > 5".
// If the second fails, it panics with "y < 100".
assert_all!(x > 5, y < 100);
}
This example introduces stringify!. Inside a repetition, stringify!($cond) captures the source text of the matched expression, not the variable name. If you pass x > 5, stringify! produces the string "x > 5". This is a common pattern for debugging macros.
Convention aside: Name your repetition variables clearly. $x is acceptable for tiny macros, but $cond, $field, or $arg makes the intent obvious. When you return to the code six months later, the variable name tells you what the repetition is capturing.
Nested repetitions and multiple captures
Repetitions can capture multiple variables at once. You can also nest repetitions to handle hierarchical data.
Capturing multiple variables is useful for key-value pairs. The pattern $( $k:ident => $v:expr ),* matches a list of key => value pairs. Both $k and $v are captured in the same repetition. They expand together.
macro_rules! make_map {
// Match key-value pairs.
// $k and $v are captured in parallel.
($($k:ident => $v:expr),*) => {
let mut map = std::collections::HashMap::new();
$(
map.insert(stringify!($k), $v);
)*
map
};
}
fn main() {
let m = make_map!(
name => "Rust",
version => 1
);
}
Nested repetitions handle lists of lists. Suppose you want a macro that defines a struct with fields, and each field can have multiple default values. The pattern $( $field:ident => $( $val:expr ),* ),* matches an outer list of fields, where each field has an inner list of values.
macro_rules! define_options {
// Outer repetition matches fields.
// Inner repetition matches values for each field.
($($field:ident => $( $val:expr ),*),*) => {
struct Options {
$(
// Each field gets a Vec of values.
$field: Vec<i32>,
)*
}
impl Default for Options {
fn default() -> Self {
Self {
$(
// Expand the inner repetition to build the Vec.
$field: vec![$($val),*],
)*
}
}
}
};
}
fn main() {
// Usage would look like:
// define_options!(
// retries => 1, 2, 3,
// timeout => 500, 1000
// );
}
Nested repetitions are powerful but hard to read. The indentation and parentheses multiply quickly. Use them sparingly. If your nesting goes deeper than two levels, consider flattening the macro or splitting it into multiple macros.
Nested repetitions are powerful but dangerous. Flatten your macro if the nesting gets deeper than two levels.
Pitfalls and compiler errors
Repetition syntax has strict rules. Violating them produces compiler errors that can be cryptic if you do not know what to look for.
Mismatched repetition operators
The matcher and expander must use the same operator. If you match with * and expand with +, the compiler rejects the macro.
macro_rules! bad_macro {
// Matcher allows zero items.
($($x:expr),*) => {
// Expander requires at least one item.
// This is invalid.
$(
println!("{}", $x);
)+
};
}
The compiler reports an error like repetition operator mismatch. The fix is to align the operators. If the input can be empty, use * in both places. If the input must have items, use + in both places.
Separator placement errors
The separator must be inside the repetition group. Placing it outside causes a syntax error.
macro_rules! bad_sep {
// Comma is outside the parentheses.
// This is invalid syntax.
($($x:expr)*) , => { ... };
}
The compiler reports unexpected token or mismatched closing delimiter. Move the separator inside: $( $x:expr ),*.
Fragment specifier limitations
You cannot repeat certain fragment specifiers in complex ways. The path and ty fragments have restrictions inside repetitions. If you need to match a list of types, you might hit a wall. The workaround is to use tt (token tree) fragments, which are more flexible but require manual parsing.
Trailing separators
The pattern $( $x:expr ),* allows a trailing comma. The input 1, 2, 3, matches successfully. The matcher consumes the comma as a separator and then sees the end of input, which is valid for *. If you want to forbid trailing commas, you cannot do it with a simple repetition. You need a more complex pattern or a recursive macro.
Convention aside: Most Rust code allows trailing commas. Macros should too. Using $( $x:expr ),* supports trailing commas naturally, which aligns with community style. Do not fight the trailing comma.
Decision: choosing repetition operators
Use $(...)* when the input list can be empty and you want the macro to accept zero arguments. Use $(...)+ when you require at least one item and want the compiler to reject empty calls. Use $(...)? when you want to match an optional single token or group, not a list. Use a recursive tt macro when you need to process tokens one by one with state or complex parsing that repetition cannot handle.
Reach for plain references when lifetimes are simple; the unsafe alternative is rarely worth it.