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.