We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 8 of 28
Macro recursion and incremental TT munchers
A macro_rules! macro can process its input one piece at a time by matching
the first piece and calling itself on the rest. This pattern is often called a
token-tree muncher, or "TT muncher". You will finish two such macros.
pub fn evaluate() -> Vec<i64>
The exercise contract
count_tokens! expands to the number of token trees (defined below) in its
input, as an i64. rpn! takes an arithmetic expression written in
reverse Polish notation (RPN), where each operator comes after its two
operands, and expands to the ordinary Rust expression that computes it:
rpn!(3 4 +) // 3 + 4 = 7
rpn!(2 3 4 * +) // 2 + (3 * 4) = 14
rpn!(2 3 + 4 *) // (2 + 3) * 4 = 20
count_tokens!(a b (c d) 1 + 2) // 6
evaluate() is already written. It returns these nine values, in this order:
| Index | Call | Value |
|---|---|---|
| 0 | rpn!(3 4 +) |
7 |
| 1 | rpn!(2 3 4 * +) |
14 |
| 2 | rpn!(2 3 + 4 *) |
20 |
| 3 | rpn!(10 2 -) |
8 |
| 4 | rpn!(-7) |
-7 |
| 5 | rpn!(1 2 + 3 4 + *) |
21 |
| 6 | count_tokens!() |
0 |
| 7 | count_tokens!(a b (c d) 1 + 2) |
6 |
| 8 | count_tokens!(1 2 3 4 5 6 7 8 9 10) |
10 |
The tests ask for some of these positions by index, or for the whole list, so
leave evaluate() and the order of its calls unchanged. The starter does not
compile; the work is in the two macro definitions.
rpn! supports integer literals and the three binary operators +, - and
*. - means a - b where b is the value on top of the stack, so
rpn!(10 2 -) is 10 - 2. A literal may carry a leading minus sign, as in
rpn!(-7), but only while fewer than two values are on the stack: with two or
more, the - is read as subtraction first (see the last section).
What a token tree is
A token is one indivisible piece of source text: an identifier such as
a, a literal such as 1 or "s", a lifetime such as 'a, or a punctuation
symbol. Multi-character symbols such as :: and => are single tokens.
A token tree (the tt fragment in a macro pattern) is either one token
that is not a bracket, or a whole delimited group: an opening (, [ or {,
everything inside it, and the matching closing bracket. That is why (c d)
counts as one in the example above, and why the six trees there are a,
b, (c d), 1, + and 2. Because a group is always one tree, a muncher
that steps over tts never splits a bracketed group in the middle.
Peeling one token tree at a time
A simple muncher has two rules:
- A rule that matches the empty input and stops.
- A rule that matches one leading token tree, does something with it, and calls the macro again on the remaining trees.
macro_rules! count_tokens {
() => { 0i64 };
($head:tt $($rest:tt)*) => { 1i64 + count_tokens!($($rest)*) };
}
$head:tt binds the first tree, and $($rest:tt)* binds zero or more trees
after it. count_tokens!(a b) therefore expands to
1i64 + count_tokens!(b), then to 1i64 + 1i64 + count_tokens!(), and the
empty rule turns the last call into 0i64. Without that empty rule, the final
call count_tokens!() matches no rule and the compiler reports
"unexpected end of macro invocation".
macro_rules! tries its rules from top to bottom and uses the first one that
matches. In count_tokens! the order of the two rules does not matter,
because the second rule needs at least one tree and so can never match empty
input. Order does matter as soon as one rule is general enough to match what
another rule was meant to handle, which is the bug in the starter's rpn!.
Carrying a stack between calls
Evaluating RPN needs a stack: literals are pushed, and each operator pops two
values and pushes the result. A macro cannot keep a variable between
expansion steps. Each step only rewrites the tokens of one call into new
tokens. So rpn! stores its stack in the arguments of the next call:
rpn!(@run [stack contents] remaining tokens...)
The stack is a comma-separated list of expressions with the top first. Each
internal rule reads the next input token, rewrites the stack and calls rpn!
again:
- a literal: push it,
[$lit $(, $stack)*] +: take the top value$band the one below it$a, and push$a + $b, giving[$a + $b $(, $rest)*](-and*work the same way)- no input left and exactly one value on the stack: that value is the result
The square brackets mark where the stack ends and the remaining input begins.
They are needed because a macro pattern may not follow an expr fragment with
just anything. Only =>, , or ; may come after one. A pattern such as
@run $($stack:expr),* $lit:literal ... is rejected when the macro is
defined, with an error saying that $stack:expr may be followed by
$lit:literal, which is not allowed for expr fragments. Inside [...], the
list of expressions is followed by the closing bracket, which is allowed.
@run is a marker for the internal rules. @ has no special meaning in
macro_rules!; it is used here because no RPN input begins with @, so only
the macro's own recursive calls start with it. Internal rules are covered in
more depth in
item 18.9.
The public entry rule, ($($t:tt)*), accepts any input and starts the
evaluation with an empty stack by calling rpn!(@run [] ...). It also matches
every @run call, because those calls are token trees too. The marker makes
internal calls recognisable, but the entry rule still has to come after
all the @run rules so that those rules get the first chance to match.
A trace by hand
rpn!(2 3 + 4 *):
@run [] 2 3 + 4 *
@run [2] 3 + 4 *
@run [3, 2] + 4 *
@run [2 + 3] 4 * <- '+' popped 3 and 2, pushed 2 + 3
@run [4, 2 + 3] *
@run [(2 + 3) * 4] <- '*' popped 4 and 2 + 3, pushed the product
(2 + 3) * 4 == 20
The parentheses in the last steps are not tokens that the macro writes. They
show grouping that comes from the expr fragment. When a pattern captures
2 + 3 as $a:expr, the compiler treats the captured value as one complete
expression, and substituting it into $a * $b multiplies the whole sum by 4.
If the macro instead pasted the plain tokens 2 + 3 in front of * 4, the
result would read 2 + 3 * 4, and normal precedence would give 14. Item
18.4
looks at this difference in detail.
The macro does not run the arithmetic. It expands to an ordinary expression,
and that expression is evaluated like any other Rust code. Because the
expression uses only literals, it is also accepted where a constant is
required, for example const C: i64 = rpn!(2 3 + 4 *);.
Limits and malformed input
Each step of a muncher matches the remaining input again, so the total matching work grows roughly with the square of the input length. For short inputs such as these it is not noticeable.
There is also a limit on how deeply macro calls may nest. The default
recursion_limit is 128. With Rust 1.98.1, count_tokens! handles 127
token trees and fails on 128, and this rpn! handles inputs of up to about
125 tokens, because its entry rule uses one extra level. The error reads
"recursion limit reached while expanding". The limit can be raised with a
crate-level inner attribute, which goes at the top of the crate root before
any item:
#![recursion_limit = "512"]
Every input in this problem has fewer than 20 tokens, so you will not need it.
The same error also appears for input that rpn! cannot evaluate, such as
rpn!(1 +), rpn!(1 2) or rpn!(). When no @run rule matches, the
internal call falls through to the entry rule, which wraps it in another
@run [] call that also fails to match, and so on until the limit is
reached. The negative-literal restriction above causes the same failure:
in rpn!(5 1 -2) the stack holds two values when - arrives, so the
subtraction rule fires and computes 5 - 1, leaving 2 and then an
unfinished stack of two values.
Check your understanding What goes wrong if the public entry rule is listed first? Show answer Hide answer
It matches the macro's own recursive calls, without end.
The entry rule ($($t:tt)*) matches any sequence of token trees. If it is the
first rule, rpn!(3 4 +) expands to rpn!(@run [] 3 4 +), and that call
matches the entry rule again, expanding to rpn!(@run [] @run [] 3 4 +), and
so on. No @run rule ever gets a chance to match. The compiler stops at the
recursion limit and reports "recursion limit reached while expanding rpn!".
This is why a rule that matches everything belongs after the more specific rules, and why internal calls carry a marker that the specific rules can match on. The starter has the entry rule first so that you see this error.
Your job
Add the rule that ends count_tokens!'s recursion, and move rpn!'s public
entry rule below the @run rules.
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.