Compiler diagnostics improvement wishlist

TL;DR: add any of your compiler error pet peeves as a response to this thread.

In the wish of improving the ergonomics of the language there’re, in my opinion, four big pillars:

  • Language features: does this code that makes sense in my mind work without/with minor changes?
  • Documentation: can I find the answers to my problems easily in an understandable way?
  • API: does it map to my mental model of the problem and is it composable?
  • Diagnostics: my code doesn’t work, does rustc tell me why in such a way that I can understand how to fix it?

Personally, I have been focusing on incrementally improving the last item. There’re already plenty of things that we’re aware needs to be improved, but I would I would like to see what people in the larger community would like to see improved. If you have any particularly unreadable, hard to understand or just plain wrong error messages the compiler has thrown at you, please add it to this thread, even if you know there’s already a ticket for it. My intention is to use the replies here as a signal to prioritize and extend the existing backlog.

6 Likes

The language should definitely be very strict with types and not do any implicit conversions. But would it be possible, in the case of a type error, for the compiler to try to understand the intended result by searching for nearby intermediaries? The closest match could be suggested along with the error message. “Did you mean…?”

This could also be applied to situations of using the wrong number of dereferences, etc.

1 Like

The other day I got some error about various math operations not being implemented for {integer} and float. I have a vague idea what was going on, but he error message alone was not enough to figure out what was happening.

I don’t have the exact error message handy, but the code that generated it was something along these lines:

let count = 25;
println!("{}", 300 * 1.5 / count);
2 Likes

Borrow checker errors often throw around esoteric terms like bound lifetime, concrete lifetime, free region, etc which have little or no meaning to most people (at least block suffix is gone though!). I imagine this could all be changing with NLL/MIR borrowck, but seemed worth mentioning.

3 Likes

Giving tons of error messages due to the same problem is not helpful. One case where the rust compiler gives way too many error messages is in the case of incorrect close delimeters. For instance, take the following code:

fn main() {
    let a: Vec<u64> = { 
        let mut b: Vec<u64> = (0..10)
            .map(|n| {
                let square = n * n;
                square + 1 
            }}) // Error
            .collect();
        b.push(1);
        b   
    };  
    println!("{:?}", a);
}

There is one } too many on the line marked “Error”. The rest of the code is fine. The compiler spits out three error messages, two of them with notes. This means that the error I actually need to respond to has scrolled off the top of my terminal, and all I see are two useless error messages.

In general, the compiler should stop outputting error messages pass the point where the error is likely to have occurred. If the user wants to see all possible error messages, a flag would make more sense, rather than making it the default. In the other direction, a flag to only output the single error most likely to be actionable would be extremely helpful. It also might help make the error messages easier to use for newer rust users, as it would less necessary to learn to ignore specious error messages.

7 Likes

I am not a heavy macro user, so it always takes me a while to wrap my mind around the interactions between macros and scoping. For example, I recently scratched my head on an elaborate variation of this mistake:

macro_rules! scoped {
    ($GenericType:ident) => {
        mod inner {
            type BoolInstance = $GenericType<bool>;
        }
    }
}

/* ...somewhere in a different code module... */

type MyVec<T> = Vec<T>;
scoped!(MyVec);

Which produces the following error message:

error[E0412]: cannot find type `MyVec` in this scope
  --> src/main.rs:10:9
   |
10 | scoped!(MyVec);
   |         ^^^^^ not found in this scope

error: aborting due to previous error

Most likely a heavier macro user would have instantly figured out that a super:: is needed before the $GenericType in the macro definition. But in this particular case, I found myself wishing that the compiler could point out that the error is in the expanded code, and not in the way I call the macro.

After all, a manually inlined variant of the above…

type MyVec<T> = Vec<T>;

mod inner {
    type BoolInstance = MyVec<bool>;
}

…produces a much more helpful error message:

error[E0412]: cannot find type `MyVec` in this scope
 --> src/main.rs:4:25
  |
4 |     type BoolInstance = MyVec<bool>;
  |                         ^^^^^ not found in this scope
help: possible candidate is found in another module, you can import it into scope
  |
4 |     use MyVec;
  |

error: aborting due to previous error

So I guess my wish would be that when there is a problem in a macro instantiation, rustc could at least display both the macro call site and the expanded code. As an example of prior art, GCC does a variant of the above for C/++ macros ("<points out error in expanded code> ...in expansion of macro... <points out macro call site>").

1 Like

The other day I tried to use to_owned() on a reference. I was surprised to find that the result wasn’t an owned, cloned copy, but another reference, which caused a type mismatch. The problem was that the struct didn’t implement Clone. I think that to_owned() returning a non-owned thing is surprising and there should be a help message that explains that you should derive Clone for that struct for it to work.

5 Likes

E0282 “type annotations needed” sometimes offers too little context. For example:

fn main() { Err(1); }

Leads to:

error[E0282]: type annotations needed
 --> asdf.rs:1:13
  |
1 | fn main() { Ok('x'); }
  |             ^^ cannot infer type for `E`

But what is this E? The program certainly doesn’t mention it. Okay, I can check the documentation for Result and see that it’s Result<T, E> and make the connection between that E and the one in the error message, but I think that’s asking a bit much.

Here’s a more confusing case:

fn main() { (|| { Err('x')?; Ok(()) })(); }

Now we have:

error[E0282]: type annotations needed
 --> asdf.rs:1:19
  |
1 | fn main() { (|| { Err('x')?; Ok(()) })(); }
  |                   --------
  |                   |
  |                   cannot infer type for `_`
  |                   in this macro invocation

I honestly don’t know where _ comes from here.

Cases like the above are not that problematic, after all we were just dealing with Result and some simple expressions. But in general, with more complicated interactions and generic types from less familiar crates than std, it can be tricky to see what exactly the compiler cannot infer.

Concretely, I think this could benefit from something like a note saying could only infer Result<char, E> where E is still generic.

10 Likes

To expand on this: when I run cargo build in a terminal I always scroll all the way to the top to see the first error messages because I don't trust anything else to be relevant. I would like it if there was a way to limit the number of errors through a global config setting (so I don't have to scroll to the top), but it would be just as good if rustc was very strict about not any emitting any errors that could possibly have been caused by an earlier error.

5 Likes

expected type parameter, found (something else)” is my personal favorite. The error message is actually not bad in isolation, but this:

fn func<T, U>(t: T) -> U {
    t
}

results in:

error[E0308]: mismatched types
 --> file.rs:2:5
  |
1 | fn func<T, U>(t: T) -> U {
  |                        - expected `U` because of return type
2 |     t
  |     ^ expected type parameter, found a different type parameter
  |
  = note: expected type `U`
             found type `T`

And this is confusing because syntactically, at the indicated place, you cannot use a type parameter at all.

3 Likes

The first module reform thread had a great user story about how missing help/suggestions make learning module system harder than it needs to be:

I think all the items in that report should be covered by diagnostics.

3 Likes

Smarter handling of code broken into multiple lines. For example:

error[E0061]: this function takes 3 parameters but 2 parameters were supplied
   --> src/a/mod.rs:39:18
    |
39  |                   .function(one, two)
    |                    ^^^^^^^^ expected 3 parameters
    | 
   ::: src/b/mod.rs
    |
88  | /     pub fn function(
89  | |         self: &mut Self,
90  | |         p_one: Something,
91  | |         p_two: Something,
...   |            // missing (3rd) parameter is outside context
104 | |         }
105 | |     }
    | |_____- defined here

Here, the context for the error doesn’t go far enough to show the actual 3rd parameter that is missing.

Newcomers to the language might also see 3 parameters in the error (self, p_one, p_two) and wonder why this thing is now requiring us to pass Self explicitly (it isn’t).

It gets a bit better if the function declaration is in a single line. It would be cool if the actual missing parameters were underlined, instead of just pointing to the whole declaration:

88 |     pub fn function(self: &mut Self, p_one: Something, p_two: Something, p_three: Something) -> Result<Something, ()> {
   |     ------------------------------------------------------------------------------------------------------------- defined here
2 Likes

cargo test stutters.

I suspect that the issue is that when executing cargo test the compiler is invoked twice: once in the regular mode, and once with the test feature defined (to actually compile the tests).

The end result, however, is that any error/warning in the “regular” mode is outputted twice. It’s not the end of the world, but I’ve never liked spamming :slight_smile:

8 Likes

I strongly, strongly want to redesign the borrow checker error messages “from scratch” to use a lot less jargon. e.g., I think we only really need to talk about “borrow”, “read” and “write”. Words like lifetime etc aren’t needed. I’ve been meaning to write up a proposal on this at some point.

10 Likes

I don’t have any concrete examples right now, but over the last year or so, the worst error messages that we regularly encounter at work are those involving diesel. We get tons of references to semi-internal traits, and also error messages containing type expressions like (((((((((((((((((((_))))))))))))))))))), except there are maybe three screens of nested parens.

Clearly, some of these error messages are caused by the specific design of the diesel crate, but diesel is far and away the best option we’ve found for type-checked SQL. I’m just brainstorming here, but maybe Rust could somehow provide more tools to library authors so that they could help guide the compiler towards generating more comprehensible error messages?

If I run into any specific examples in the immediate future, I’ll try to post them here.

4 Likes

Maybe automatically pipe output through less or more when the number of errors is above a certain threshold.

One example that really sucks is when you have a call site where two different versions of the same trait are being referenced (one as required, the other as implemented). The compiler doesn’t understand what’s happening, and just tells you that two traits that are absolutely identical are incompatible.

See also https://github.com/rust-lang/rust/issues/44798 (Better error message for “[E0601]: main function not found”) and https://github.com/rust-lang/rust/issues/44695 (Ergonomics: &String does not implement PartialEq).

2 Likes

I’d say this is 100% clear and I’m not a Rust programmer. This says, “You’ve declared that this function returns something of type U, but, you are returning t which is of type T”.

1 Like

Well I do, really. But It's annoying to type cargo test --color=always 2>&1 | head -n30 rather than just cargo test

Edit: Oh, automatically. Yeah, that's a good idea. I didn't read your comment properly the first time.

Syntax errors (previously noted in this thread) are annoying because they cause a cascade of unrelated failures.

https://github.com/rust-lang/rust/issues/32577 is a fun one where the borrow error message points to the wrong borrow.

https://github.com/rust-lang/rust/issues/42984 had me scratching my head for a while. I suppose my issue was that I didn’t understand (and still only vaguely have an idea) of what was causing the borrow of buffer to last for so long (note that changing from a trait object to a struct fixes it). I haven’t thought about it too much, but something like a ‘breakdown’ mode where the compiler explains the decisions it’s made about a lifetime might be useful?

1 Like