-
Notifications
You must be signed in to change notification settings - Fork 13.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure arbitrary_self_types
method resolution is forward-compatible with custom autoref
#136987
Comments
Nominating for lang team discussion, as this is timely and relevant to the arbitrary self types stabilization. @rustbot label I-lang-nominated cc @adetaylor @rust-lang/lang @rust-lang/lang-advisors |
Interestingly, I was talking with @nikomatsakis about doing the "inverse" of this in the trait system: If you have a trait with a It does make sense to me that we would want something that works both ways; in other words, that we would have autoref for pin. But the way I've been thinking we would do pin ergonomics was to build them into the borrow checker; at that point, pin becomes more like another one of the special reference types we have today. |
Specializing trait methods
This also feels related to being able to implement
We can do this for On
|
Maybe it's worth drilling into this as a data structures and algorithms problem. This will be a bit rough, but maybe we can start putting some boundaries around the problem. Defining an efficient autorefToday method resolution proceeds in Let's say our goal is to make it so that for each type we consider along the Receiver chain, we can consider a list of user defined methods named Right now the dev guide describes an algorithm where we (roughly) adjust each receiver type with an extra Extending to user defined autorefIf we allow arbitrary new types with autoref-like behavior, the same approach would not work: We would have to consider I'm somewhat optimistic we can still make resolution If we do this for each method, we've responsibly bounded the set of self types and adjustments to be considered for that method instead of just trying Note that this can work if we allowed applying multiple kinds of autoref, like Comparison to status quoNow for the bad news: This limitation does not apply to autoref as it exists today. Consider this example: trait Foo { fn method(self); }
trait Bar { fn method(&self); }
impl<T: Copy> Foo for T { fn method(self) {} }
impl Bar for String { fn method(&self) {} }
fn main() {
let x: String = String::new();
x.method();
} This does not compile because when we consider I think we should be willing to give up on this code ever calling PrecedenceSince we have a bounded set of self types to consider for each method, I don't think we should introduce further levels of precedence. We can add a single last level of precedence that considers all custom autoref types, and errors if there is any ambiguity. Arguably we should also do this for the builtin reference types in a future edition. Summary sketchLet's pretend we have a core trait trait AutoRef<From> {} with some to-be-designed rules around the kinds of impls you can write, and a custom reference type struct MyRef<T>(*mut T);
impl<T> AutoRef<T> for MyRef<T> {}
impl<T> AutoRef<&T> for MyRef<T> {} In summary, I think we could still support:
We would not support:
Footnotes |
Cc @rust-lang/types |
@ssbr posted some thoughts in this area too - taking the view that |
While I believe we can figure out a way to change deref into trait Deref {
type Target;
type Output<'a> = &'a Self::Target;
fn deref<'a>(&'a self) -> Self::Output<'a>;
} in a way that doesn't break anyone, I'm not sure that
We can't just rely on a This is of course resolveable by saying that similar to how we decide, depending on the usage of the result of the deref, whether to invoke There's a slightly odd, but potentially simpler solution if we figure out |
I think something like this would work:
I believe such an approach would have fewer method resolution complications than a new trait (in particular I don't think it would have complexity implications), but I am not sure if it would solve everything (or even a majority of the things) that people want to solve with custom autoref that cannot be solved by making carve-outs for special types. |
a solution to customizable e.g. the following pub struct BitRefMut<'a> {
byte: &'a mut u8,
bit: usize,
}
impl Deref for BitRefMut<'_> {
type Target = bool;
type Output<'a> = BitPlace;
fn deref(&self) -> Self::Output<'_> {
BitPlace { value: *self.byte & (1 << self.bit) != 0 }
}
}
struct BitPlace {
value: bool,
}
impl Deref for BitPlace {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl DerefMut for BitRefMut<'_> {
type Output<'a> = BitPlaceMut<'a>;
fn deref_mut(&mut self) -> Self::Output<'_> {
BitPlaceMut { value: *self.byte & (1 << self.bit) != 0, byte: self.byte, bit: self.bit }
}
}
struct BitPlaceMut<'a> {
value: bool,
byte: &'a mut u8,
bit: usize,
}
impl Deref for BitPlaceMut<'_> {
type Target = bool;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl DerefMut for BitPlaceMut<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}
impl Drop for BitPlaceMut<'_> {
fn drop(&mut self) {
let bit = 1 << self.bit;
if self.value {
*self.byte |= bit;
} else {
*self.byte &= !bit;
}
}
} |
cc @eholk, who is working on pin ergonomics. |
So, I've thought a lot about this recently (resulting in more review comments over on rust-lang/reference#1725), and I'm not seeing a problem here (famous last words, I know). The reason is essentially the one you gave, from the RFC:
The way the algorithm works conceptually is as follows:
Prior to arbitrary self types, the receiver set was equal to the deref set and the set of type constructors that could be applied to the The expanded receiver set causes us to search more impl blocks. Within those blocks, the criteria for a candidate method is the same as it was before. There are now more possible If we later expand that autoref step to add more entries, that all still seems fine. At least, I'm not seeing how searching more impl blocks and allowing more composition of
|
@traviscross The intuition you have is what sent me down the rabbit hole of writing my long comment above. In short I believe it is more complicated than doing a straightforward set lookup, because we have to perform type unification with every candidate. When you have an The limitation I describe is to disallow user-defined autoref from applying through these "opaque" impls and only apply when the autoref type is named in the impl (on the impl block or in the self type of a method). This means that when considering a candidate we only have to consider user-defined autoref in the places naming a type with an Autoref impl, instead of extending the current strategy of trying every possible autoref on each candidate and seeing what sticks. |
Yes, you're right of course. I had tenuously in mind for that a scheme where one would elaborate receiver rows with bounds satisfied and the index for them with inner parts of the type recursively replaced with placeholders, with the idea being to frontload some cost by building this database, essentially, so that the query cost could remain constant (or at least Probably I wonder how much it actually matters. My guess would be that in a large program the number of custom autoref types would scale logarithmically with program size. So maybe the cost here, which is in the worst case linear in the number of autoref type constructors, wouldn't much matter. If they had to be brought explicitly in scope, that seems even more likely. But, fortunately, we probably don't need to fully design an arbitrary autoref scheme right now. We just need to work out that stabilizing arbitrary self types doesn't close too many doors for it. That's the main thrust. |
Discussed in the lang meeting today-- overall there was relatively high shared confidence that stabilizing arbitrary_self_types will not significantly complicate the work needed for custom autoref. Notably, this comment from the RFC:
only means having to search the impl blocks of Thanks for the discussion here! |
arbitrary_self_types
(v1 RFC, v2 amending RFC) is currently being considered for stabilization in #135881. This feature, designed to support methods which takeself
by an expanded set of user-defined types likeself: SmartPointer<T>
,self: CppRef<T>
, makes several changes to method resolution.How method lookup works today
Today, we search for methods on a type
T
in this order (rustc dev guide, reference):T
with aSelf
type ofself
,&self
,&mut self
T
isDeref
,NewT = <T as Deref>::Target
, considerT
,&T
,&mut T
T
is no longerDeref
, ifT == [A; N]
, consider[A]
,&[A]
,&mut [A]
For each considered type, preference is given for inherent methods over extension methods (trait methods).
How method lookup works under
arbitrary_self_types
arbitrary_self_types
v2 changes this look up (RFC. reference PR):Receiver
trait rather than theDeref
trait for looking up candidate receiver types. This allows for method receivers likefn takes_cppref(self: CppRef<T>)
whereCppRef
is some type that cannot implementDeref<Target = T>
.arbitrary_self_types
allows for methods to be defined forimpl MyType { fn foo(self: PtrLike<MyType>) { ... } }
for customPtrlike
types.Other related outstanding features
arbitrary_self_types_pointers
feature considers*const T
methods for each candidate receiver type which is a*mut T
.pin_ergonomics
feature as documented here consider "method[s] with aPin
that's reborrowed" (note: I, cramertj@, don't actually understand at this time how this changes the candidate or method set).Why custom autoref
I created this issue because I believe that most uses of
arbitrary_self_types
that I'm aware of actually want custom autoref behavior (the exception is external std-likes e.g. RFL'sArc
). That is, rather than extending the candidate set of receiver types, I believe they may instead/also want to modify the per-candidate set ofSelf
types searched. I want to ensure that the parts ofarbitrary_self_types
being stabilized do not hamper our ability to do add autoref behavior (at least forPin
, if not for custom types).For example:
Doing lookup for
foo
onMyType
, we look for methods takingself: MyType
,self: &MyType
, andself: &mut MyType
, see that there's no receiver impl or unsizing to follow, and give up. What should happen is thatCppRef
should behave as&
does (as it is strictly less powerful than&
and can be created fromT
"for free").Note that this is a per-candidate type behavior, as we'd want
Box::new(MyType{ ... }).foo()
to work as well. That is, method resolution forfoo
onBox<MyType>
should look forfoo
as a by-value, by-ref, by-cppref, by-mutref, and by-cppmutref method on candidate typesBox<MyType>
and thenMyType
.self: Box<MyType>
,self: &Box<MyType>
,self: CppRef<Box<MyType>>
,self: &mut Box<MyType>
,self: CppMutRef<Box<MyType>>
self: MyType
,self: &MyType
,self: CppRef<MyType>
<<< this one is found and selectedSimilarly, we could imagine doing the same thing in order to support by-
Pin
methods on types which areUnpin
(Unpin
types can convert fromSelf
toPin<&Self>
/Pin<&mut Self>
"for free"):Similarly, we'd also love for
Pin<&mut Self>
methods to be callable onPin<Box<Self>>
receiver types, which can create aPin<&mut Self>
"for free", maybe with an impl like:Other examples of autoref-like non-
Receiver
s that we'd like to consider in method resolution:faer
MatMut
type which is a mutable view over a matrix. Ideally, methods which work on aMatMut
would be callable on a local of typeMat
.Is this even a thing we can do?
Maybe not. Making method resolution search a (# of autoref types for
Self
) * (Receiver/Deref impls forSelf
+ unsize) number of types when looking for a method seems expensive, but I don't have a good idea of how expensive.However, I think this is well-motivated at least for
Pin
, and possibly some other select set of types.Future compatibility issues
At this point this issue is mostly FUD, unfortunately-- I don't have a specific concern besides "method resolution is getting more complicated but maybe in the wrong way."
Pin
is already stable as aself
type, so any extensions we make to support at leastPin
-autoref have to be done in a backwards compatible way. Therefore, it may be the case that we're not making anything worse for ourselves by allowing other non-stdlib types to have this ability.The
arbitrary_self_types
v2 RFC does say thatarbitrary_self_types
makes it so that "a wider set of locations is searched for methods with those receiver types." I haven't completely understood this-- it seems like the set of places we have to look today for a potentialself: Arc<Self>
method is the same set of locations we'd have to look for aself: NonStdArc<Self>
(that is, we have to search both the impls of...Arc
andSelf
as well as any<Self as Receiver>::Target
).Am I (@cramertj) missing something? Are we committing to other method resolution complexities by stabilizing
arbitrary_self_types
that will make it harder to add autoref support forPin
orCppRef
?The text was updated successfully, but these errors were encountered: