Better user-definable error messages

So I've done a little research and come upon the following:

  1. rustc_on_unimplemented - this is an attribute that lets the error when using an unimplemented trait to be tweaked by the trait definition itself. As implied by the rustc_ prefix it's meant for compiler use only.
  2. A way to customize Rust error messages - this post suggests writing a tool to parse Rust error messages and then emit new, more helpful and library specific error messages.

After thinking about this for a bit, I have the following thoughts:

  • The scenario that first prompted down this rabbit hole was figuring out if there was a way to generate better error messages for type safe builder methods. Type safe builders make heavy use of generics + structs, and most methods for the structs are limited to certain combinations of type parameters. If we extended rustc_on_unimplemented to work with impl blocks of structs, not just trait definitions, that'd fill this use case (provided we stabilized it too, which would be a lot of work) - I could customize the unimplemented error for the setters to say that the value was already set.
  • Besides type safe builders, the other places where errors fall flat are things with complex type signature mismatches, and it seem like rustc_on_implemented isn't quite powerful enough to give truly nice error messages - the diesel example from the referenced blog post is a good example.

Is there any work being done on pursuing these ideas? It seems like any way to trigger stable emissions of full type signatures for method not implemented for type xxx because of bound yyy with full type paths in the error information would be a good start.

1 Like

There certainly is a desire to cater to crate writers and allow customization of error messages. At the same time, I've been spending a lot of time trying to improve the generic errors themselves so that even if the crate writers don't use the (currently nonexistent) customization features, users will still encounter reasonable compiler output. rustc_on_unimplemented has proven to be pretty flexible, but there's a desire to not stabilize in waiting for something nicer.

Could you share some code for the type-safe builder? I would like to see what the current output is and see if there are generic things we could immediately do to improve that use case, particularly because it sounds like they aren't E0277 errors.

I'm really excited to see improvements in this area.

2 Likes

Here's a minimal type safe builder that:

  1. Doesn't allow a setter to be called twice.
  2. Doesn't allow the build method to be called until both setters have been called.

The main method has two intentional compile errors.

pub struct Builder<T, A> {
    t: T,
    a: A,
}

pub struct Unset;

impl Builder<Unset, Unset> {
    pub fn new() -> Builder<Unset, Unset> {
        Builder { a: Unset, t: Unset }
    }
}

impl <T> Builder<T, Unset> {
    pub fn set_a<A>(self, a: A) -> Builder<T,A> {
        Builder {
            t: self.t,
            a
        }
    }
}

impl <A> Builder<Unset, A> {
    pub fn set_t<T>(self, t: T) -> Builder<T,A> {
        Builder {
            t,
            a: self.a
        }
    }
}

impl Builder<u32, u32> {
    pub fn build(self) -> (u32, u32) {
        (self.a, self.t)
    }
}

pub fn main(){
    Builder::new()
        .set_a(11u32)
        .set_t(12u32)
        // set_a doesn't exist because we called set_a once already
        .set_a(13u32);
        
    Builder::new()
        .set_a(11u32)
        // we need to call set_t for `.build()` to exist
        .build();
}

Here are the errors:

   Compiling playground v0.0.1 (/playground)
error[E0599]: no method named `set_a` found for struct `Builder<u32, u32>` in the current scope
  --> src/main.rs:42:10
   |
1  | pub struct Builder<T, A> {
   | ------------------------ method `set_a` not found for this
...
42 |         .set_a(13u32);
   |          ^^^^^ method not found in `Builder<u32, u32>`

error[E0599]: no method named `build` found for struct `Builder<Unset, u32>` in the current scope
  --> src/main.rs:44:33
   |
1  | pub struct Builder<T, A> {
   | ------------------------ method `build` not found for this
...
44 |     Builder::new().set_a(11u32).build();
   |                                 ^^^^^ method not found in `Builder<Unset, u32>`

error: aborting due to 2 previous errors

The first compile seems like it be pretty easy to fix. If you try to call a method that doesn't exist, but was callable at some point in the chain, then point out at what point in the chain the call existed, and what call made it so the method was unusable. In this instance set_a disappeared when set_a was called. I could see an error help like this being useful in a chain of transformations on an option type for example - like if someone tried calling map after calling unwrap.

The second one is a bit harder, but I definitely think it'd be nice to call out what generic parameters need to change in order for the build method to exist - in this case it was the T bound on the struct.

4 Likes

Yeah, it seems like most of the time things that would be helped by simple customization things like rustc_on_unimplemented would be fixable through better error messages. And many have been; thanks for all your work in this area! I understand the desire to wait for something nicer since the biggest ROI for custom error messages will be in the complex cases which rustc_on_unimplemented isn't well suited for.

2 Likes

Looking at the examples, I think there are things we can do that would help for the general case. The idea of checking the types of different parts in a chain of method calls/field accesses in an expression containing the current failure is quite good. Quickly mocking something up, I think something like the following could work well:

error[E0599]: no method named `set_a` found for struct `Builder<u32, u32>` in the current scope
  --> src/main.rs:42:10
   |
1  |   pub struct Builder<T, A> {
   |   ------------------------ method `set_a` not found for this
...
39 | /     Builder::new()
   | |     -------------- method `set_a` is found for `Builder<_, Unset>`
40 | |         .set_a(11u32)
41 | |         .set_t(12u32)
   | |_____________________- this expression is of type `Builder<u32, u32>`
42 |           .set_a(13u32);
   |            ^^^^^ method not found in `Builder<u32, u32>`

error[E0599]: no method named `build` found for struct `Builder<Unset, u32>` in the current scope
  --> src/main.rs:44:33
   |
1  | pub struct Builder<T, A> {
   | ------------------------ method `build` not found for this
...
44 |     Builder::new().set_a(11u32).build();
   |                                 ^^^^^ method not found in `Builder<Unset, u32>`
note: method `build` would be found for `Builder<u32, u32>`
   |
XX | impl Builder<u32, u32> {
   |      ----------------- this type provides a `build` method
XX |     pub fn build(self) -> (u32, u32) {
   |            ^^^^^ method provided here
4 Likes