Should `.into()` disrupt coercion?

Currently, references are allowed to be coerced to pointers, and the docs for std::convert::Into says Into is reflexive, which means that Into<T> for T is implemented.

I'm running into an issue where calling .into() where self is a reference and T is a pointer, which should coerce, results in E0277.

struct Raw {
    data: *const u8
}

struct Safe<'a> {
    data: &'a u8
}

fn main() {
    let number = 14u8;
    let reference = &number;
    
    let safe = Safe { data: reference }; // compiles
    let raw = Raw { data: reference }; // compiles
    let safe2 = Safe { data: reference.into() }; // compiles
    let raw2 = Raw { data: reference.into() }; // error[E0277]: the trait bound `*const u8: From<&u8>` is not satisfied
}

I'm unsure where this should go in terms of category as I'm mostly just curious if this is intended behaviour and if not, what peoples thoughts are as to what should be intentional.

Coercions only occur at sites where the type of the expression and the type of the context are known to be different.

In this case, the expression reference.into() doesn’t have a known type (because it’s not known what the T in Into<T> should be) until it is inferred from the context — which then immediately fails because the implementation that would be required is not available. There is no opportunity to have different, coercible types.

It’s not relevant, in this case, that impl Into<T> for T exists, because the type inference on this code is never looking for a Into<&'a u8> impl.


Now, should this behavior be different? I think probably not. Type inference is already fragile in several ways, and allowing inference to include solutions that involve coercions would create many new possibilities and therefore new ambiguities.

Essentially, it would be problematic for the same reason that .into().into() never compiles: there isn’t a reasonable well-defined procedure by which to pick the intermediate type. But since coercion sites are implicit, you wouldn’t be able to avoid it the way you can avoid writing .into().into().

7 Likes

I guess my initial source of confusion was the compiler error being a bit misleading but now with hindsight that makes sense.

The compiler error is correct and straightforward in the narrow sense that it tells you exactly the only possible trait implementation that could be added to make this code compile.

But: what would you have wanted it to tell you instead, so that it is not misleading to you? This is not a rhetorical question; perspectives like yours are needed in order to make the error messages better.

5 Likes

I'm not a new user, but I do think a check for "would this work without the .into()" would be nice, for the same reason we have a clippy lint for unnecessary .into(). So a machine-applicable "suggestion: &u8 coerces to *const u8" that just removes the .into().

Given the T -> T case is covered by the reflexive impl, and coercion doesn't do auto(de)ref, we'd only need to check if a coercion Receiver -> Expected exists when a lookup for Into::into fails to resolve.

This should probably also suppress the help suggesting other impls if a coercion is found, since if a coercion exists, the other impls are highly unlikely to be relevant.


I could see impl From<&'_ T> for *const T being a reasonable impl, given we already have the coercion which can remove the lifetime without any syntactic marker (e.g. as present when using ptr::from_ref). But this would be a breaking blanket impl to add; since references are a fundamental type, you can write e.g.

pub struct Q;
impl From<&'_ Q> for *const Q {
    fn from(q: &Q) -> Self { q }
}
5 Likes

Funny you should mention that. This PR with a more discerning lint than clippy::useless_into is getting close to mergeable: `<T as Into<T>>::into` lint by estebank · Pull Request #129249 · rust-lang/rust · GitHub.

I have made some PRs in the past to detect unnecessary method calls on E0277 and suggest removing them, I think that it makes perfect sense to add logic to do that to this case. The case is just niche enough that I'd not seen it, but it 100% should be part of the error. The easiest way to do it is to use #[rustc_on_unimplemented(..)] on From, but might not be as good as what we can do "by hand".

1 Like

I'm not the OP either, but since it's been a minute and conversation is moving on, here's what I think is a more useful presentation for the coder.

error[E0277]: the trait bound `&u8: Into<*const u8>` is not satisfied
    --> src/main.rs:16:38
     |
  16 |     let raw2 = Raw { data: reference.into() }; // error[E0277]: the trait bound `*const u8: From<&u8>` is not satisfied
     |                                      ^^^^ the trait `Into<*const u8>` is not implemented for `&u8`
     |
     = note: trait bound `*const u8: From<&u8>` is also not satisfied (which
             would also result in `&u8` implementing `Into<*const u8>`)

help: '&u8' can coerce to '*const u8' without using `into`
       -    let raw2 = Raw { data: reference.into() };
       +    let raw2 = Raw { data: reference };

help: `u8` implements trait `Into<T>` for some `T`:
       u8: Into<u16>
       u8: Into<char>
       ... and however many others

For more information about this error, try `rustc --explain E0277`.

(The diff would be proper rustc diff style of course.)

And the rest of this comment explains how I got there.


The first thing I noticed is that 2/3rds of the error message is noise. u8 is From<bool> and From<AsciiChar>... okay? Defined in a macro in their foreign crate..... great I guess? But so what?[1]

The macro details can probably just be dropped if it's not in a local crate. When some From<_> details may be relevant or not is a harder call... but I think they're taking up an undue proportion of the space here. And they are in the middle of the more pertinent information,[2] not just strictly before or after.

And I'm also not actually sure why the diagnostics made a leap from "need *const u8: From<&u8>" to "here's some u8: From<T>". If it was because the method receiver is reference and *reference is a u8, it should have been telling me about u8: Into<T> -- or T: From<u8> -- instead. (The list in my revised error is based on that assumption.)

Also "help: u8 implements trait From<T>" makes it sound like there's an implementation for all T, not some select Ts.

Into<*const u8> for &u8 could also be added.[3]

Now, maybe that sounds like useless nitpicking in this case, where indeed From would be the correct choice... for whomever can implement it (in this case, the coder cannot). But there is a wider issue where error messages / suggestions prefer to tell you about the blanket implementation at the end of some chain, instead of the direct bound / trait the code needed (#22590, #87272).

I feel this particular message would have been clearer if it started talking about Into (the trait which was actually trying to be invoked) and then making the connection with From, instead of immediately talking about From while highlighting .into().


And as was already discussed, the error doesn't suggest just using coercion yet, but should.


  1. Also, AsciiChar is unstable. As are the const details of the trait implementations. ↩︎

  2. the note about the unmet trait bound gets separated from the highlighted error ↩︎

  3. The phrase "required for &u8 to implement Into<*const u8>" is counterfactual. It's required for the blanket implementation to apply, but that is not the only way for the implementation to exist. ↩︎

You've essentially outlined my issues with that error message better than I could have.

Overall I feel like rust errors do a good job of teaching you, however this one feels like it requires a lot of prior knowledge that makes it less helpful than it could be.

1 Like