Skip to content

Commit f83e52a

Browse files
authored
Merge pull request #1941 from fiveseven-lambda/fix-compound-assign-eval-order
Clarify operand evaluation order in compound assignment with primitiv…
2 parents 4d77198 + 864ca4a commit f83e52a

File tree

1 file changed

+92
-27
lines changed

1 file changed

+92
-27
lines changed

src/expressions/operator-expr.md

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -900,51 +900,116 @@ r[expr.compound-assign.no-value]
900900
Attempting to use a value expression is a compiler error rather than promoting it to a temporary.
901901

902902
r[expr.compound-assign.operand-order]
903-
Evaluation of compound assignment expressions depends on the types of the operators.
903+
Evaluation of compound assignment expressions depends on the types of the operands.
904904

905-
r[expr.compound-assign.primitive-order]
906-
If both types are primitives, then the modifying operand will be evaluated first followed by the assigned operand.
907-
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.
905+
r[expr.compound-assign.primitives]
906+
If the types of both operands are known, prior to monomorphization, to be primitive, the right hand side is evaluated first, the left hand side is evaluated next, and the place given by the evaluation of the left hand side is mutated by applying the operator to the values of both sides.
907+
908+
```rust
909+
# use core::{num::Wrapping, ops::AddAssign};
910+
#
911+
trait Equate {}
912+
impl<T> Equate for (T, T) {}
913+
914+
fn f1(x: (u8,)) {
915+
let mut order = vec![];
916+
// The RHS is evaluated first as both operands are of primitive
917+
// type.
918+
{ order.push(2); x }.0 += { order.push(1); x }.0;
919+
assert!(order.is_sorted());
920+
}
921+
922+
fn f2(x: (Wrapping<u8>,)) {
923+
let mut order = vec![];
924+
// The LHS is evaluated first as `Wrapping<_>` is not a primitive
925+
// type.
926+
{ order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
927+
assert!(order.is_sorted());
928+
}
929+
930+
fn f3<T: AddAssign<u8> + Copy>(x: (T,)) where (T, u8): Equate {
931+
let mut order = vec![];
932+
// The LHS is evaluated first as one of the operands is a generic
933+
// parameter, even though that generic parameter can be unified
934+
// with a primitive type due to the where clause bound.
935+
{ order.push(1); x }.0 += { order.push(2); (0u8,) }.0;
936+
assert!(order.is_sorted());
937+
}
938+
939+
fn main() {
940+
f1((0u8,));
941+
f2((Wrapping(0u8),));
942+
// We supply a primitive type as the generic argument, but this
943+
// does not affect the evaluation order in `f3` when
944+
// monomorphized.
945+
f3::<u8>((0u8,));
946+
}
947+
```
908948

909949
> [!NOTE]
910-
> This is different than other expressions in that the right operand is evaluated before the left one.
950+
> This is unusual. Elsewhere left to right evaluation is the norm.
951+
>
952+
> See the [eval order test] for more examples.
911953
912954
r[expr.compound-assign.trait]
913-
Otherwise, this expression is syntactic sugar for calling the function of the overloading compound assignment trait of the operator (see the table earlier in this chapter).
914-
A mutable borrow of the assigned operand is automatically taken.
955+
Otherwise, this expression is syntactic sugar for using the corresponding trait for the operator (see [expr.arith-logic.behavior]) and calling its method with the left hand side as the [receiver] and the right hand side as the next argument.
915956

916-
For example, the following expression statements in `example` are equivalent:
957+
For example, the following two statements are equivalent:
917958

918959
```rust
919-
# struct Addable;
920960
# use std::ops::AddAssign;
921-
922-
impl AddAssign<Addable> for Addable {
923-
/* */
924-
# fn add_assign(&mut self, other: Addable) {}
925-
}
926-
927-
fn example() {
928-
# let (mut a1, a2) = (Addable, Addable);
929-
a1 += a2;
930-
931-
# let (mut a1, a2) = (Addable, Addable);
932-
AddAssign::add_assign(&mut a1, a2);
961+
fn f<T: AddAssign + Copy>(mut x: T, y: T) {
962+
x += y; // Statement 1.
963+
x.add_assign(y); // Statement 2.
933964
}
934965
```
935966

967+
> [!NOTE]
968+
> Surprisingly, desugaring this further to a fully qualified method call is not equivalent, as there is special borrow checker behavior when the mutable reference to the first operand is taken via [autoref].
969+
>
970+
> ```rust
971+
> # use std::ops::AddAssign;
972+
> fn f<T: AddAssign + Copy>(mut x: T) {
973+
> // Here we used `x` as both the LHS and the RHS. Because the
974+
> // mutable borrow of the LHS needed to call the trait method
975+
> // is taken implicitly by autoref, this is OK.
976+
> x += x; //~ OK
977+
> x.add_assign(x); //~ OK
978+
> }
979+
> ```
980+
>
981+
> ```rust,compile_fail,E0503
982+
> # use std::ops::AddAssign;
983+
> fn f<T: AddAssign + Copy>(mut x: T) {
984+
> // We can't desugar the above to the below, as once we take the
985+
> // mutable borrow of `x` to pass the first argument, we can't
986+
> // pass `x` by value in the second argument because the mutable
987+
> // reference is still live.
988+
> <T as AddAssign>::add_assign(&mut x, x);
989+
> //~^ ERROR cannot use `x` because it was mutably borrowed
990+
> }
991+
> ```
992+
>
993+
> ```rust,compile_fail,E0503
994+
> # use std::ops::AddAssign;
995+
> fn f<T: AddAssign + Copy>(mut x: T) {
996+
> // As above.
997+
> (&mut x).add_assign(x);
998+
> //~^ ERROR cannot use `x` because it was mutably borrowed
999+
> }
1000+
> ```
1001+
9361002
r[expr.compound-assign.result]
937-
Like assignment expressions, compound assignment expressions always produce [the unit value][unit].
1003+
As with normal assignment expressions, compound assignment expressions always produce [the unit value][unit].
9381004
9391005
> [!WARNING]
940-
> The evaluation order of operands swaps depending on the types of the operands:
941-
> with primitive types the right-hand side will get evaluated first, while with non-primitive types the left-hand side will get evaluated first.
942-
> Try not to write code that depends on the evaluation order of operands in compound assignment expressions.
943-
> See [this test] for an example of using this dependency.
1006+
> Avoid writing code that depends on the evaluation order of operands in compound assignments as it can be unusual and surprising.
9441007
9451008
[`Try`]: core::ops::Try
1009+
[autoref]: expr.method.candidate-receivers-refs
9461010
[copies or moves]: ../expressions.md#moved-and-copied-types
9471011
[dropping]: ../destructors.md
1012+
[eval order test]: https://github.com/rust-lang/rust/blob/1.58.0/src/test/ui/expr/compound-assignment/eval-order.rs
9481013
[explicit discriminants]: ../items/enumerations.md#explicit-discriminants
9491014
[field-less enums]: ../items/enumerations.md#field-less-enum
9501015
[grouped expression]: grouped-expr.md
@@ -961,10 +1026,10 @@ Like assignment expressions, compound assignment expressions always produce [the
9611026
[Unit-only enums]: ../items/enumerations.md#unit-only-enum
9621027
[value expression]: ../expressions.md#place-expressions-and-value-expressions
9631028
[temporary value]: ../expressions.md#temporaries
964-
[this test]: https://github.com/rust-lang/rust/blob/1.58.0/src/test/ui/expr/compound-assignment/eval-order.rs
9651029
[float-float]: https://github.com/rust-lang/rust/issues/15536
9661030
[Function pointer]: ../types/function-pointer.md
9671031
[Function item]: ../types/function-item.md
1032+
[receiver]: expr.method.intro
9681033
[undefined behavior]: ../behavior-considered-undefined.md
9691034
[Underscore expressions]: ./underscore-expr.md
9701035
[range expressions]: ./range-expr.md

0 commit comments

Comments
 (0)