Skip to content

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

Medium Primitives

Preserve expression grouping in a macro

Fix the transcriber of double_tt_fixed! so it doubles the value of the whole input expression. Keep its matcher exactly ($($t:tt)*) and leave double!, double_tt! and compare unchanged.

The supplied compare(a, b) deliberately shows two different results:

Macro call Arithmetic for this input
double!(a + b) (a + b) * 2
double_tt!(a + b) a + (b * 2)

For a = 1 and b = 2, compare returns (6, 5). The first value is the intended doubling operation; the second demonstrates the precedence problem. Your repair belongs in the third macro, double_tt_fixed!.

Required behavior

The wrapper calls the fixed macro with three unparenthesized expressions:

Output field Required value
expr (a + b) * 2, from the unchanged double!
tt a + (b * 2), from the unchanged double_tt!
fixed_sum (a + b) * 2
fixed_diff (a - b) * 2
fixed_mixed (a * b + 1) * 2

Inputs are i64 values for which every intermediate operation in these expressions fits in i64. No wrapping or saturating arithmetic is required. For example, a = 3, b = 4 gives expr = 14, tt = 11, fixed_sum = 14, fixed_diff = -2 and fixed_mixed = 26.

Do not compensate by changing a call site or by special-casing the test values. The macro should group the expression itself, so callers do not need extra parentheses. Keep the token-tree matcher; change how its captured tokens are placed into the output expression.

Why expr preserves grouping

$e:expr captures a parsed expression. Substituting it into $e * 2 preserves that expression as a unit, so an input of a + b behaves as if it were parenthesized before multiplication. Expansion produces an expression, not its numeric answer; the resulting expression evaluates to 6 when a = 1, b = 2.

By contrast, $($t:tt)* captures a sequence of token trees. For a + b, the sequence is a, +, b. Substituting that sequence before * 2 produces a + b * 2. Multiplication takes precedence, so the result is 5 for those inputs.

A single token tree can also be a delimited group, such as (a + b). Thus tt does not inherently lose grouping: the problem is this macro's ungrouped repetition. Add grouping around the complete captured sequence in the transcriber.

Choose a fragment for the syntax you accept

Use expr when you want Rust to parse an expression for you. Use token trees when your macro needs to work with custom syntax or forward tokens without first parsing them as an expression. Many useful macros combine fragment kinds; complex macros do not all require token-tree parsing.

A captured expr is opaque when forwarded to another macro_rules! matcher. That matcher can accept it as an expression, but cannot match the original expression against literal tokens. tt captures can still be matched that way. This complete example demonstrates the difference:

macro_rules! classify {
    (3) => { "literal three" };
    ($other:expr) => { "expression" };
}

macro_rules! forward_expr {
    ($value:expr) => { classify!($value) };
}

macro_rules! forward_tt {
    ($value:tt) => { classify!($value) };
}

fn main() {
    assert_eq!(forward_expr!(3), "expression");
    assert_eq!(forward_tt!(3), "literal three");
}

Fragment choices also constrain what may follow a capture in a matcher. For example, $a:expr + $b:expr is rejected: + is not an allowed follower of an expr capture. A comma-separated interface can use expr captures; an infix interface may need delimited operands or token-tree parsing. These restrictions apply to the matcher, not to writing $e * 2 in the transcriber.

Check behavior as well as expansion text

Both original arithmetic macros compile for the supplied calls. The compiler checks that their expressions are valid Rust, but it cannot infer which arithmetic you intended. Tests that exercise different operators expose the mistake; a zero-only test can miss it.

Expansion viewers are useful, but printed expansion text is not a guaranteed round trip back to the same program. Internal grouping may not be preserved in the text, and different tools can print it differently. Do not assume every viewer prints the two macros identically, or that copying its output preserves all macro behavior. stringify! displays token text too; it does not prove expression grouping or evaluate arithmetic.

Your solution must compile, match the outputs and pass Clippy with warnings treated as errors.