Commit 8c6ac0bc authored by Alex Crichton's avatar Alex Crichton

Add internal documentation about errors

This adds documentation about how various classes of errors are handled
in `gobject_gen!`, hopefully providing useful context to those who
wonder how it all fits together!
parent ca5563fb
......@@ -5,5 +5,6 @@
- [Parsing into the Abstract Syntax Tree](./parsing.md)
- [High-level Internal Representation](./hir.md)
- [Type conversions between Rust and Glib](./types.md)
- [Error and Diagnostic Handling](./errors.md)
- [Testing strategy](./testing.md)
- [Glossary](./glossary.md)
# 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:
```rust
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!`][failure]). 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:
```rust
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.
[failure]: https://docs.rs/failure/0.1.3/failure/macro.bail.html
[`cargo expand`]: https://crates.io/crates/cargo-expand
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment