Image: Ayan El Aichi.
rustc
treats macro definitions as some opaque piece of tokens and don't do any
check on them. For instance, the following macro definition is valid:
macro_rules! my_vec {
($($t:expr),*) => {{
let mut buffer = Vec::new();
$(
buffer->push($t);
)*
buffer
}};
}
However, any call to the my_vec
macro is invalid, as ->
can't be used for
method calls.
Luckily for us, this crate provides the expandable::expr
macro, that
checks that the macro expands to a valid expression. Let's use it on
js_concat
:
#[expandable::expr]
macro_rules! my_vec {
($($t:expr),*) => {{
let mut buffer = Vec::new();
$(
buffer->push($t);
)*
buffer
}};
}
This emits the following error 1:
error: Potentially invalid expansion. Expected an expression, an identifier, `!=`, `!`, `%`, `&&` or 37 others.
--> tests/ui/fail/my_vec.rs:9:17
|
9 | buffer->push($t);
| ^^
Macros can expand to different things depending on where they are called.
As a result, expandable
must know what the macro expands to. To do so,
multiple macros are available:
- Macros that expand to expressions are checked by
expandable::expr
, - Macros that expand to items are checked by
expandable::item
, - Macros that expand to patterns are checked by
expandable::pat
, - Macros that expand to statements are checked by
expandable::stmt
, - Macros that expand to types are checked by
expandable::ty
,
This section briefly describes what the macros can detect and what they can't. All the terminology used in this section is taken from the Ferrocene Language Specification.
This crate aims to detect all the things that are not considered by rustc
as an invalid macro definition. It may or may not detect what rustc
already detects. Some errors can't be detected because they are not
possible to detect in the first place.
rustc
is quite good at detecting invalid matchers. This crate deliberately
does not try to compete with it. Whether if this crate returns an error on
an invalid matcher is left unspecified.
Some macros can't be expanded because they don't respect the rules for a transcription to happen. For instance, the following macro exhibits a transcription error:
macro_rules! invalid_expansion {
( $( $a:ident )? ) => { $a }
}
In this example, the repetition nesting of $a
used in the macro
transcription does not match the repetition nesting of $a
defined in the
matcher.
This crate aims to detect all the possible invalid transcription.
Some macro expand correctly (ie: they respect the transcription rules), but the produced AST is invalid. For instance, the following macro expands to an invalid AST:
macro_rules! invalid_expansion {
() => { * }
}
(*
is an invalid item, an invalid expression, an invalid pattern, an
invalid statement and an invalid type -- there is no context in which it can
be called).
rustc
doesn't (and can't) detect any potentially invalid AST. This is the
raison d'être of this crate.
In the general case, proving that a macro is well-formed is impossible. This crates has to make assumptions about the macros it is checking. This section lists them and gives a short rationale of why they are needed.
This crate assumes that the macro expansion does not contain any macro
definition. For instance, the following macro definition can't be checked
with expandable
:
macro_rules! foo {
() => {
macro_rules! bar {
() => { 42 }
}
}
}
This limitation is caused by the fact that it's impossible to tell if a
sequence of tokens is a macro definition or not. For instance, the
macro_rules
token may be passed by the user when invoking the macro.
Having no guarantee of what's a macro and what is not makes it impossible
to ensure that a metavariable is properly defined.
This crate assumes that one expansion of a macro works. It does not check that the recursive expansion of the macro works. It checks that the macro invocation itself is legal, but don't check that it matches any rule.
This is caused by the fact that other macros may add more constrains on the macro invocation. This requirement may be lifted in the future for macros that call themselves.
This crate requires a metavariable indication used in a transcriber to have the same repetition stack as the corresponding metavariable that appears in the matcher.
For instance, the following code is rejected by expandable
:
#[expandable::expr]
macro_rules! fns {
($($a:ident)*) => {
$(
fn $a() {}
)+
}
}
expandable
currently supports Rust 1.70 and above. Bumping the MSRV is not
considered a breaking change. If you believe this is an issue, then consider
funding this crate's development and maintenance. Send a mail to sasha dot
pourcelot at
protonmail dot
com for more info.
Note that the embedded parser will support syntax that was introduced after Rust 1.70.
Adding support for newer syntax is not considered a breaking change.
The error messages generated by expandable
are not stable and may change
without notice.
Any change that may trigger errors on previously accepted code is considered a breaking change.
Licensed under the MIT license.
Footnotes
-
The Rust grammar is not fully implemented at the moment, leading to incomplete "expected xxx" list this will be fixed before the first non-alpha release of this crate. ↩