Skip to content

Binary operator LHS type is fixed instead of being subtyped #45425

Closed
@shepmaster

Description

@shepmaster

This started with a Stack Overflow question which boiled down to:

use std::ops::Rem;

fn powm<T>(fbase: &T, modulus: &T)
where
    for<'x> &'x T: Rem<&'x T, Output = T>,
{
    fbase % modulus;
}

fn main() {}
Complete error
error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
 --> src/main.rs:7:11
  |
7 |     fbase % modulus;
  |           ^
  |
note: first, the lifetime cannot outlive the lifetime 'b as defined on the function body at 3:1...
 --> src/main.rs:3:1
  |
3 | / fn powm<'a, 'b, T>(fbase: &'a T, modulus: &'b T)
4 | | where
5 | |     for<'x> &'x T: Rem<&'x T, Output = T>,
6 | | {
7 | |     fbase % modulus;
8 | | }
  | |_^
note: ...so that reference does not outlive borrowed content
 --> src/main.rs:7:13
  |
7 |     fbase % modulus;
  |             ^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 3:1...
 --> src/main.rs:3:1
  |
3 | / fn powm<'a, 'b, T>(fbase: &'a T, modulus: &'b T)
4 | | where
5 | |     for<'x> &'x T: Rem<&'x T, Output = T>,
6 | | {
7 | |     fbase % modulus;
8 | | }
  | |_^
note: ...so that types are compatible (expected std::ops::Rem, found std::ops::Rem<&T>)
 --> src/main.rs:7:11
  |
7 |     fbase % modulus;
  |           ^

Some poking around found some workarounds:

  • If we say that the input references can be unified to the same
    lifetime, it works:

    fn powm<'a, T>(fbase: &'a T, modulus: &'a T)
  • If we say that 'b outlives 'a, it works:

    fn powm<'a, 'b: 'a, T>(fbase: &'a T, modulus: &'b T)
  • If we say that we can have two different lifetimes in the operator, it works:

    for<'x, 'y> &'x T: Rem<&'y T, Output = T>,
  • If we directly call the Rem::rem method, it works:

    Rem::rem(fbase, modulus);
  • If we dereference and re-reference, it works:

    &*fbase % &*modulus;

I poked @eddyb for further explanation why the original case didn't work. Our tidied IRC conversation:

@eddyb
The answer is Rem::rem(fbase, modulus); doesn't do unification, it does a reborrow coercion.
That is, it's equivalent to &*fbase % &*modulus
You can figure this out by having an operator implemented with &str on the RHS and passing &String or other stuff like that.
If it works with &String, then this is generally approved

@shepmaster
But what's the difference of a % b and Rem::rem(a, b) — I thought that was straight desugaring?

@eddyb
There is no such thing as a straight desugaring; the first is Rem::rem(a, b) the second is (Rem::rem)(coerce a, coerce b)
IDK, I think we should coerce, but we can't coerce the LHS, because......... what type to coerce to?
It has to be asymmetrical. "has to"
Wait, no, why is this a... I might be wrong; yeah no I am wrong.

@eddyb
This is about traits... so Rem::rem(a, b) is <_ as Rem<_>>::rem(a, b) and a % b is <typeof a as Rem<typeof b>>::rem(a, b)
So we do coercions on the RHS, but I think we need to also subtype the LHS, which we're not doing right now.

@shepmaster
There's some piece of "this doesn't work because the lifetimes are disjoint, even though they could be unified at least as much as the lifetime of the function call"

@eddyb
It's not really disjointness; hint: an asymmetrical 'b: 'a or whatever you added only helps because the RHS is getting coerced. The RHS can be a subtype but the LHS is fixed.
Lemme rephrase :D
<typeof a as Rem<_>>::rem(a, b) whereas you want _ extends (typeof a) or something as the Self for Rem

@eddyb
We already use forced subtyping to have more flexibility around unsizing coercions because the trait matching of T: Unsize<U> is invariant.
It wasn't invariant for a while, which was a soundness bug..... introduced by yours truly in the original implementation.

@eddyb
This shows lhs_ty being used for Self and rhs_ty_var for the trait parameters so the LHS type is fixed instead of being subtyped.

@eddyb
This is where we do the right thing, going through an inference variable & extra subtyping (or LUB but that doesn't apply here) to make something more flexible than what the trait system allows.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions