Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler errors for auto traits can be very unhelpful, especially when combined with min_specialization. #90601

Closed
BGR360 opened this issue Nov 5, 2021 · 2 comments
Labels
A-diagnostics Area: Messages for errors, warnings, and lints F-auto_traits `#![feature(auto_traits)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@BGR360
Copy link
Contributor

BGR360 commented Nov 5, 2021

I have here a collection of scenarios where the compiler fails to produce a helpful error message in response to a missing auto trait implementation. All examples are done on 1.58.0-nightly.

Why do I care?

This issue cost me the entirety of an afternoon as I attempted to track down the cause of a very vague error. I've provided a minimal repro of that particular scenario in Example 4. Figuring out that it was due to a missing auto trait implementation was... not fun.

The root of the problem is that a type only a receives an auto trait implementation if the entire recursive closure of types that make it up implements the trait. If there is some unknown (to the user) negative impl somewhere deep inside that recursive closure of types, or (as was the case for me) there is some Box<T: !Sized>1 hidden somewhere in there, then the compiler needs to point out the offending type to the user, or otherwise leave them baffled as to what's going on.

Example 1: Good

Below is an example where the compiler does give a diagnostic that I find satisfactory:

Code
#![feature(auto_traits)]

auto trait Something {}

fn destroy<T: Something>(_: T) {
    println!("Destroyed something!");
}

struct Stringy(Box<dyn ToString>);

fn main() {
    destroy("hello");
    destroy(Stringy(Box::new("hello")));
}

The compiler correctly identifies the offending type, dyn ToString:

Compile error
error[E0277]: the trait bound `(dyn ToString + 'static): Something` is not satisfied in `Stringy`
  --> src/main.rs:16:13
   |
16 |     destroy(Stringy(Box::new("hello")));
   |     ------- ^^^^^^^^^^^^^^^^^^^^^^^^^^ within `Stringy`, the trait `Something` is not implemented for `(dyn ToString + 'static)`
   |     |
   |     required by a bound introduced by this call
   |
   = note: required because it appears within the type `*const (dyn ToString + 'static)`
   = note: required because it appears within the type `Unique<(dyn ToString + 'static)>`
   = note: required because it appears within the type `Box<(dyn ToString + 'static)>`
note: required because it appears within the type `Stringy`
  --> src/main.rs:12:8
   |
12 | struct Stringy(Box<dyn ToString>);
   |        ^^^^^^^
note: required by a bound in `destroy`
  --> src/main.rs:5:15
   |
5  | fn destroy<T: Something>(_: T) {
   |               ^^^^^^^^^ required by this bound in `destroy`

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=3b256c12fe5979c5372ed94c3b4f939a

Example 2: What if it was a trait?

This example is like Example 1 but I've turned the destroy function into a trait, Destroy:

Code
#![feature(auto_traits)]

auto trait Something {}

trait Destroy {
    fn destroy(self);
}

impl<T: Something> Destroy for T {
    fn destroy(self) {
        println!("Destroyed something!");
    }
}

struct Stringy(Box<dyn ToString>);

fn main() {
    "hello".destroy();
    Stringy(Box::new("hello")).destroy();
}

With this little change, suddenly the compiler has lost all context regarding dyn ToString:

Compile error
error[E0599]: the method `destroy` exists for struct `Stringy`, but its trait bounds were not satisfied
  --> src/main.rs:25:32
   |
21 | struct Stringy(Box<dyn ToString>);
   | ----------------------------------
   | |
   | method `destroy` not found for this
   | doesn't satisfy `Stringy: Destroy`
   | doesn't satisfy `Stringy: Something`
...
25 |     Stringy(Box::new("hello")).destroy();
   |                                ^^^^^^^ method cannot be called on `Stringy` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `Stringy: Something`
           which is required by `Stringy: Destroy`
           `&Stringy: Something`
           which is required by `&Stringy: Destroy`
           `&mut Stringy: Something`
           which is required by `&mut Stringy: Destroy`
note: the following trait must be implemented
  --> src/main.rs:5:1
   |
5  | auto trait Something {}
   | ^^^^^^^^^^^^^^^^^^^^^^^

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=72b138ae232c87f5b34e9d96f3aff0bd

Example 3: With specialization

This expands on Example 2 by adding a specialized implementation of Destroy using feature(min_specialization):

Code
#![feature(auto_traits)]
#![feature(min_specialization)]
#![feature(rustc_attrs)]

auto trait Something {}

trait Destroy {
    fn destroy(self);
}

impl<T: Something> Destroy for T {
    default fn destroy(self) {
        println!("Destroyed something!");
    }
}

#[rustc_specialization_trait]
trait Special {}

impl<T: Something + Special> Destroy for T {
    fn destroy(self) {
        println!("Destroyed something special!");
    }
}

struct SpecialSomething(Box<dyn ToString>);

impl Special for SpecialSomething {}

fn main() {
    "hello".destroy();
    SpecialSomething(Box::new("hello")).destroy();
}

The error message is even less helpful!

Compile error
error[E0599]: no method named `destroy` found for struct `SpecialSomething` in the current scope
  --> src/main.rs:36:41
   |
31 | struct SpecialSomething(Box<dyn ToString>);
   | ------------------------------------------- method `destroy` not found for this
...
36 |     SpecialSomething(Box::new("hello")).destroy();
   |                                         ^^^^^^^ method not found in `SpecialSomething`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
note: `Destroy` defines an item `destroy`, perhaps you need to implement it
  --> src/main.rs:7:1
   |
7  | trait Destroy {
   | ^^^^^^^^^^^^^

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=bc60361c1dbdcb0a26c128965def0239

Example 4: Specialization and Result

This is a representative example of what I was working on when I encountered these unhelpful errors:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=06c83fd106d7b7f9ac3b489a18459099

My project involves modifying core::result::Result to have specialized FromResidual behavior for error types that implement a certain trait. I then have a wrapper type in "user space" that wraps some arbitrary error type and implements the specialization trait.

To enable ?-conversion from one wrapper to another, I provide a blanket From impl. I utilize auto traits and negative impls to ensure that it does not collide with the blanket From<T> for T from the standard library.

If the wrapped error type does not receive the NotSame auto trait, then the blanket From impl fails to apply to it, which in turn makes the FromResidual impl fail to apply, and I get the error below:

Compile error
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
   --> src/main.rs:112:13
    |
111 | / fn foo() -> Result<(), Special<ErrorA>> {
112 | |     Ok(bar()?)
    | |             ^ cannot use the `?` operator in a function that returns `Result<(), Special<ErrorA>>`
113 | | }
    | |_- this function should return `Result` or `Option` to accept `?`
    |
    = help: the trait `FromResidual<Result<Infallible, Special<ErrorB>>>` is not implemented for `Result<(), Special<ErrorA>>`
note: required by `from_residual`

This error message is even less helpful than the last, as it doesn't even mention any trait or type that is remotely related to my actual test code!


A few things seem clear here:

  1. The act of adding a specialized trait impl greatly degrades the compiler's ability to forward information about unimplemented auto traits.
  2. Even without specialization, the error messaging can be subpar.

I'd be happy to take a stab at improving things here, but I do not know what part of the compiler is responsible for doing this sort of inference (if that's even the correct term to use).

cc #13231 #31844 #68970
maybe also cc #84277

Footnotes

  1. I will say that it's remarkably unintuitive that non-sized types do not receive auto traits by default. I only ended up learning this when I stumbled across this code. There is no mention of this peculiarity in the Unstable Book.

@BGR360 BGR360 added A-diagnostics Area: Messages for errors, warnings, and lints T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Nov 5, 2021
@BGR360
Copy link
Contributor Author

BGR360 commented Nov 5, 2021

@rustbot label +F-auto_traits

@BGR360
Copy link
Contributor Author

BGR360 commented Nov 7, 2021

After spending some time with the rustc code, I've realized that there are definitely two different things going on here. The first is a byproduct of fully-qualifying vs. not fully-qualifying a trait method, and the second is related to specialization.

As such, I've split this issue off into two others.

@BGR360 BGR360 closed this as completed Nov 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-diagnostics Area: Messages for errors, warnings, and lints F-auto_traits `#![feature(auto_traits)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

2 participants