Skip to content

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

Medium Primitives

Classify syntax with macro fragment specifiers

Complete kind_of! so the supplied classify() function labels its macro inputs correctly. Keep also! and classify() unchanged.

pub fn classify() -> Vec<String>

There are 24 calls: 12 to kind_of!, followed by 12 to also!. The starter's catch-all makes the first 12 return "tt"; the supplied also! calls already produce their required results.

Match the requested syntax

Add these rules to kind_of! in order, before the existing catch-all: lifetime, literal, ident, stmt followed by ;, block, path, ty, then expr. Each rule returns the string naming its fragment kind.

Input to kind_of! Required tag
'static "lifetime"
42, -7 "literal"
foo, pub "ident"
let z = 1; "stmt"
{ let x = 1; x } "block"
std::vec::Vec "path"
[u8; 4], &str "ty"
1 + 2 "expr"
a b 1 2 "tt"

A single tt matches one token or one delimited group. The existing $($any:tt)* catch-all repeats that match, so it can accept several tokens.

The supplied tagged macro checks additional cases:

Input to also! Required tag
pat Some(ref x), pat _ "pat"
ty _ "ty"
ident _ "no"
vis pub fn, vis fn "vis"
meta doc = "hi", meta derive(Debug) "meta"
item fn nothing() {} "item"
bare_stmt let z = 1; "no"
expr21 1 + 2 "expr21"
expr21 _ "no"

The result vector preserves the order of all 24 calls in classify(). Captured syntax is classified, not evaluated: foo does not need a value declaration, and the captured function item is not emitted into the program.

Fragment kinds overlap

The same input can match several kinds. foo can be an identifier, a path, a type or an expression. The first successful rule determines the tag, so put ident before the broader alternatives. [u8; 4] and &str are types, but not paths. _ matches an inferred type or a wildcard pattern, but not an ident. Keywords such as pub can match ident.

The literal matcher can capture -7, including its minus sign. This is a macro matching rule; in ordinary expression syntax, negation is a unary operation. The vis matcher can consume no tokens, which lets one rule accept both public and private item declarations.

Match the let-statement semicolon explicitly

For let z = 1;, $s:stmt captures the statement without its final ;. A matcher containing only $s:stmt therefore does not match the whole input. The kind_of! rule needs $s:stmt ; to match the remaining separator.

This is not universal for every statement: item statements that require a semicolon include it in the captured fragment. Keep the distinction between matching input tokens and emitting an already-parsed statement.

The macro definition's edition matters

In edition 2024, expr also accepts a top-level _ or const { ... }. expr_2021 excludes those top-level forms. Since edition 2021, pat accepts top-level alternatives; pat_param excludes them. These choices depend on the edition of the macro definition, not merely the caller.

Here is a complete example that isolates the expression-edition difference:

macro_rules! expression_kind {
    (old $e:expr_2021) => { "old" };
    (new $e:expr) => { "new" };
    ($($other:tt)*) => { "no" };
}

fn main() {
    assert_eq!(expression_kind!(old 1 + 2), "old");
    assert_eq!(expression_kind!(old _), "no");
    assert_eq!(expression_kind!(new _), "new");
    assert_eq!(expression_kind!(new const { 3 }), "new");
}

The untagged kind_of! cascade is designed for the supplied calls, not as a universal Rust syntax classifier. Fragment parsing can produce a diagnostic instead of falling through to a later rule on malformed input. Leading tags, as used by also!, let a caller select the intended grammar explicitly. They do not make malformed fragments valid.

Your solution must compile, preserve the supplied calls, return all tags in order and pass Clippy with warnings treated as errors.