Deref coercion behaves differently for Sized vs !Sized targets

In the following playground, the line coercing Bar to BarRef works (where BarRef is !Sized), but the line coercing Foo to FooRef fails. Is there a reason for this inconsistency, and is there a way to get this to work for Foo too?

let foo: &FooRef = &make_foo()?;
let bar: &BarRef = &make_bar()?;

The real use case involves the foreign_types crate, whose types follow this pattern.

1 Like

The problem here seems to be with type inference. Desugaring the ? gives something like

let foo: &FooRef = &match Try::branch(make_foo()) {
    ControlFlow::Continue(v) => v,
    ControlFlow::Break(r) => return FromResidual::from_residual(r),
};

at some point between determining the return type of make_foo(), looking up the Try implementation, type-checking the ControlFlow::Continue(v) => v arm, and connecting this type information to the return value of the match block, there must be a point where the information that the whole thing evaluates to a Foo doesn’t propagate “fast enough” or “obviously enough” in order to the compiler to consider this place as a candidate for coercion anymore.

I don’t know if that’s something that can be easily improved. Intuitively speaking, instead of trying to coerce the &Foo to &FooRef, the compiler could also, alternatively, consider the possibility of coercing, say, the Result<Foo, …> to Result<FooRef, …> in the call to Try::branch, or perhaps the Foo to FooRef in the match arm, so there’s perhaps also some ambiguity.

In any case, you can help out the compiler here and do some explicit coercion by introducing an as _; then it’s all clear where exactly you want to convert, and the code compiles:

let foo: &FooRef = &make_foo()? as _;

As to the question why a !Sized target type BarRef leads to different behavior, I have some theories: It might be that there’s less ambiguity because you cannot have BarRef in Result or by-value anymore, so the options mentioned above for other places that could do some coersion disappear.

Or perhaps, the way to think about this is that while type inference (for the Sized case) can, after determining to perhaps not consider the outer = assignment for a place of coercion, proceed to determine that

  • the &match … { } is &FooRef, hence
  • the match … { } is FooRef, hence
  • the v return value in the match arm is FooRef

(and so on)

whilst with BarRef: !Sized, match … { } can no longer be BarRef because you can’t have !Sized types by-value, and for some reason, due to this, if can go back to considering the assignment a coercion site again, after all.

Or perhaps, type inference is more nonlinear, starting in the middle: it starts out with match … { } being some abstract yet sized type T, then determining that &match … { } is &T, meaning that when matching &T against &BarRef, you do get a mismatch (since BarRef is !Sized) inducing a coercion-site, whereas for FooRef it simply unifies T == FooRef and proceeds.

TL;DR, I don’t really know how exactly type inference works in Rust, and it’s often a bit confusing and inconsistent, but also type inference while (potentially) allowing coercions in so many places is probably a hard problem to begin with.

2 Likes

You're correct. It's just fundamentally hard.

Typical type inference (like Hindley–Milner type system - Wikipedia) are based around type equality. When foo(x) means that x has exactly the same type as the parameter of foo, as Unification (computer science) - Wikipedia can eagerly collapse type variables, and it tends to work great in practice (even if contrived examples can be very slow).

But once you have even just foo(identity(x)) in rust, now there's two coercion points. Do you coerce before calling identity, or after it but before foo? Both would work, so there's no unique answer any more, so arguably failing is better -- obviously for identity it doesn't actually matter, but in general it certainly could, and having behaviour depend on complicated nuance like that is non-great.

Similar problems exist for all kinds of extensions -- like how inherent methods can't be called on type variables. Or, for a non-Rust example, "type systems with subtyping enabling object-oriented programming [..] do not support HM-style type inference".

1 Like

According to the reference,

  • TyCor(T) to TyCor(U), where TyCor(T) is one of
    • &T
    • &mut T
    • *const T
    • *mut T
    • Box<T>

And where U can be obtained from T by unsized coercion.

And later notes that those cases are beyond that of other cases. I assume that's what's going on here.

This is the

  • &T or &mut T to &U if T implements Deref<Target = U> .

case we’re talking about here. And anyways, with more clear type information, e.g.

let f: &Foo = &foo()?;
let f: &FooRef = f;

it does work as an implicit conversion (without as).

1 Like

So maybe the next question is “should this be filed as an issue?” Type inference is hard, but if this is “supposed” to work then there should be something tracking it, yeah?

A more diligent search that I evidently should have done sooner reveals that this is a known issue with breaking consequences. Oh well.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.