Skip to content

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

Medium Primitives

Count macro arguments with stable Rust

Implement two macros that count comma-separated token trees. One should use a unit array and the other an addition expression. Both must work in const contexts on stable Rust.

Keep the supplied public items:

pub const N: usize = count_args!(a, b, c, d, e, f, g);
pub fn sizes() -> (usize, usize, usize, usize, [u8; N])

Required result

Complete replace_expr!, count_args! and count_args_add!. Preserve the supplied N and sizes definitions. The returned tuple must contain:

Element Value
count_args!() 0
count_args!(solo) 1
count_args!(a, b, c, d, e, f, g) 7
count_args_add!(p, q, r,) 3
The final array Seven zero bytes.

Each argument matches one tt token tree. An identifier such as a counts once, as does a delimited group such as (a + b) or [1, 2]. The identifiers do not need to name variables: the macros count syntax without evaluating it. An ungrouped multi-token expression such as a + b does not match one tt.

Both macros must accept an empty invocation and an optional trailing comma after a nonempty list. A comma-only invocation is outside the exercise's input syntax; the supplied matcher reports a local ambiguity for it.

The harness selects tuple values with zero, one, seven and three. array_len checks the final array's length and array_sum checks that its bytes sum to zero. Unknown selectors return usize::MAX in the supplied harness. Those output checks do not independently prove which macro technique you used.

Replace matched syntax with a fixed expression

replace_expr! receives one token tree followed by an expression. Emit only the expression. This lets an outer repetition use the number of matches without using their contents.

Here is a complete example of that helper:

macro_rules! replace_expr {
    ($_ignored:tt $sub:expr) => { $sub };
}

fn main() {
    let value = replace_expr!((not_a_variable + either) 5usize);
    assert_eq!(value, 5);
}

The ignored group is never evaluated or resolved as an expression. The replacement can be unit () for the first counting macro, or 1usize for the second.

Two const-compatible techniques

For count_args, repeat a replacement unit value inside an array, then take its length with <[()]>::len(&[...]). Here [()] names the slice type, and the borrowed array coerces to a slice. Specifying the element type also makes the empty-array case unambiguous.

Unit has size zero, so the array's elements need no storage. That fact is separate from const evaluation: a const context requires the count to be evaluated at compile time.

For count_args_add, start with 0usize and repeat + 1usize, using replace_expr for each matched argument. The zero makes an empty invocation a valid expression. Use repetition rather than recursion for this exercise.

Both techniques can size an array. The addition expression is just as valid in an array-length const context as the slice-length expression. Implement one macro with each technique to practice both, not because one lacks that capability. Very large expansions can still hit compiler limits; this exercise uses small argument lists.

Why the starter fails on stable Rust

The starter uses ${count($x)}, a macro metavariable expression. Rust 1.98.1 rejects it with E0658 because that feature is unstable. Replace both uses with the stable techniques above; do not enable a nightly feature gate.

A computed const expression is not automatically a literal token. Array lengths accept const expressions, while an attribute such as repr(align(8)) requires its numeric argument in the attribute's accepted syntax. Computing an equivalent const does not make repr(align(N)) valid. Macro rules can map known tokens to literal tokens, but ordinary const evaluation does not rewrite tokens for an attribute.

Submissions must compile, pass the output checks and pass Clippy with warnings treated as errors.