Errors and Diagnostic Handling

As with any procedural macro, gnome-class goes to great lengths to enusre that errors are handled properly all throughout the macro. The errors that we're interested in fall into a few categories:

  • Parse errors. For example the tokens inside of a gobject_gen! { ... } invocation are invalid or malformed.

  • AST errors. While it's syntactically valid to define the same function twice it's semantically invalid to do so. This class of errors is any error which originates in the the procedural macro while it's generating the final set of tokens.

  • User code errors. For example if the user places let x: u32 = "foo" into a function that'll generate an error at compile time.

Each of these error cases are handled slightly differently, so let's go over them in turn.

Parse Errors

The first step of the gobject_gen! macro is to parse the input into an internal AST representation. This parsing operation is fallible, and errors can happen at any time!

All parsing happens in src/parser/* and it generates an ast::Program. The trick for handling errors here is all related to syn, the parsing library that we're using. The syn crate provides a Parse trait which is used to define custom parsers. Each custom parser can be defined in terms of other parsers as well. The implementation of Parse for ast::Program transitively uses many implementations of Parse for items already in syn.

The parsers themselves defined in this macro are each responsible for error handling. Errors can be generated when a sub-parser fails or explicitly generated via syn methods. The syn crate provides many useful opportunities to produce good error messages during parsing, for example pointing directly at an erroneous token and indicating what expected tokens were there.

The tl;dr; of handling parse errors is "we use syn and it just works".

AST Errors

Things get a little more interesting with AST errors or other semantic errors that are detected after parsing is completed. Outside of parsing we're not using syn's framework of error handling, but we still use syn::parse::Error for our fundamental error type!

The first thing you'll notice is that almost all functions in the procedural macro are fallible, returning a Result<T>. This is a typedef for Result<T, Errors> where the Errors type is defined in the src/errors.rs module. An instance of Errors represents a list of errors to present to the user. A list is used here so as many errors about the AST can be collected and presented to the user, rather than forcing them to go through errors one at a time.

The Errors type is a list of syn::parse::Error errors, and implements From from the syn error as well. Typically the Errors type is only constructed in loops where each iteration is fallible (and errors are collected across iterators). Otherwise it's vastly more common to only create one error and return it via the From impl.

The primary way to create an Error is via the bail! macro:


# #![allow(unused_variables)]
#fn main() {
bail!(some_item, "my message here");
#}

The some_item argument must implement the ToTokens trait and the error returned will point to the spans of some_item. This is a convenient and lightweight way of creating a custom error message on a specified set of spanned tokens. Internally this uses syn::parse::Error::new_spanned to create an error which actually spans the tokens represented by some_item.

With this idiom you'll find bail! used liberally throughout the library. Almost all semantic errors are created and returned through this macro (which is similar to the failure crate's own version of bail!). The first argument is typically whatever token is being examined or construct that's relevant, and is used to provide context for the error to ensure the users sees not only the error message but where in the code it's actually pointing to.

Note that there is also a format_err! macro to create an instance of syn::parse::Error if necessary.

User Errors

The final class of errors has to do with errors in user-written code, such as type errors or borrow-check errors. These errors do not come from gobject_gen! or the macro here, but rather from the compiler. If this happens, though, we want to make sure that the compiler errors are presented in a meaningful fashion.

This class of errors is largely transparently handled by simply using syn. The syn crate preserves all Span information of all tokens which means that all errors messages will be appropriately positioned by rustc. The crucial aspect of this error handling is ensuring that the Span information is not erased or forgotten from the input tokens, as the Span on each token is used to generate compiler diagnostics.

FAQ

The above describes a few high-level classes of errors and how gobject_gen! handles them, but there's also various questions about how other pieces work! Here's some common questions that may arise:

How does this all work?

All of this is fundamentally built on the concept of Span and the ability for a macro to expand to arbitrary tokens, including other macro invocations. A Span represents a pointer to a part of the code, and of the tokens in the original TokenStream are annotated with a span of where they came from. These Span objects are then used to set spans on the returned TokenStream or otherwise tokens may be preserved as-is in the output. By ensuring as many tokens as possible have correct Span information we can have the highest quality diagnostics from the compiler.

If an error actually happens then we'll bubble out an Err(error_list) all the way to the entry point of the macro. We still have to return a TokenStream though! To do this we convert the error_list to a TokenStream by iterating over each error and converting it to a TokenStream. The way syn::error::Error is converted to tokens looks like:


# #![allow(unused_variables)]
#fn main() {
compile_error!("your custom message");
#}

It generates an invocation of the compile_error! macro which is a way to produce custom error messages when compiling Rust code. This macro is defined by the Rust compiler.

By controlling the span information on each of these tokens (the compile_error identifier, the ! punctuation, and the ( ... ) group) we can control where the error message is pointed to. The implementation will adjust the span of each of these tokens generated to the tokens relevant to the error messages, causing rustc to produce a directed aerror message at the tokens we want.

Why are my errors pointing at the macro invocation?

The "default span" is created with Span::call_site() which represents the call-site of the macro, or the macro invocation. This Span is used by default for all tokens generated by quote! (liberally used to create TokenStream). If an error happens on tokens that point to Span::call_site() then the error will look like it comes from the macro invocation.

This typically happens when the macro itself generates invalid code. For example if you were to return quote! { let x: u32 = "foo"; } then that's a type error but the error message will point to the entire macro invocation (of gobject_gen!, not quote!) due to the usage of Span::call_site() on each of these tokens.

One helpful way to investigate these errors it to use the cargo expand subcommand. That subcommand will print out the output of the macro, allowing you to manually inspect the output or otherwise run it through rustc to figure out where the error is happening.