We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 9 of 28
Internal rules and push-down accumulation
Make a macro that writes a whole enum — plus an impl block to go with it.
build_enum! { Color: Red = 1, Green = 2, Blue = 4 }
build_enum! { Level: Low = 10, High = 20, }
pub fn variants() -> Vec<(String, u8)>
Each invocation must expand to a #[derive(Debug, …)] #[repr(u8)] pub enum with
the given variants and discriminants, and an impl with an
all() -> Vec<(&'static str, u8)> associated function listing them in order.
This is item 18.1’s third justification made concrete: functions cannot generate items. Macros can, and that is when they stop being a convenience.
Why an accumulator is unavoidable
A muncher (18.8) peels one item at a time. That works when each step can emit its
own piece of output — 1 + count!(rest) glues fine because + is an expression
operator.
Item generation cannot work that way. You cannot emit half an enum, recurse,
and emit the other half; an enum is a single item and has to come out whole. So
the recursion has to carry the finished work forward and emit everything on
the last step:
build_enum!(@acc Color [] Red = 1, Green = 2, Blue = 4)
build_enum!(@acc Color [Red = 1,] Green = 2, Blue = 4)
build_enum!(@acc Color [Red = 1, Green = 2,] Blue = 4)
build_enum!(@acc Color [Red = 1, Green = 2, Blue = 4,]) <- emits everything
That is push-down accumulation. Only the final rule produces code; every earlier one just moves a variant from the input into the accumulator.
Learners reliably try to “return” a value from the recursion — to have the inner call hand something back to the outer one. There is no such thing. Expansion goes one way, outwards, and the only channel between steps is the argument list of the next invocation.
The accumulator must be one token tree
Notice [...] around the accumulator. That is not cosmetic. Brackets make the
whole accumulated list a single token tree, so the matcher can bind it with
one [$($done:tt)*] and know exactly where it ends. Write it bare and the
matcher cannot tell where accumulated output stops and pending input starts;
you get “local ambiguity” errors that are very hard to read.
@ is a convention, not magic
@acc has no special meaning to the compiler. @ is simply a token that cannot
begin any legitimate user invocation of this macro, which makes it a reliable
marker for “this rule is internal”. Any such token works — ~, !!, a
__private identifier. @ is what the ecosystem settled on, so use it.
Two ordering consequences, and both matter:
- Internal rules go first. They are the specific ones; the public entry rule is the general one. The general rule must never get a chance to swallow an internal invocation (18.8 shows what that costs).
-
The tag is what makes internal rules unreachable by users. Someone writing
build_enum!(@acc Color [] …)by hand is doing something wrong, and the sigil is the sign that says so.
The allow-by-default rustc lint unused_macro_rules is useful here: switch it on
while developing and it will tell you which of your arms is never reached, which
is the fastest way to find an ordering mistake.
The trailing-comma arm
build_enum! { Level: Low = 10, High = 20, } ends with a comma; the Color
invocation does not. That needs two munching rules — one for $vn = $vv, with a
comma, one for a final $vn = $vv without — and the second normalises it by
appending a comma to the accumulator anyway. After that, the terminal rule only
ever sees a uniform comma-terminated list, which is what lets its matcher be the
clean [$($vn:ident = $vv:literal,)*]`.
Normalising on the way in so the final rule can be simple is a pattern you will
reuse constantly.
::: question The terminal rule matches `[$($vn:ident = $vv:literal,)*] — it takes the accumulator apart again. Why not just splice the tokens straight through?
Because the accumulated tokens have to be used in two places, in different shapes.
Splicing $($done)* into the enum body works. But all() needs the same
variants written differently — as (stringify!($vn), $name::$vn as u8)` tuples —
and there is no way to transform raw token soup into that. Re-matching the
accumulator with a structured matcher recovers `$vn and $vv as separate
metavariables, and once you have those you can emit them in as many shapes as you
like.
This is the general move: accumulate loosely as tts while munching, then
re-parse structurally in the terminal rule. It costs one extra matcher and buys
you arbitrary output.
:::
Your job
Write the two internal munching rules. The terminal rule and the public entry rule are already there; the starter fails to compile because nothing bridges them.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.