We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 4 of 28
expr is atomic, tt is not: the precedence trap
Two macros that look identical. One is right, one is silently wrong, and the difference is a number you can print.
macro_rules! double { ($e:expr) => { $e * 2 }; }
macro_rules! double_tt { ($($t:tt)*) => { $($t)* * 2 }; }
pub fn compare(a: i64, b: i64) -> (i64, i64) // (double!(a+b), double_tt!(a+b))
For a = 1, b = 2, compare returns (6, 5). For a = 3, b = 4 it returns
(14, 11).
Read that again. Same input, same arithmetic, same * 2 in the transcriber, and
the answers differ. Nothing in the source hints at it, both versions compile
without a warning, and clippy -D warnings is perfectly happy with both. Only an
assertion catches this class of bug — which is exactly why it is worth
manufacturing one on purpose.
Why 6 and why 5
double!(a + b) expands to 6 because $e:expr` captured `a + b` as **one
parsed expression node**. When the transcriber writes `$e * 2, that node is
spliced back in as a single, indivisible unit. The result behaves exactly as
(a + b) * 2 would — even though no parentheses appear anywhere in the source.
A typed fragment carries invisible grouping.
double_tt!(a + b) expands to 5 because $($t:tt)* captured three separate
token trees — a, +, b — and the transcriber pasted all three back
literally, followed by * 2. The token stream that reaches the parser is:
a + b * 2
and * binds tighter than +, so it is a + (b * 2) = 1 + 4 = 5.
This is the C preprocessor bug, reproduced faithfully inside Rust:
#define DOUBLE(x) x * 2
DOUBLE(1 + 2) /* 1 + 2 * 2 == 5 */
tt forfeits the grouping that a typed fragment carries for free. That is
the sentence to keep.
::: question If expr is so much safer, why does anyone ever use tt?
Because grouping is the only thing expr gives you, and it costs you
everything else.
An expr capture is opaque: once tokens are parsed into an expression node, no
later matcher can look inside it. If your macro needs to inspect the syntax —
count the operators, find the top-level commas, decide whether the first token is
a keyword — you need tt, because tt keeps the tokens as tokens.
Every non-trivial macro_rules! macro is therefore built on tt: the token-tree
munchers of item 18.8, the accumulators of 18.9, the callback bundles of 18.10,
the expression evaluator of 18.15. All of them re-establish structure by hand,
and all of them do it by writing parentheses into the transcriber.
There is also a follow-set reason (18.12): you cannot legally put an operator
after an expr fragment in a matcher, so ($a:expr + $b:expr) is rejected
outright. If your call syntax needs an infix operator, tt is the way.
:::
Your job
double_tt_fixed! starts as a copy of double_tt!. Make it agree with
double! for every input, without changing its matcher — it must stay
($($t:tt)*).
There is exactly one honest fix, and it goes in the transcriber.
One warning. The most common wrong move is to “fix” it at the call site —
writing double_tt_fixed!((a + b)) and declaring victory. That is not a fix;
it is a note in the documentation that every caller must remember forever, and
the first caller who forgets gets a silently wrong number. So the three
invocations that grade this problem live in the harness, not in your file. You
cannot reach them. The transcriber is the only place a fix can go.
Why cargo expand would lie to you here
Tools that print a macro’s expansion render it as text. But expr atomicity is
not text — it is a property of the node in the token stream. An expansion viewer
prints
a + b * 2
for both macros, because that is what the tokens say. The difference lives in
the invisible delimiters the compiler attached to the expr capture, and they do
not survive being printed. If you ever debug a precedence problem in a macro and
the expansion looks correct, this is why.
Item 18.13 covers what does work: stringify! for looking at captured tokens,
and rustc’s meta_variable_misuse lint for catching mistakes at definition time.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.