Skip to content

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

Medium Primitives

Building std's macros: vec!, matches!, a HashMap literal

Rebuild three macros you have used hundreds of times, and watch the magic evaporate.

pub fn run(xs: Vec<i64>) -> (Vec<i64>, Vec<bool>, Vec<(String, i64)>)

Three macros to finish:

  • my_vec! — the list form with a trailing comma, plus [elem; n].
  • my_matches!my_matches!(expr, pattern) and my_matches!(expr, pattern if guard).
  • hashmap!hashmap!{ k => v, … } with a trailing comma, pre-sized with with_capacity using the stable counting idiom from 18.7.

Every one of these is a plain macro_rules! definition in the standard library. Not a compiler intrinsic, not a special form. vec! is thirty lines you could have written.

my_vec! — same three arms as the real thing

You did the list and repeat arms in 18.5. The real vec! looks like this:

macro_rules! vec {
    () => ( Vec::new() );
    ($elem:expr; $n:expr) => ( from_elem($elem, $n) );
    ($($x:expr),+ $(,)?) => ( <[_]>::into_vec(Box::new([$($x),+])) );
}
```

`<[_]>::into_vec(Box::new([...]))` builds a fixed-size array, boxes it, and
converts the box into a `Vec` **without reallocating** — the allocation is
already the right size and the right layout. That is why `vec![1, 2, 3]` costs
exactly one allocation and nothing else.

The starter's list arm already uses that formulation; it just needs `$(,)?` and
the `[elem; n]` arm.

## `my_matches!` — the tricky one

($e:expr, $p:pat $(if $g:expr)?) => {

match $e {
        $p $(if $g)? => true,
    _ => false,
}

};


Three things are happening at once.

**The optional group.** `$(if $g:expr)?` makes the guard optional, and the same
`$(if $g)?` appears in the transcriber. Both halves must agree; you cannot make
the matcher optional and the transcriber unconditional.

**The follow set.** `$p:pat` is followed by `if` — which is legal, because `if`
is one of the five tokens in `pat`'s follow set (18.12). This is not a
coincidence: `if` is in that list precisely so that `matches!` can be written.

**Order matters inside the arm.** `$p $(if $g)? => true` puts the guard between
the pattern and the `=>`, which is where a `match` arm wants it. Emit them in
the wrong order and you get a parse error inside the expansion.

Note that `$p:pat` in edition 2024 accepts a top-level or-pattern, so
`my_matches!(n, 1 | 2)` works with no extra arm. That is the change that removed
`|` from `pat`'s follow set — you get the feature, you lose the follower.

## `hashmap!` — counting, and the empty case

```rust
hashmap! {
    "alpha".to_string() => 1i64,
    "beta".to_string()  => 2i64,
}
```

Two arms. The list arm should pre-size the map:

```rust
let mut m = HashMap::with_capacity(count_tts!($($k),*));
$( m.insert($k, $v); )+
m

count_tts! is the array idiom from 18.7, already written for you. Pre-sizing is the whole reason to bother: without it a five-entry literal reallocates as it grows, which is silly when the count is sitting right there in the source.

The empty case needs its own arm, and the reason is a lint. If you try to serve hashmap!{} from the list arm, the expansion contains let mut m = … with no inserts, and rustc’s warn-by-default unused_mut fires:

warning: variable does not need to be mutable

Under clippy -D warnings that fails your submission — and it fails it at the call site of the empty invocation, which is a genuinely confusing place for it to appear. Giving the empty case its own arm that just says HashMap::new() is exactly what vec! does, and now you know why.

::: question clippy has lints called match_like_matches_macro and useless_vec. Won’t they fire on code that implements those macros?

useless_vec will, if you invoke vec! where a slice or array would do — it is about call sites, not definitions, so building your own Vec machinery is safe.

match_like_matches_macro is the more interesting one. It fires on a match whose arms only produce true and false, telling you to use matches! instead — which is precisely the shape of a correct my_matches! implementation. It does not fire here, because clippy suppresses a range of style lints inside macro expansions; the whole point of the lint is to improve hand-written code, and code produced by a macro was not hand-written.

That behaviour is worth knowing in both directions. It means implementing a standard-library macro will not fight the linter. It also means clippy is quieter inside your macros than outside them, so a mistake that a lint would normally catch can hide in an expansion. :::

Your job

Finish all three. The starter compiles none of the six invocations in run.