We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 15 of 28
A mini DSL: compile-time expression evaluation
Build an arithmetic macro with two precedence levels. The starter evaluates
every operator from left to right, so calc!(2 + 3 * 4) produces 20.
Change it to multiply first and produce 14.
Keep the supplied ANSWER constant and results() function. Implement the
parsing in macro_rules!, with an additive tier for + and -, a
multiplicative tier for * and /, and atom rules for integers and parentheses.
| Invocation | Expected value |
|---|---|
calc!(2 + 3 * 4) |
14 |
calc!((2 + 3) * 4) |
20 |
calc!(10 - 3 - 2) |
5 |
calc!(20 / 4 / 5) |
1 |
calc!(2 * (3 + 4) - 5) |
9 |
calc!(((8))) |
8 |
Operators at the same precedence level associate left to right. Division is
integer division: 7 / 2 is 3. The exercise uses nonnegative integer
literals, although subtraction can produce negative results. You do not need
to support variables, function calls, unary operators or other Rust syntax.
Assume intermediate values fit in i64 and divisors are nonzero.
Expansion builds the expression
The macro matches tokens and emits a Rust expression. Rust then evaluates that expression. In this declaration, evaluation must succeed at compile time:
pub const ANSWER: i64 = calc!(2 + 3 * 4);
The same macro can appear inside results(). That use does not itself require
constant evaluation, even if the compiler optimizes the arithmetic away.
Compiling a constant checks that its initializer is permitted in a constant
context; the expected value still needs checking.
Split the grammar into tiers
expr := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
factor := integer_literal | '(' expr ')'
The additive tier collects a whole term before passing it to the multiplicative
tier. For 2 + 3 * 4, those terms are 2 and 3 * 4. The multiplicative tier
splits the latter into factors 3 and 4. Each atom is a literal or a group
that starts this process again.
A parenthesized group such as (2 + 3) is one token tree. Scanning it as a
$head:tt keeps it intact, so the outer tier does not mistake its inner +
for a separator. The supplied atom rule opens the group and recurses:
(@atom ($($inner:tt)+)) => { calc!($($inner)+) };
(@atom $n:literal) => { $n };
Carry the work through recursive calls
Give each tier two token buffers: completed output and the piece currently being collected. An additive call can have this shape:
calc!(@add [completed output] [current term] remaining tokens)
Handle three cases:
- At
+or-, finish the nonempty current term through the multiplicative tier, append that result and the operator to the output, then clear the term. - Otherwise, move one token tree into the current term and recurse.
- With no input left, finish the last term and emit the accumulated expression.
Use the same pattern for * and /, passing factors to the atom rules.
Put operator-specific arms before the generic token-consuming arm. Keep the
public catch-all arm last so it does not intercept internal @add or @mul
calls. This builds on token-tree munchers.
Preserve operator order when appending output. A flat 10 - 3 - 2 expression
evaluates left to right; explicitly generating 10 - (3 - 2) changes the
answer to 9. Parentheses around each generated subexpression make the
intended boundaries clear, but they do not fix an incorrectly grouped parse.
Keep expansion manageable
These recursive rules suit the small inputs here. Long inputs can hit the compiler's expansion recursion limit. Check for a catch-all arm intercepting internal calls before increasing the limit: a larger limit does not fix unbounded recursion. Expression-producing arms should emit expressions without a trailing semicolon inside the output delimiters.
The harness reads the fixed vector from results() and optionally selects
indices. It does not parse arithmetic strings from JSON. The complete vector
should be [14, 20, 5, 1, 9, 7, 21, 8, 14], including ANSWER at the end.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.