Skip to content

← Modules, Visibility, Testing and Docs step 10 of 22

Hard Framework

macro_rules! scoping is not item scoping

Everything you have learned so far about items being order-independent is true. You can call a function declared at the bottom of the file from the top; you can name a struct before you define it; modules resolve regardless of where the mod block sits.

macro_rules! is the exception, and it will bite you exactly once.

A macro_rules! macro is textually scoped: it exists from the line of its definition to the end of the enclosing block, and nowhere else. Use it one line above and you get E0433 — cannot find macro foo in this scope, with a note that reads “a macro with the same name exists, but it appears later”. Which is a wonderfully clear message once you know the rule, and completely bewildering before.

That is what the starter does. Read the error, then fix it.

The task

pub fn run(cases: Vec<(i64, i64, Option<String>)>) -> Vec<Option<String>>

Write my_assert_eq! with two arms, reproducing std’s assert_eq! panic payload byte for byte:

assertion `left == right` failed
  left: 4
 right: 5

and, with a message,

assertion `left == right` failed: ctx
  left: 4
 right: 5

Note the alignment — two spaces before left, one before right, so the colons line up. That is std’s formatting and the tests compare exactly.

For each (left, right, message), invoke the macro inside catch_unwind and return None if it passed, or Some(payload) if it panicked. When message is Some(m), use the message arm.

Writing the two arms

Arms are tried top to bottom, so the more specific arm goes first:

($left:expr, $right:expr, $($arg:tt)+) => { … };
($left:expr, $right:expr $(,)?)        => { … };
```

`$($arg:tt)+` means "one or more token trees", so a two-argument call cannot
match the first arm and falls through by itself. The `$(,)?` on the second arm
allows a trailing comma, which is what std does and what people expect.

For the message, `format_args!($($arg)+)` builds a `Display` value without
allocating a `String` first — pass it straight into `panic!` as a `{}`
argument. This is exactly how std does it, and it is the reason the message arm
accepts `"{} of {}", a, b` and not just a literal.

## The two things to move it into a module

The syllabus for this item asks for one more thing, and the tests cannot check
it, so it is on you: **define the macro inside `mod asserts` and bring it back
with `use`.** The pattern is:

mod asserts {

macro_rules! my_assert_eq { … }
pub(crate) use my_assert_eq;   // now it is an ordinary item with a path

}

use asserts::my_assert_eq;


That `pub(crate) use` line is the modern idiom, and it is worth understanding
what it buys: textual scoping is replaced by path-based scoping, so the macro
now obeys the same privacy and import rules as everything else in your crate.

The old alternative is `#[macro_export]`, and it has a surprise of its own:
**it hoists the macro to the crate root regardless of which module you wrote it
in.** People carefully organise a macro into `crate::util::asserts` and then
find their users writing `my_crate::my_assert_eq!`.

## `$crate` and the lint that will save you

Inside an exported macro, never write `crate::helper()`. Write
`$crate::helper()`. `crate` in a macro body means "the crate that *invoked* the
macro", which is not yours, so the path resolves to something that does not
exist in every downstream user's code. `$crate` is the hygiene escape hatch that
expands to a path to *your* crate.

`clippy::crate_in_macro_def` is warn-by-default and catches this — which makes
it a real gate here, not a suggestion. It is one of the few lints that catches a
bug you cannot possibly hit while testing your own crate.

## And the panic hook

`catch_unwind` catches the unwind, but the default panic hook still prints to
stderr on the way past, so a suite of intentional panics produces a wall of
noise. Take the current hook, install a no-op, run, then put the original back:

let previous = std::panic::takehook(); std::panic::set_hook(Box::new(|| {})); // … std::panic::set_hook(previous);


The hook is process-global, so putting it back is not optional politeness.