How to report errors in a procedural macro using the quote macro?
Asked Answered
H

4

34

I am writing a procedural macro which works fine, but I am having trouble reporting errors in an ergonomic way. Using panic! "works" but is not elegant and does not present the error message to the user nicely.

I know that I can report good errors while parsing a TokenStream, but I need to produce errors while traversing the AST after it has been parsed.

The macro invocation looks like this:

attr_test! {
    #[bool]
    FOO
}

And should output:

const FOO: bool = false;

This is the macro code:

extern crate proc_macro;
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{Attribute, parse_macro_input, Ident, Meta};

struct AttrTest {
    attributes: Vec<Attribute>,
    name: Ident,
}

impl Parse for AttrTest {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(AttrTest {
            attributes: input.call(Attribute::parse_outer)?,
            name: input.parse()?,
        })
    }
}

#[proc_macro]
pub fn attr_test(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let test: AttrTest = parse_macro_input!(tokens);
    let name = test.name;
    let first_att = test.attributes
        .get(0)
        .and_then(|att| att.parse_meta().ok());
    if let Some(Meta::Word(ty)) = first_att {
        if ty.to_string() != "bool" {
            panic!("expected bool");
        }
        let output = quote! {
            const #name: #ty = false;
        };
        output.into()
    } else {
        panic!("malformed or missing metadata")
    }
}

I would like to produce an error if anything other than bool is specified in the attribute. For example, input like this:

attr_test! {
    #[something_else]
    FOO
}

should result in something like:

error: expected bool
attr_test! {
    #[something_else]
      ^^^^^^^^^^^^^^ expected bool
    FOO
}

During parsing, there is a Result, which has lots of useful information including a span, so the resulting errors can highlight the exact parts of the macro call that have a problem. But once I'm traversing the AST, I can't see a good way to report errors.

How should this be done?

Helenhelena answered 27/1, 2019 at 20:44 Comment(0)
R
56

Apart from panicking, there are currently two ways to reports errors from a proc-macro: the unstable Diagnostic API and "the compile_error! trick". Currently, the latter is mostly used because it works on stable. Let's see how they both work.

The compile_error! trick

Since Rust 1.20, the compile_error! macro exists in the standard library. It takes a string and leads to an error at compile time.

compile_error!("oopsie woopsie");

Which leads to (Playground):

error: oopsie woopsie
 --> src/lib.rs:1:1
  |
1 | compile_error!("oopsie woopsie");
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This macro has been added for two cases: macro_rules! macros and #[cfg]. In both cases, library authors can add better errors if the user uses the macro incorrectly or has the wrong cfg values.

But proc-macro programmers had an interesting idea. As you might know, the TokenStream you return from your procedural macro can be created however you like. That includes the spans of those tokens: you can attach any spans you like to your output tokens. So the main idea is this:

Emit a tokenstream containing compile_error!("your error message"); but set the span of those tokens to the span of the input token that caused the error. There is even a macro in quote which makes this easier: quote_spanned!. In your case, we can write this:

let output = if ty.to_string() != "bool" {
    quote_spanned! {
        ty.span() =>
        compile_error!("expected bool");
    }
} else {
    quote! {
        const #name: #ty = false;
    }
};

For your faulty input, the compiler now prints this:

error: expected bool
 --> examples/main.rs:4:7
  |
4 |     #[something_else]
  |       ^^^^^^^^^^^^^^

Why exactly does this work? Well: the error for compile_error! shows the code snippet containing the compile_error! invocation. For that, the span of the compile_error! invocation is used. But since we set the span to point to the faulty input token ty, the compiler shows the snippet underlining that token.

This trick is also used by syn to print nice errors. In fact, if you are using syn anyway, you can use its Error type and in particular the Error::to_compile_error method which returns exactly the token stream we manually created with quote_spanned!:

syn::Error::new(ty.span(), "expected bool").to_compile_error()

The Diagnostic API

As this is still unstable, just a short example. The diagnostic API is more powerful than the trick above as you can have multiple spans, warnings and notes.

Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();

After that line, the error is printed, but you can still do stuff in your proc-macro. Usually, you would just return an empty token stream.

Rucker answered 27/1, 2019 at 23:51 Comment(3)
I'd like to point out that the unwrap() in ty.span().unwrap() is not anything with Result/Option, but a method for converting proc_macro2::Span to proc_macro::Span.Intuitivism
Noting here that the compile_error! trick is still handy when using syn, when the appropriate location is not in the derive input, but the derive declaration itself. such as: ``` #[derive(YourMacro)] ^^^^^^^^^ your message ```. It has workaround vibes but it's neat enough. Let me know if something cleaner is available.Hermaphroditus
Unfortunately proc_macro::Diagnostic is still unstable, and ty.span().unwrap().error("...").emit() (which is the same) is unstable too.Diphosgene
S
9

The accepted answer mentioned the unstable Diagnostic API, which gives you much more power and control than the regular compile_error. Until the Diagnostic API stabilizes, which probably will not be any time soon, you can use the proc_macro_error crate. It provides a Diagnostic type that is designed to be API compatible with the unstable proc_macro::Diagnostic. The entire API is not implemented, only the part that can be reasonably implemented on stable. You can use it by simply adding the provided annotation to your macro:

#[proc_macro_error]
#[proc_macro]
fn my_macro(input: TokenStream) -> TokenStream {
    // ...
    Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
}

proc_macro_error also provides some useful macros for emitting errors:

abort! { input,
    "I don't like this part!";
        note = "A notice message...";
        help = "A help message...";
}

However, you might want to consider sticking to using the Diagnostic type as it will make it easier to migrate to the the official Diagnostic API when it stabilizes.

Styptic answered 11/2, 2021 at 22:45 Comment(0)
F
2

The existing solutions just rely on external libraries without explaining how it actually works.

You do not need to use quote or proc-macro2. Those are convenience libraries that make life a bit easier.

To render a compiler_error block, just literally do exactly that, like this:

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

enum MyErr {
    Nah,
}

fn my_macro_impl(input: TokenStream) -> Result<TokenStream, MyErr> {
    let tokens: Vec<TokenTree> = input.into_iter().collect();
    if tokens.len() != 0 {
        return Err(MyErr::Nah);
    }
    Ok([TokenTree::Literal(Literal::string("Hello world"))].into_iter().collect())
}

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    match my_macro_impl(input) {
        Ok(v) => v,
        Err(_) => [
            TokenTree::Ident(Ident::new("compile_error", Span::mixed_site())),
            TokenTree::Punct(Punct::new('!', Spacing::Alone)),
            TokenTree::Group(Group::new(
                Delimiter::Parenthesis,
                [TokenTree::Literal(Literal::string("Some error message here!"))].into_iter().collect(),
            )),
        ]
        .into_iter()
        .collect(),
    }
}

...and if you call it, like this:

use my_macros::my_macro;

#[test]
pub fn main() {
    assert_eq!(my_macro!("."), "Hello world");
}

You'll get this when you compile it:

error: Some error message here!
 --> tests/000_simple.rs:5:16
  |
5 |     assert_eq!(my_macro!("."), "Hello world");
  |                ^^^^^^^^^^^^^^
  |
  = note: this error originates in the macro `my_macro` (in Nightly builds, run with -Z macro-backtrace for more info)

TokenStream is a Vec of TokenTree; if you want to render the block:

'compiler_error' '!' '(' 'error' ')'

Then the AST you need to return is:

- Ident -> 'compiler_error'
- Puct -> '!'
- Group 
-- Delimiter::Parenthesis
-- TokenStream
---- Literal -> "my error"

That's how it works.

Everything else is just 'magic' to make it easier to use.

Filthy answered 20/2, 2023 at 7:42 Comment(0)
E
0

This answer still gives the full macro call as the span for the error. But the syn crate carefully sets the span of all produced tokens, so the error points to an exact spot within the macro call.

So, this is the full example using the compiler_error! trick, without using syn or quote! that pinpoints the exact location of the error:

fn my_macro_impl(input: TokenStream) -> Result<TokenStream, Span> {
    //parse input and generate output
    //...
    if oops {
        return Err(span_where_the_error_occurred);
    }
    //...
    Ok(output_stream)
}

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    use proc_macro::TokenTree as TT;
    match my_macro_impl(input) {
        Ok(output_stream) => output_stream,
        Err(span) => [
        TT::Punct({
            let mut punct = Punct::new(':', Spacing::Joint);
            punct.set_span(span);
            punct
        }),
        TT::Punct({
            let mut punct = Punct::new(':', Spacing::Alone);
            punct.set_span(span);
            punct
        }),
        TT::Ident(Ident::new("core", span)),
        TT::Punct({
            let mut punct = Punct::new(':', Spacing::Joint);
            punct.set_span(span);
            punct
        }),
        TT::Punct({
            let mut punct = Punct::new(':', Spacing::Alone);
            punct.set_span(span);
            punct
        }),
        TT::Ident(Ident::new("compile_error", span)),
        TT::Punct({
            let mut punct = Punct::new('!', Spacing::Alone);
            punct.set_span(span);
            punct
        }),
        TT::Group({
            let mut group = Group::new(Delimiter::Brace, {
                TokenStream::from_iter(vec![TT::Literal({
                    let mut string = Literal::string("Error: it wen wron HERE");
                    string.set_span(span);
                    string
                })])
            });
            group.set_span(span);
            group
        }),].into_iter().collect(),
            )),
        ]
        .into_iter()
        .collect(),
    }
}

Note how every TokenTree node has it's span set. Otherwise they would default to Span::call_site().

Exurbanite answered 11/2 at 18:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.