Description
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 isRem::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 ofa % b
andRem::rem(a, b)
— I thought that was straight desugaring?
@eddyb
There is no such thing as a straight desugaring; the first isRem::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... soRem::rem(a, b)
is<_ as Rem<_>>::rem(a, b)
anda % 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 theSelf
forRem
@eddyb
We already use forced subtyping to have more flexibility around unsizing coercions because the trait matching ofT: 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 showslhs_ty
being used forSelf
andrhs_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.