Skip to content

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

Easy Primitives

Your first macro_rules!: matchers and transcribers

Add three rules to describe!: two literal-token cases and a final catch-all. Keep the existing empty-input rule and the supplied run function.

pub fn run(n: usize) -> String

The macro must expand to these string literals:

Tokens inside the invocation Expansion
No tokens "nothing"
one "one thing"
one, two "two things"
Any other valid token-tree sequence "something else"

Match those specific cases exactly. For example, one,, one two and one, two, use the catch-all. Whitespace between tokens does not change a match. The words one and two are literal tokens here, not variable values.

What run returns

The function contains seven macro invocations. They all need to expand successfully during compilation, even though only one match arm runs for a given n.

n Returned string
0 nothing
1 or 4 one thing
2 or 5 two things
3 or any value greater than 5 something else

The quotes in the first table are Rust string-literal syntax, not characters in the returned string. The starter only handles empty input, so its other invocations fail to compile.

Matchers and transcribers

Each rule has a matcher before => and a transcriber after it. The matcher recognizes the input tokens; the transcriber supplies the expansion. A macro invocation is expanded by the compiler, rather than calling a runtime function. The resulting expression can still evaluate to a value at runtime.

Here is a complete example with a different vocabulary:

macro_rules! reply {
    (hello) => { "hi" };
    ($($other:tt)*) => { "unrecognized" };
}

fn main() {
    assert_eq!(reply!(hello), "hi");
    assert_eq!(reply!(goodbye), "unrecognized");
}

No variable named hello or goodbye is needed. The matcher recognizes or captures those tokens, and the transcriber does not emit them as variable uses. Macro-generated code is still subject to the relevant syntax, name-resolution, type and borrow checks. Expansion interacts with name resolution; it is not simply one text-replacement pass before all other compiler work.

Put the catch-all last

Rules are tried in source order. Rust uses the first successful match, without ranking rules by specificity. If its expansion is invalid, Rust reports that error; it does not try a later rule as a fallback.

$($rest:tt)* captures zero or more token trees. A token tree is one token or a balanced delimited group. This pattern accepts the empty case and all the other well-formed inputs in this exercise. Put it before the specific cases and it will shadow them. Such ordering can compile while producing the wrong answers, which is why the examples test more than the catch-all case.

Outer delimiters and inner groups

For matching, the invocation's outer delimiters can be parentheses, brackets or braces. describe!(one), describe![one] and describe!{one} all select the same rule.

Delimiters inside the invocation are still tokens to match. describe!((one)) contains a parenthesized group, so it does not match the literal (one) rule; it reaches the catch-all. The first parentheses in a rule enclose the matcher, while the extra parentheses in that invocation are part of its input.

The surrounding Rust syntax also matters. For example, brace-delimited macro invocations used as statements can omit a trailing semicolon. The supplied calls are expressions in match arms, so use them as written.

Keep the definition before run

This local, unqualified macro call uses textual scope. Keep macro_rules! describe before run, so the definition is in scope where the calls appear. Rust macros also have path-based scoping and export mechanisms; this exercise does not need them.

Your solution must compile, return the required strings and pass Clippy with warnings treated as errors.