I have been working on a set of token munching macro_rules! for a project at work recently. Par
for the course for this sort of project I ran into all sorts of gnarly macro matching failures and
inscrutable rustc diagnostics that tend to accommodate them. Somewhere along the way I was able to
come up with a way to use a combination of compile_error!, stringify! to achieve what I think
can be best described as a print-debugging equivalent for macro_rules. Here is how to apply this
and what to look out for.
I will, naturally, use some pretty obviously faulty and perhaps nonsensical-in-isolation
macro_rules! for the sake of examples here, but I promise – this approach works just as well in
macros that span a thousand lines. Or, at least, it has worked for me! Here’s the first specimen:
#[macro_export]
macro_rules! fragment_typo {
($exprtts:tt) => {
$crate::fragment_typo!(@expr $expr)
};
(@expr $expr:expr) => {
$expr
};
}Passing this macro an expression 42 will produce the following diagnostic:
error: no rules expected `$`
--> src/main.rs:4:38
|
2 | macro_rules! fragment_typo {
| -------------------------- when calling this macro
3 | ($myexpr:tt) => {
4 | $crate::fragment_typo!(@expr $expr)
| ^^^^^ no rules expected this token in macro call
...
12 | let x = fragment_typo!(42);
| ------------------ in this macro invocation
|
note: while trying to match meta-variable `$expr:expr`
--> src/main.rs:6:12
|
6 | (@expr $expr:expr) => {
| ^^^^^^^^^^
= note: this error originates in the macro `fragment_typo` (in Nightly builds, run with -Z macro-backtrace for more info)In case you haven’t spotted the problem yet, the first arm of the macro is using $expr token
which wasn’t bound anywhere. Easy to see in a 10 line macro, trivial to miss in a 1000 line one.
Here’s what I started doing. First, I determine from the diagnostics which line to “instrument”. In
this case rustc barfs at main.rs:4, so I wrap it inside compile_error!(stringify!()), as such:
macro_rules! fragment_typo {
($exprtts:tt) => {
+ compile_error!(stringify!(
$crate::fragment_typo!(@expr $expr)
+ ))
};
(@expr $expr:expr) => {
$expr
};
}Building again, compiler now spits out:
error: $crate :: fragment_typo! (@ expr $expr)At this point there are two things that I do to help me debug. I can now copy this macro invocation into my code to be invoked directly, rather than through many layers of macro invocations above. That way it is possible to modify the invocation as needed, to determine if the problem is in the macro layers above (e.g. them accumulating/grouping tokens in the wrong way), or the macro being invoked (e.g. forgot to add an arm that would match this particular shape), or perhaps this specific invocation itself as is the case here.
The other approach is informed by the very useful property of how compile_error!(stringify!())
works: it prints out the tokens that fragments have matched, if it is able to. With that in mind,
$expr staying an unexpanded “fragment” looks quite suspect here. Indeed, what we have in scope
is $exprtts instead! Let’s fix and rebuild to exemplify what I mean:
macro_rules! fragment_typo {
($exprtts:tt) => {
compile_error!(stringify!(
- $crate::fragment_typo!(@expr $expr)
+ $crate::fragment_typo!(@expr $exprtts)
))
};
(@expr $expr:expr) => {
$expr
};
}error: $crate :: fragment_typo! (@ expr 42)
--> src/main.rs:4:9To close this off, one more very nice property of compile_error! in modern Rust is that it does
not stop on the first occurrence. So you can do something like this:
#[macro_export]
macro_rules! fragment_typo {
($exprtts:tt) => { {
compile_error!(stringify!($crate::fragment_typo!(@expr $exprtts)));
$crate::fragment_typo!(@expr $exprtts)
} };
(@expr $expr:expr) => { {
compile_error!(stringify!($expr));
$expr
} };
}and get an actually useful expansion trace:
error: $crate :: fragment_typo! (@ expr 42)
--> src/main.rs:4:9
...
error: 42
--> src/main.rs:8:9That’s all for this one, now you too can enjoy macro crimes without any of the headache!