Declarative Macros: macro_rules!

28 min read
On this page

Declarative macros are well-suited for turning repetitive syntactic structures into a set of patterns, such as generating multiple implementations, accepting a variable number of expressions, or providing concise domain-specific syntax. If you are simply performing algorithms on values with already determined types, regular functions and generics are usually easier to understand, debug, and inspect.

Macros expand at compile time, but the generated code may still allocate, loop, and produce side effects at runtime; const fn can also be evaluated at compile time, so you cannot distinguish macros from functions solely by "compile-time vs. runtime."

Token trees in the callToken groups and parsed fragments, not text replacement1Match rules in orderSelect the first matching rule; repeat and substitute2Expand to the call siteForms valid syntax such as expressions, items, and patterns3Name, type, and borrow checking4Declarative Macros: Match syntax first, then check the expanded codeCode generated by macros must still comply with standard Rust rulesMacro expansion does not exempt ownership rules, nor does it automatically guarantee that arguments are evaluated only once.

A small macro to illustrate evaluation count

macro_rules! list {
    () => { ::std::vec::Vec::new() };
    ($($value:expr),+ $(,)?) => {{
        let mut output = ::std::vec::Vec::new();
        $(output.push($value);)*
        output
    }};
}

fn main() {
    let mut n = 0;
    let values = list![{ n += 1; n }, { n += 1; n },];
    assert_eq!(values, [1, 2]);
    assert_eq!(n, 2);
    let empty: Vec<u8> = list![];
    assert!(empty.is_empty());
}

The repetition ensures that each expression appears once in the generated push calls, so each item is evaluated once and executed in the order written. If $value were written in two places, it could duplicate side effects or trigger use-after-move errors; the macro system will not cache results for you.

The inner block within the double curly braces makes the entire expansion a single expression and confines temporary variables to that block. Here, the ::std path is used to explicitly indicate that the example depends on std; macros exported from no_std libraries need to handle alloc paths appropriately and cannot be copied directly.

One expansion can evaluate an expression twice

A side-effecting argument exposes the distinction between expansion and execution. next() is an ordinary runtime operation; the macro determines how many times it appears in generated syntax. Binding controls evaluation count, while later reuse still follows Copy, borrowing and move rules.

Preparing the visual
One expansion can evaluate an expression twice

Repeating $e repeats evaluation; binding let value=$e first evaluates it once.

Fragments are syntactic categories, not Rust types

fragmentMatchesCommonly confused boundaries
exprExpressionsDoes not reveal the inferred type (e.g., whether it is u32 or String)
tyType syntaxDoes not automatically expand aliases or check trait implementations
identIdentifier shapeNot an arbitrary multi-segment path
pathPathsCan be used for qualified names and other corresponding syntax
pat / pat_paramPatternsTop-level or pattern rules depend on the edition
ttA single token treeA single token or a tree wrapped in balanced delimiters, not arbitrary-length input
itemItemsFunctions, structs, impls, etc.
meta / visAttribute content / Visibilityvis can match empty visibility

To match an arbitrary number of token trees, write $($tokens:tt)*. When forwarding already-parsed fragments like expr to another macro, they are typically opaque fragments of their respective categories and cannot be split and matched by arbitrary literal tokens; ident, lifetime, and tt have specific exceptions. Macro Matching Rules

What the next layer sees differs when forwarding expr vs. tt

macro_rules! inspect {
    (3) => { "literal token" };
    ($value:expr) => { "expression fragment" };
}
macro_rules! pass_expr { ($value:expr) => { inspect!($value) }; }
macro_rules! pass_tt { ($value:tt) => { inspect!($value) }; }
fn main() {
    assert_eq!(inspect!(3), "literal token");
    assert_eq!(pass_tt!(3), "literal token");
    assert_eq!(pass_expr!(3), "expression fragment");
}

pass_expr has already parsed 3 as an expression fragment, so the subsequent inspect cannot match this opaque fragment against the literal 3, thus it falls through to the expr rule. tt preserves the matchable token tree shape. If you need to perform token-level syntax analysis next, keep the corresponding tokens in the interface; if you are just generating code that uses expressions, expr's syntactic constraints are clearer.

Note that pass_tt!(1 + 2) is not "an expression counts as one token tree"; it contains multiple ungrouped tokens and cannot match a single $value:tt; you can group them explicitly or accept repeated tokens as needed. Choosing a fragment affects what you can do next; it is not just a placeholder chosen arbitrarily to swallow input.

Repetition, rule order, and ambiguity

$()* is zero or more times, $()+ is at least once, $()? is optionally once; comma separation and optional trailing commas are usually combined as $($x:expr),* $(,)?. The nested structure of repeated output must match the capture; when multiple variables are repeated simultaneously, their counts must also correspond.

Rules are attempted in definition order. If the expansion fails after the first match, it does not automatically backtrack to try another rule. Place more specific rules before broader rules that could swallow them.

Matching is not an arbitrary backtracking general-purpose parser. For example, repeating ident followed immediately by another ident may make it impossible to determine which part the current token belongs to at that point, resulting in local ambiguity. Using delimiters, explicit keywords, or grouping to clarify boundaries is more robust than constantly adding recursion.

Capture paired data within the same repetition layer

macro_rules! pairs {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut output = ::std::vec::Vec::new();
        $(output.push(($key, $value));)*
        output
    }};
}
fn main() {
    let mut calls = 0;
    let entries = pairs!(
        "first" => { calls += 1; calls },
        "second" => { calls += 1; calls },
    );
    assert_eq!(entries, [("first", 1), ("second", 2)]);
    assert_eq!(calls, 2);
}

Each repetition unit captures both key and value, so a pair of data naturally corresponds. If designed as two independent repetitions, and then placed into the same repetition layer in the output, you must ensure the capture counts of both groups are consistent; macro expansion will not fill in missing items for the caller or automatically perform a Cartesian product.

This pairs only generates a Vec; it does not check for duplicate keys, nor does it create a Map. Syntactic sugar should accurately name the capabilities it provides; if requirements include duplicate key strategies, type conversion, or resource limits, these still need to be handled by generating corresponding code or delegating to regular functions.

Delimiters are also boundaries for long-term syntactic compatibility

The tokens allowed after a macro fragment are constrained by follow-set rules. For example, expr followed by =>, ,, or ; are common valid boundaries; you cannot arbitrarily invent a sequence of symbols following an expression and expect the parser to always distinguish them clearly. Explicit grouping and keywords also keep error locations closer to user input, rather than reporting no match only after multiple layers of recursion.

A clearly bounded example of recursive token processing

The following writes text expressions in order into a String, using semicolons to delimit instructions. It is a small TT muncher and does not pretend to be a complete HTML/XML parser.

macro_rules! text_script {
    (@step $out:ident;) => {};
    (@step $out:ident; text $value:expr; $($rest:tt)*) => {
        {
            use ::std::fmt::Write as _;
            ::std::write!(&mut $out, "{}", $value).expect("write String");
        }
        text_script!(@step $out; $($rest)*);
    };
    (@step $out:ident; $($invalid:tt)+) => {
        compile_error!("expected: text <expression>;");
    };
    () => { ::std::string::String::new() };
    ($($input:tt)+) => {{
        let mut output = ::std::string::String::new();
        text_script!(@step output; $($input)*);
        output
    }};
}

fn main() {
    assert_eq!(text_script!(text "value="; text 42;), "value=42");
    assert_eq!(text_script!(), "");
}

Each step consumes one text instruction and forwards the remaining tokens; a termination rule must exist. The specific @step error rule is placed before the broad entry point to prevent illegal input from repeatedly entering the entry point and only reporting a recursion depth error. Repeatedly matching the entire tail for long inputs may also lead to high, even quadratic, macro expansion costs.

<$tag:ident> ... </$tag:ident> is not a valid "match same-name tag" trick: binding the same variable name in the same matcher repeatedly will cause an error, and unlimited children repetitions encountering closing tags easily lead to ambiguity. Complex nested syntax can be changed to explicit grouping, or use procedural macros for true parsing.

Hygiene and cross-crate references

macro_rules has mixed-position hygiene: local variables created inside the macro do not simply conflict with caller variables via text substitution, but name resolution for item paths still needs to be understood according to rules. You cannot just say it is "fully hygienic, so all name issues disappear."

When exporting macros that reference helper items from the defining crate, use $crate::helper instead of assuming the caller names the dependency a specific string. $crate does not bypass visibility rules; helper items that need to be accessed by external expansions must still have appropriate visibility.

Standard macro_rules does not provide the ability to generate arbitrary stable string concatenations to form identifiers. Accepting an ident provided by the caller, referencing a fixed name, and splicing a new name from two tokens are different requirements; the latter may require procedural macros or specific support.

value in macro definitionLocal binding inside the macro$input passed by callerStill resolved in the caller's contextGenerated resultSame name, distinct bindingsLocal temporary names and caller input, preserving different name sourcesHygiene handles names, not repeated evaluation or business side effectsGenerated code determines evaluation count and timingmacro_rules has mixed-position hygiene; item paths and cross-crate helper functions still need explicit references.

Same-named local variables do not simply replace via text

macro_rules! offset {
    ($input:expr) => {{
        let value = 10;
        $input + value
    }};
}
fn main() {
    let value = 7;
    assert_eq!(offset!(value), 17);
    assert_eq!(value, 7);
}

The passed value still points to the 7 at the call site, while the temporary value introduced by the macro is 10. This does not mean the macro can read any same-named variable from the caller; if you want to generate a binding that the caller uses later, you should accept the corresponding ident and explicitly define the expansion position and scope.

Helper items in exported macros must be located from the defining crate

#[doc(hidden)]
pub fn __decorate(text: &str) -> String { format!("[{text}]") }
#[macro_export]
macro_rules! decorated {
    ($text:expr) => { $crate::__decorate($text) };
}
fn main() {
    assert_eq!(decorated!("ready"), "[ready]");
}

$crate points to the crate defining the macro, allowing location even if the user renames the dependency; it does not turn private functions into public ones. Here, the helper function must be publicly visible; doc(hidden) only hides regular display in generated documentation, not access permissions. Public macros generating code also have compatibility responsibilities for the helper interfaces they require and cannot be removed arbitrarily.

Edition and checking strategies

The edition where the macro is defined affects the matching scope of fragments like expr and pat. Rust 2024's expr expands the shapes of expressions that can be matched, while expr_2021 retains the corresponding old rules; when upgrading the edition, check whether the selection of previous branches has changed.

When verifying macros, in addition to successful examples, you should also cover empty input, trailing delimiters, parameters with side effects, invalid syntax, and usage across modules/crates. The expansion output is suitable for understanding structure, but the final behavior depends on the compilation of the calling code.

2024's expr changes rule selection

The following complete example compiles with edition 2024:

macro_rules! broad_first {
    ($value:expr) => { "expression" };
    (const $value:expr) => { "special const" };
}
macro_rules! legacy_first {
    ($value:expr_2021) => { "expression" };
    (const $value:expr) => { "special const" };
}
fn main() {
    assert_eq!(broad_first!(const { 1 }), "expression");
    assert_eq!(legacy_first!(const { 1 }), "special const");
}

2024's expr can accept top-level const blocks, so the first rule of broad_first already matches; expr_2021 retains the old fragment scope, allowing the second const rule to still have a chance to be selected. During migration, you should decide whether to retain old behavior or adjust rule order and accept new syntax, rather than just looking at whether it still compiles. The edition that determines the matching scope is the macro definition's edition, not the macro caller's edition. 2024 Fragment Migration

Macro failure cases are also part of the interface documentation: record which custom error is expected to be triggered for illegal input, confirm rejection when repetition counts do not match, and test exported macros in external crates. Successful calls in the same source file alone cannot cover dependency renaming, visibility, and path hygiene.