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

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.

Here's one significant difference: The += operator supports two-phase borrows, the desugaring using explicit &mut … doesn't.

Edit: I've overcomplicated the example... This is easier: Rust Playground

But I should probably verify what the evaluation order is in the first place....

Edit2: Nevermind, not a two phase borrow at all. The left hand side is simply evaluated after the right hand side. Rust Playground

Seems a long story for it.

Oh wait, I might have seen that discussion. Was it that the order changes depending on whether it's built in or using the trait? Looks like something along those lines.. Rust Playground

As least that settles the question whether there could be any equivalent desugaring: No.

2 Likes

Woah that's crazy.

1 Like

Also, I guess that means there is two stage borrowing after all.

1 Like

That's the exact example of case 2 in this thread: Settling execution order for `=`, `+=`

Update: The newest execution of compound assignment expression lies in the Reference book:

Evaluation of compound assignment expressions depends on the types of the operators.

If both types are primitives, then the modifying operand will be evaluated first followed by the assigned operand. It will then set the value of the assigned
operand's place to the value of performing the operation of the operator with the values of the assigned operand and modifying operand.

Note: This is different than other expressions in that the right operand is evaluated before the left one.

Otherwise, this expression is syntactic sugar for calling the function of the overloading compound assigment trait of the operator (see the table earlier in
this chapter). A mutable borrow of the assigned operand is automatically taken.

src: reference#compound-assignment-expressions

The key is whether the two operands are both primitives. playground

1 Like

Which leaves us with funny effects such as that

fn main() {
    let mut v = vec![1, 2, 3];
    v[0] += v[1];
}

or

use std::num::Wrapping;

fn main() {
    let mut v = [Wrapping(1), Wrapping(2), Wrapping(3)];
    v[0] += v[1];
}

compiles fine, but

use std::num::Wrapping;

fn main() {
    let mut v = vec![Wrapping(1), Wrapping(2), Wrapping(3)];
    v[0] += v[1];
}

doesn’t.

1 Like

Yes, the issue is already there.

And this compiles:

fn main() {
    let mut b = vec![Wrapping(1), Wrapping(2), Wrapping(3)];
    let v = b.as_mut_slice(); // Add this line
    v[0] += v[1];
}
1 Like