Diagnostic namespace

Over the course of the last year I've worked on ways to improve compiler error messages emitted by trait heavy crates. This work was funded by the Rust Foundation. I would like to thank the Rust Foundation for providing the opportunity to work on this project. This blog post summarises the progress made in the last months.

Problem description

The rust compiler has the reputation to generate helpful error messages when something goes wrong. Nevertheless there are always cases of error messages that can be improved. One common example of such error messages in the rust ecosystem are those error messages that are generated by crates using the type system to verify certain invariants at compile time. While these crates provide additional guarantees about these invariants, they sometimes generate large incomprehensible error messages when such an invariant is violated. These error messages do not always indicate clearly what went wrong. Well known examples of crates with such issues include axum or diesel.

Consider the following example cases:

Axum

async fn handler(foo: bool) {}

fn main() {
    axum::Router::new().route("/", axum::routing::get(handler));
}

Output:

error[E0277]: the trait bound `fn(bool) -> impl Future<Output = ()> {handler}: Handler<_, _, _>` is not satisfied
   --> src/main.rs:9:55
    |
9   |     axum::Router::new().route("/", axum::routing::get(handler));
    |                                    ------------------ ^^^^^^^ the trait `Handler<_, _, _>` is not implemented for fn item `fn(bool) -> impl Future<Output = ()> {handler}`
    |                                    |
    |                                    required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S, B>`:
              <Layered<L, H, T, S, B, B2> as Handler<T, S, B2>>
              <MethodRouter<S, B> as Handler<(), S, B>>

In this case the user tried to use some function argument type in the handler function that does not satisfy a required trait bound. This means the argument cannot be extracted from a given http Request.

Diesel

use diesel::*;

table! {
    users {
        id -> Integer,
        name -> Text,
        age -> Nullable<Integer>,
    }
}

fn ferris_age(conn: &mut PgConnection) -> QueryResult<()> {
    let age: i32 = users::table
        .filter(users::name.eq("ferris"))
        .select(users::age)
        .get_result(conn)?;
    Ok(())
}

Output:

error[E0277]: the trait bound `i32: FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>` is not satisfied
    --> src/main.rs:20:21
     |
20   |         .get_result(conn)?;
     |          ---------- ^^^^ the trait `FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>` is not implemented for `i32`
     |          |
     |          required by a bound introduced by this call
     |
     = help: the trait `FromSql<diesel::sql_types::Integer, Pg>` is implemented for `i32`
     = note: required for `i32` to implement `Queryable<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>`
     = note: required for `i32` to implement `FromSqlRow<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>`
     = note: required for `diesel::sql_types::Nullable<diesel::sql_types::Integer>` to implement `load_dsl::private::CompatibleType<i32, Pg>`
     = note: required for `SelectStatement<FromClause<table>, SelectClause<age>, NoDistinctClause, WhereClause<Grouped<Eq<name, ...>>>>` to implement `LoadQuery<'_, diesel::PgConnection, i32>`
     = note: the full type name has been written to '/home/explorer/playground/target/debug/deps/playground-e6a51ee2dd30ed1c.long-type-17474038542738457663.txt'
note: required by a bound in `get_result`
    --> /home/explorer/.cargo/registry/src/index.crates.io-6f17d22bba15001f/diesel-2.1.1/src/query_dsl/mod.rs:1723:15
     |
1721 |     fn get_result<'query, U>(self, conn: &mut Conn) -> QueryResult<U>
     |        ---------- required by a bound in this associated function
1722 |     where
1723 |         Self: LoadQuery<'query, Conn, U>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RunQueryDsl::get_result`

In this case the user tried to load a Nullable<Integer> value from the database into a plain i32. The rust side type cannot represent a possible nullable value, therefore this mapping is disallowed.

Proposed solution

Such error messages usually have a pretty clear cause. Crate maintainers and seasoned users of these crates can identify the underlying problem easily and help new users by pointing into the right direction. For all cases presented above the underlying issue is an unimplemented trait for some complex type. It might help users in some cases if the complex type error is replaced by a simpler message or if the existing error is extended by additional notes that point the helpful resources.

The rust compiler already has existing tooling to change error messages for unimplemented traits as part of the standard library implementation. This attribute is used quite extensively in rusts standard library to improve for example error messages around the Iterator trait. This attribute is currently considered to be rustc internal, which means it is not usable on stable rust by crates other than the standard library and there is no path for a potential stabilisation of this attribute. To enable the usage of this feature by other crate I've proposed to introduce a new tool attribute namespace, which allows to control diagnostics emitted by the compiler.

The idea is to have a set of attributes that allow to specify additions/changes to the error message emitted for certain error conditions, like for example an unimplemented trait error. Such attributes are threaded as hints by the compiler, which means the compiler can use them if it seems to be fitting. Such a mechanism is tightly coupled to how the compiler emits error messages and therefore it might for the compiler at some point not be possible to support a certain attribute of this namespace completely. As a consequence the compiler is explicitly allowed to ignore these attributes. The proposal is written in a more general way than the existing attribute to allow adding more attributes at a later point in time. The corresponding RFC for the diagnostic attribute namespace is now accepted.

Implementation

After the RFC was accepted I've started working on an implementation. This resulted in landing two PR's to the rust compiler. The first one adds support for the general namespace setup. It required to make sure that adding such a new namespace does not break existing proc-macro attributes which use the same path (i.e. #[diagnostic::some_macro] reexported from a local diagnostic module). The second PR adds support for the first attribute #[diagnostic::on_unimplemented]. This attribute closely mirrors parts of the existing #[rustc_on_unimplemented] attribute. It also reuses the existing implementation, by just adding another way to construct the necessary types. Both changes are now accepted into rustc and can be used with a nightly rust compiler.

Usage

As a final step I want to demonstrate how the error messages of our motivating examples are improved by using this new feature. For this I've modified both crates to include the new #[diagnostic::on_unimplemented] attribute in the relevant source code locations. The relevant changes are merged to axum and diesel

Axum

async fn handler(foo: bool) {}

fn main() {
    axum::Router::new().route("/", axum::routing::get(handler));
}

Output:

error[E0277]: the trait bound `fn(bool) -> impl Future<Output = ()> {handler}: Handler<_, _, _>` is not satisfied
   --> src/main.rs:9:55
    |
9   |     axum::Router::new().route("/", axum::routing::get(handler));
    |                                    ------------------ ^^^^^^^ the trait `Handler<_, _, _>` is not implemented for fn item `fn(bool) -> impl Future<Output = ()> {handler}`
    |                                    |
    |                                    required by a bound introduced by this call
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message
    = help: the following other types implement trait `Handler<T, S, B>`:
              <Layered<L, H, T, S, B, B2> as Handler<T, S, B2>>
              <MethodRouter<S, B> as Handler<(), S, B>>

For axum the error message changes only slightly. The new error message includes an additional note that points to an existing macro provided by axum to improve error messages for this case. This macro works by generating some additional static code that checks certain invariants. Adding information about the existence of this proc-macro attribute to the error is considered helpful as users might not be aware about this tool.

By applying the suggested attribute we are able to get a much better error message.

#[axum::debug_handler]
async fn handler(foo: bool) {}

fn main() {
    axum::Router::new().route("/", axum::routing::get(handler));
}

Output:

error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
 --> src/main.rs:7:23
  |
7 | async fn handler(foo: bool) {}
  |                       ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`
  |
  = note: Function argument is not a valid axum extractor. 
          See `https://docs.rs/axum/latest/axum/extract/index.html` for details
  = help: the following other types implement trait `FromRequestParts<S>`:
            <HeaderMap as FromRequestParts<S>>
            <Extension<T> as FromRequestParts<S>>
            <Method as FromRequestParts<S>>
            <Uri as FromRequestParts<S>>
            <Version as FromRequestParts<S>>
            <ConnectInfo<T> as FromRequestParts<S>>
            <axum::extract::Path<T> as FromRequestParts<S>>
            <RawPathParams as FromRequestParts<S>>
          and 25 others
  = note: required for `bool` to implement `FromRequest<(), Body, axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
 --> src/main.rs:7:23
  |
7 | async fn handler(foo: bool) {}
  |                       ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`

The error message now points to the relevant problem. It also includes a link where to get additional information by using the new #[diagnostic::on_unimplemented] attribute.

Diesel

use diesel::*;

table! {
    users {
        id -> Integer,
        name -> Text,
        age -> Nullable<Integer>,
    }
}

fn ferris_age(conn: &mut PgConnection) -> QueryResult<()> {
    let age: i32 = users::table
        .filter(users::name.eq("ferris"))
        .select(users::age)
        .get_result(conn)?;
    Ok(())
}

Output:

error[E0277]: Cannot deserialize a value of the database type `diesel::sql_types::Nullable<diesel::sql_types::Integer>` as `i32`
    --> src/main.rs:17:21
     |
17   |         .get_result(conn)?;
     |          ---------- ^^^^ the trait `FromSql<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>` is not implemented for `i32`
     |          |
     |          required by a bound introduced by this call
     |
     = note: Double check your type mappings via the documentation of `diesel::sql_types::Nullable<diesel::sql_types::Integer>`
     = help: the trait `FromSql<diesel::sql_types::Integer, Pg>` is implemented for `i32`
     = help: for that trait implementation, expected `diesel::sql_types::Integer`, found `diesel::sql_types::Nullable<diesel::sql_types::Integer>`
     = note: required for `i32` to implement `Queryable<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>`
     = note: required for `i32` to implement `FromSqlRow<diesel::sql_types::Nullable<diesel::sql_types::Integer>, Pg>`
     = note: required for `diesel::sql_types::Nullable<diesel::sql_types::Integer>` to implement `load_dsl::private::CompatibleType<i32, Pg>`
     = note: required for `SelectStatement<FromClause<table>, SelectClause<age>, NoDistinctClause, WhereClause<...>>` to implement `LoadQuery<'_, diesel::PgConnection, i32>`
     = note: the full type name has been written to '/tmp/diesel-test/target/debug/deps/diesel_test-6cd0cfa86ee3e6fb.long-type-15267316502817571224.txt'
note: required by a bound in `get_result`
    --> /home/weiznich/.cargo/git/checkouts/diesel-6e3331fb3b9331ec/0377c25/diesel/src/query_dsl/mod.rs:1722:15
     |
1720 |     fn get_result<'query, U>(self, conn: &mut Conn) -> QueryResult<U>
     |        ---------- required by a bound in this associated function
1721 |     where
1722 |         Self: LoadQuery<'query, Conn, U>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RunQueryDsl::get_result`

For diesel the improved error message now points out the underlying problem. It also includes a note that points to additional information about allowed type mappings. Overall this change hopefully enables users to find solutions for their compiler errors faster.

Next Steps

The #[diagnostic::on_unimplemented] attribute is now available on nightly rust compilers. In addition axum and diesel have merged patches, which allow to use this attribute behind a feature flag.

The next step in bringing this feature forward is getting some real world testing of the implementation.

You can help:

  • If you are a user of one of the mentioned crates, consider using the relevant feature in your development workflow.
  • If you are a maintainer of a crate that suffers from similar error messages you can incorporate the new attribute to improve the error messages.

You can support future developments by sponsoring me on GitHub