Why does += require manual dereference when AddAssign() does not?

Yes, the . syntax has implicit auto-derefence rules, regular function calls and the += operator don't have any auto-derefence rules.

When the compiler sees this:

(&mut mri).add_assign(1); 

it tries to compile it in various ways, in particular:

AddAssign::add_assign(&mut mri, 1);

but this wouldn't work, so it also tries:

AddAssign::add_assign(*&mut mri, 1);

which does work.

3 Likes

Thank you! This is what I'm asking. Where's that documented?

There's a bit of a conflict between the language reference, which says very little (unless it's somewhere else):

If the type of the container operand implements Deref or DerefMut depending on whether the operand is mutable, it is automatically dereferenced as many times as necessary to make the field access possible. This processes is also called autoderef for short.

and The Book (which seems to imply . syntax is not special):

It happens automatically when we pass a reference to a particular type’s value as an argument to a function or method that doesn’t match the parameter type in the function or method definition.

It's documented under Method Call Expressions.

The Deref coercion that The Book is talking about is something different that doesn't apply here.

1 Like

Thanks for the pointer to the Method-call expressions docs.

And where is that documented?

It's documented under Type coersions.

Interestingly, this works:

fn foo(a: &mut i32, b: i32) {
    AddAssign::add_assign(a, b);
}

fn main() {
    let mut mri = &mut 0;
    foo(&mut mri, 1); // works due to Deref coercion
}

I think this doesn't work if you call AddAssign::add_assign directly because the Deref coercion doesn't apply to the self parameters in methods. That's somewhat confusingly documented on the same page:

For method calls, the receiver (self parameter) can only take advantage of unsized coercions.

It's a bit confusing because this exception is also true if you use the explicit AddAssign::add_assign(...) notation rather than the method call notation, which the docs don't clearly state.

2 Likes

I've been confused about that line for approximately :infinity:, but that's not what it means.

Deref coercion definitely still works on the receiver using fully qualified syntax. And for DerefMut too.

I think this is more a matter of trait impl resolution.


More generally, I've learned to be skeptical when the documentation says operator X is the same as method call Y. Operator lookup is it's own underdocumented distinct thing from both method calls and method dispatch; as far as I'm aware, there's no desugaring that universally works for at least some operators.

That said, I haven't find a particular counter example for this case (AddAssign).

3 Likes

I am trying to figure out how this resolution works by reading the documentation.

Then, for each candidate type T, search for a visible method with a receiver of that type in the following places:

  1. T's inherent methods (methods implemented directly on T).
  2. Any of the methods provided by a visible trait implemented by T. If T is a type parameter, methods provided by trait bounds on T are looked up first. Then all remaining methods in scope are looked up.

I think this documentation is wrong because the self parameter type can be different from the type on which the trait or an inherent method is implemented. For instance, the type of self can be &T in an impl T which contradicts this algorithm.

Edit: I have filed a ticket about this.

Nevermind, here's an example that AddAssign::add_assign(&mut $LHS, $RHS) can't be a literal desugaring of $LHS += $RHS.

4 Likes

That documentation is for obj.method() not Some::Path::method(obj) (and I was pretty loose with my language). This is the documentation for the latter. But (as it typical), I don't think it's nuanced enough to actually answer the question.

I wrote that when I thought you were talking about fully-qualified syntax and then wrote this section...

For one, it says

All function calls are sugar for a more explicitly fully-qualified syntax.
[...]
Refer to RFC 132 for further details and motivations.

However, those two resources don't agree with each other. The reference page linked to indicates that a fully-qualified syntax always starts with <Type> or <Type as Trait<...>>, where as the RFC allows a full path to a trait method. A path is only rewritten as <...> if it's a type, according to the RFC.

The RFC also doesn't discuss any algorithm for the path-based grammar though, so let's forge on and assume that the reference is right and a path to a trait is sugar for <_ as Trait<...>>. That would be this portion, but it's too vague to really be useful in this case.

In this case we have <_R as AddAssign<_L>>::add_assign(x, y) and we're asking "how does it infer _L and _R", and since it isn't documented and I haven't learned the compiler internals yet, this is the part where I would run off and throw a bunch of examples at the compiler to see if I can discern the behavior that way.

I haven't done that yet but maybe I will.


...but from your ticket I see you were talking about method call resolution after all.

The algorithm looks for a receiver of the candidate type, for each candidate. Not an implementer of the candidate type. So:

  1. Build a candidate list: Foo, &Foo, &mut Foo
  2. There are no Foo receivers in methods named foo; next candidate
  3. impl Foo has a &Foo receiver in the method named foo

What it says is: "T's inherent methods (methods implemented directly on T)", and even more clearly wrong for traits: "trait implemented by T" (where T is the candidate receiver type).

1 Like

Ah yes, I see. I'll add anything else to the issue.

I think that's just a restriction that the LHS must be a place expression? The desugaring could still be true, after the restriction to place expressions is checked.

2 Likes

Do you happen to know if there is an actual source-code-equivalent desugaring? I couldn't find one for == (sometimes it coerces like you called &(rhs as _) but sometimes &(rhs as _) is too ambiguous to compile in fully qualified syntax, for example) and the impression I got was "operator resolution is its own thing" from the issues, RFCs, and PRs I read (but I don't know the implementation myself).

I could check the compiler source later.

2 Likes

Aside from the method call rules concentrated above, I want to point out the following sentence is wrong

A += B is actually TypeOfA += TypeOfB. In your case, mri is of type &mut i32, 1 is of type i32.

The AddAssign is defined as:

pub trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}

And the +=

  • desugars to Self += Rhs
  • with the method call <Self as AddAssign<Rhs>>::add_assign(&mut Self, Rhs).
    Or, personally speaking, equivalently in the following two short forms:
    • Self: AddAssign<Rhs>
    • impl AddAssign<Rhs> for Self

So:

  • mri += 1 desugars to &mut i32 += i32 with <&mut i32 as AddAssign<i32>>::add_assign(&mut &mut i32, i32), or
    • &mut i32: AddAssign<i32>
    • impl AddAssign<i32> for &mut i32 which doesn't exist and the compiler emits error[E0368]: binary assignment operation += cannot be applied to type &mut {integer}
  • while AddAssign::add_assign(mri, 1) is the call AddAssign::add_assign(&mut i32, i32), i.e.
    • Self = i32, Rhs = i32
    • impl AddAssign<i32> for i32 does exist

they are not equivalent, since OP didn't stand on the perspective of Rust's type system.

1 Like

I'll give my vote for it.

fn main() {
    let mut f = Foo;
    f += (); // print: Foo
    let mut ff = &mut Foo;
    ff += (); // print: &mut Foo
    Foo += (); // error[E0067]: invalid left-hand side of assignment
    &mut Foo += (); // error[E0067]: invalid left-hand side of assignment
}

playground

It has to be more lenient than that.

When S has another impl like impl AddAssign<()> for S, the code is failed.

So I guess there might be an implicit rule for a single implementation of AddAssign with a reference as a Rhs.

An example here.

That seems to be following the general principle that coercion sites are removed when type inference cannot proceed. The call has an unknown RHS type as long as the compiles still considers a coercion of the right hand side possible. If there's only a single applicable trait implementation, then it would choose that impl and the expected type is known for the tight hand side. So the compiler tries coercing the right hand side to that type, and deref coercion for references work.

On the other hand, if multiple applicable impls are still around, then the possibility for coercion of the RHS is removed, which helps type inference since after removing the coercion it knows that the trait impl must exactly match the RHS type. Which creates an error if the type does not match exactly but is merely coercible.

1 Like

I mean the desugaring cannot be literally true (as in, all there is to it / how the compiler implements the operator), since += for primitive types is compiler-implemented and their AddAssign impls call +=, not the other way around. But that argument doesn't rule out that the += might behave exactly as if it was desugared to an AddAssign::add_assign call, so that the wax these AddAssign impls use += might be merely an implementation detail.