Skip to content

Conversation

@adwinwhite
Copy link
Contributor

@adwinwhite adwinwhite commented Oct 31, 2025

Fixes #148283

Given a deref chain like &C -> &B -> &A, we do can coerce &C -> &A directly with adjustments:

rustc_hir_typeck::fn_ctxt::_impl::apply_adjustments adj=[
    Deref(None) -> C,
    Deref(Some(OverloadedDeref { mutbl: Not, ..)) -> B,
    Deref(Some(OverloadedDeref { mutbl: Not, ..)) -> A,
    Borrow(Ref(Not)) -> &A
]

But if we coerce in two steps: &C -> &B and &B -> &A, it errs with

can't compose [
    Deref(None) -> C,
    Deref(Some(OverloadedDeref { mutbl: Not, ..)) -> B,
    Borrow(Ref(Not)) -> &B
] and [
    Deref(None) -> B,
    Deref(Some(OverloadedDeref { mutbl: Not, ..)) -> A,
    Borrow(Ref(Not)) -> &A
]

This PR bridges the gap.

About the FIXME, I'm not familiar with unsafe fn pointer so that's not covered.

Edited: My change allows more than deref composition. I think restricting it to exactly deref composition is suitable, to avoid surprising behavior where coercion order affects method selection.
Other unsupported composition can be reported by proper diagnostics.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Oct 31, 2025
@rustbot
Copy link
Collaborator

rustbot commented Oct 31, 2025

r? @BoxyUwU

rustbot has assigned @BoxyUwU.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@theemathas
Copy link
Contributor

theemathas commented Oct 31, 2025

With this PR, the following code compiles, and prints Two method: 1. Is this intended?

(The PR makes the compiler do the coercion of &Three -> &Two -> &dyn OneTrait, even though a direct coercion &Three -> &dyn OneTrait is available and has different behavior.)

(This code ICEs on nightly.)

use core::ops::Deref;

trait OneTrait {
    fn method(&self);
}
struct Two(i32);
struct Three(Two);

struct OneThing;
impl OneTrait for OneThing {
    fn method(&self) {
        println!("OneThing method");
    }
}

impl Deref for Three {
    type Target = Two;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

// Commenting the following impl causes the code to output `Three method: 1`
impl OneTrait for Two {
    fn method(&self) {
        println!("Two method: {}", self.0);
    }
}

// Commenting the following impl does NOT change the output
impl OneTrait for Three {
    fn method(&self) {
        println!("Three method: {}", self.0.0);
    }
}

fn main() {
    let x = match 0 {
        0 => &Three(Two(1)),
        // Commenting the below line causes the code to output `Three method: 1`
        1 => &Two(2),
        _ => &OneThing as &dyn OneTrait,
    };
    x.method();
}

@jieyouxu
Copy link
Member

jieyouxu commented Oct 31, 2025

This seems a bit fishy, why would we special-case two-step coercions? Does this handle three-steps &D -> &C -> &B -> &A? Or is this supposed to do an n-ary &Tn -> ... -> &T1 deref-coercion-chain where we do a "big-step" from &Tn -> &T1? This seems very surprising and not-obvious from a user POV?

EDIT: oh, reading the original issue, is this supposed to be an intended language behavior? That feels a bit surprising to me.

@adwinwhite
Copy link
Contributor Author

It does handle arbitrary steps of composition. But I may need to further restrict what composition is allowed, given the example.

@theemathas
Copy link
Contributor

theemathas commented Oct 31, 2025

The following code ICEs both on nightly and with this PR:

use std::ops::Deref;

trait One {}
trait Two {}

struct Thing;
impl One for Thing {}
struct Thing2;
impl Two for Thing2 {}

impl<'a> Deref for dyn One + 'a {
    type Target = dyn Two + 'a;
    fn deref(&self) -> &(dyn Two + 'a) {
        &Thing2
    }
}

fn main() {
    let _ = match 0 {
        0 => &Thing as &dyn One,
        1 => &Thing,
        _ => &Thing2 as &dyn Two,
    };
}

@theemathas
Copy link
Contributor

This code also ICEs both on nightly and with this PR:

trait Super {}
trait Sub: Super {}

struct Thing;
impl Super for Thing {}
impl Sub for Thing {}

fn main() {
    let _ = match 0 {
        0 => &Thing as &dyn Sub,
        1 => &Thing,
        _ => &Thing as &dyn Super,
    };
}

@adwinwhite
Copy link
Contributor Author

can't compose [
    Deref(None) -> Thing,
    Borrow(Ref(Not)) -> &Thing,
    Pointer(Unsize) -> &dyn One
] and [
    Deref(None) -> dyn One,
    Deref(Some(OverloadedDeref { mutbl: Not, ..)) -> dyn Two,
    Borrow(Ref(Not)) -> &dyn Two
]

This can be supported as well. Not so sure about whether we should.
I need to think about your first example first before allowing more code.

@theemathas
Copy link
Contributor

Of note: This code currently compiles on stable, and seems to rely on a special case in the compiler to treat this as a double coercion and allow it, I think.

fn never() -> ! { panic!() }

fn main() {
    let _ = match 1 {
        0 => never(),
        1 => &Box::new(2),
        _ => &3,
    };
}

@theemathas theemathas added I-lang-nominated Nominated for discussion during a lang team meeting. T-lang Relevant to the language team labels Oct 31, 2025
@theemathas
Copy link
Contributor

Lang-nominating, since a decision needs to be made on what exactly should LUB coercion do with double coercions, especially in cases where an X -> Y -> Z coercion has different run time behavior from an X -> Z coercion.

@theemathas
Copy link
Contributor

theemathas commented Oct 31, 2025

These two code snippets seem to involve a double-coercion, and compiles on stable:

use std::ops::Deref;
use std::marker::PhantomData;

struct Thing;
struct Wrap<T>(PhantomData<T>);

// Sub is a subtype of Super
type Sub = Wrap<for<'a> fn(&'a ()) -> &'a ()>;
type Super = Wrap<fn(&'static ()) -> &'static ()>;

impl Deref for Thing {
    type Target = Sub;
    fn deref(&self) -> &Sub {
        &Wrap(PhantomData)
    }
}

fn main() {
    let _ = match 0 {
        0 => &Thing,
        1 => &Wrap(PhantomData) as &Super,
        _ => &Wrap(PhantomData) as &Sub,
    };
}
use std::ops::Deref;
use std::marker::PhantomData;

struct Thing;
struct Wrap<T>(PhantomData<T>);

// Sub is a subtype of Super
type Sub = Wrap<for<'a> fn(&'a ()) -> &'a ()>;
type Super = Wrap<fn(&'static ()) -> &'static ()>;

impl Deref for Super {
    type Target = Thing;
    fn deref(&self) -> &Thing {
        &Thing
    }
}

fn main() {
    let _ = match 0 {
        0 => &Wrap(PhantomData) as &Super,
        1 => &Wrap(PhantomData) as &Sub,
        _ => &Thing,
    };
}

Oddly enough, swapping the order of Super and Sub makes it not compile:

use std::marker::PhantomData;

struct Wrap<T>(PhantomData<T>);

// Sub is a subtype of Super
type Sub = Wrap<for<'a> fn(&'a ()) -> &'a ()>;
type Super = Wrap<fn(&'static ()) -> &'static ()>;

fn main() {
    let _ = match 0 {
        1 => &Wrap(PhantomData) as &Sub,
        _ => &Wrap(PhantomData) as &Super,
    };
}
error[E0308]: mismatched types
  --> src/main.rs:12:36
   |
12 |         _ => &Wrap(PhantomData) as &Super,
   |                                    ^^^^^^ one type is more general than the other
   |
   = note: expected reference `&Wrap<for<'a> fn(&'a ()) -> &'a ()>`
              found reference `&Wrap<fn(&()) -> &()>`

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

@adwinwhite
Copy link
Contributor Author

My change does allow more than deref composition. I think restricting it to exactly deref composition is suitable.
Other unsupported composition can be reported by proper diagnostics.

@theemathas
Copy link
Contributor

The question here isn't whether these double coercions can be supported. The question is whether they should be supported.

@theemathas
Copy link
Contributor

theemathas commented Oct 31, 2025

Even for the case of chaining two deref coercions together, there can still be strange behavior.

In the following code, the following coercions are available:

  • Three -> Two (deref coercion)
  • Two -> One (deref coercion)
  • Three -> One (unsize coercion)

With this PR, the compiler decides to do the Three -> Two -> One roundabout coercion, and the code prints Wrap([12]).

(This code ICEs on nightly.)

use core::ops::Deref;

#[derive(Debug)]
struct Wrap<T: ?Sized>(T);

type One = Wrap<[u8]>;
struct Two;
type Three = Wrap<[u8; 0]>;

impl Deref for Three {
    type Target = Two;
    fn deref(&self) -> &Self::Target {
        &Two
    }
}

impl Deref for Two {
    type Target = One;
    fn deref(&self) -> &One {
        &Wrap([12u8])
    }
}

fn main() {
    let x = match 0 {
        0 => &Wrap([]) as &Three,
        // Commenting the below line causes the code to output `Wrap([])`
        1 => &Two,
        _ => &Wrap([34u8]) as &One,
    };
    println!("{x:?}");
}

@BoxyUwU BoxyUwU removed T-lang Relevant to the language team I-lang-nominated Nominated for discussion during a lang team meeting. labels Oct 31, 2025
@BoxyUwU
Copy link
Member

BoxyUwU commented Oct 31, 2025

unnominating until i get a chance to look more at this PR and probably write up a description that is less impl details. also this seems more like t-types than t-lang to me but I've yet to properly look at this

@theemathas
Copy link
Contributor

In case you missed it, the reference has a section on LUB coercions. It's ambiguous on if and how double coercions should work though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test of least upper bound coercion behavior triggers ICE

5 participants