RFA-140 · Case file with fixtures · Case 112 of 694 · Compiler evidence
Why a Forwarded macro_rules expr Cannot Match a Literal Token
A captured expr is forwarded as an opaque AST fragment. The receiving macro can match it with another expr specifier, but not inspect its original literal tokens; ident, lifetime, and tt are the documented token-matchable exceptions.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Forwarded fragments are opaque AST nodes to the receiving macro except for ident, lifetime, and tt fragments, which may still be matched by literal tokens.
- First discriminating check
- Inspect the outer fragment specifier and change it to tt only when the inner macro intentionally needs token-level literal matching.
The failing program has two declarative macros. The outer one captures $value:expr and forwards it to an inner macro. The inner macro has a literal (3) arm. Even when the call is visibly forward_expression!(3), rustc says “no rules expected expr metavariable.”
The number did not change. What changed is the form in which the next macro receives it.
A fragment specifier parses more than tokens
The Rust Reference section on forwarding fragments explains that a matched fragment forwarded to another macro is seen as an opaque AST of that fragment type.
$value:expr tells the outer macro to parse a Rust expression. After that match, the inner macro does not receive a fresh token stream that it can compare with the literal token 3. It receives an expression fragment.
The inner macro can accept the category:
macro_rules! accept_expression {
($value:expr) => { /* use the expression */ };
}
It cannot reopen the expression and branch on its original syntax using a literal matcher.
I use this model:
tt capture -> token-level forwarding
expr capture -> parsed-expression forwarding
This makes the diagnostic less mysterious.
tt is a documented exception
The Reference lists ident, lifetime, and tt as exceptions that may be matched by literal tokens after forwarding. The repaired program captures $value:tt, so the receiving macro can match (3).
This repair is correct because the outer macro expects one token tree and intentionally delegates its interpretation. It would not accept an ungrouped multi-token expression such as 1 + 2 as one tt; callers could wrap it in parentheses, or the macro design could use a different grammar.
Changing every fragment to tt is not a general fix. An expr matcher validates that the input is an expression and handles expression grammar for us. A tt matcher transfers more parsing responsibility into macro arms.
Choose where syntax decisions belong
Often the inner macro should not inspect whether an expression was written as exactly 3. If the desired choice is based on its evaluated value, ordinary Rust control flow is clearer:
let value = expression;
match value {
3 => "three",
_ => "other",
}
Macro literal matching acts at compile-time syntax level. It distinguishes tokens, not values. 1 + 2, { 3 }, and a constant named THREE may evaluate to the same number but have different syntax.
I keep token-level dispatch for code generation where spelling or grammar really matters. I use runtime or const evaluation for semantic values.
Forward the category when the inner macro owns behaviour
Another repair is changing the inner matcher to $value:expr. This works when the inner macro simply needs to generate code using any expression. The outer macro can keep its expression validation, and no token inspection occurs.
If several layers need different syntactic decisions, I redesign the entry grammar so one macro performs the parsing and dispatches an explicit marker to helpers:
outer!(special 3)
outer!(value some_expression)
Named forms produce better diagnostics and avoid relying on a helper to reverse a fragment into its original tokens.
Edition changes can affect expression matching
Macro fragment grammars follow edition rules associated with the macro definition. New expression forms may become accepted by an expr fragment in newer editions. This is another reason I test macros from a downstream fixture using the editions I support.
The token-tree exception remains a deliberate low-level interface. If a public macro forwards tt into internal arms, its accepted grammar and error messages become part of the user experience even when not expressed as Rust types.
The Rust Book macro chapter provides the wider distinction between declarative pattern matching and procedural macro transformation. For this failure, the decisive detail is the declarative fragment boundary.
My debugging sequence
When a forwarded macro value produces “no rules expected” despite visible matching tokens, I do this:
- Reduce the expansion to the outer capture and inner matcher.
- Write down the outer fragment specifier:
expr,ty,path,tt, or another kind. - Check whether the inner macro matches the same category or literal tokens.
- Use
ttonly when token-level forwarding is intentional and the grammar is bounded. - Use an
exprarm when the helper should accept the parsed category. - Move value-based decisions into Rust code rather than syntax matching.
The broader principle is that macro boundaries have interfaces just like functions. A parsed AST fragment is not a bag of tokens anymore. Designing the fragment type deliberately makes nested macros easier to compose and their errors much easier to explain.