scrabsha / expandable

What if we could check declarative macros before using them?
MIT License
45 stars 4 forks source link

expandable

An opinionated attribute-macro based macro_rules! expansion checker.



A young hedgehog standing on top of a hill. They are looking at the moon through a telescope. The moon is surrounded by $( on its left and )* on its right. The sky is clear, plenty of stars are visible. In the background, there's a forest, and a treehouse, and a bunch of mountains. Image: Ayan El Aichi.

Textbook example

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 [^error-message]:

error: Potentially invalid expansion. Expected an expression, an identifier, `!=`, `!`, `%`, `&&` or 37 others.
 --> tests/ui/fail/my_vec.rs:9:17
  |
9 |           buffer->push($t);
  |                 ^^

[^error-message]: 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.

Expansion context

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:

What can it detect?

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.

Invalid matcher

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.

Invalid transcription

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.

Invalid produced AST

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.

Opinionated?

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.

No recursive macro definition

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.

No macro call (for now)

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.

The repetition stack must match

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() {}
        )+
    }
}

Minimal Supported Rust Version (MSRV), syntax support and stability

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.

License

Licensed under the MIT license.