Skip to content

← Macros, FFI and Type-Driven Design step 4 of 28

Medium Primitives

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.