So I was going through old notifications (ah, the backlog!) and I was reminded that there are some unsettled questions as to Rust execution order that we really ought to get to the bottom of.
In particular, Rust generally prefers left-to-right execution order for all expressions – however, we do make some exceptions. Apparently, before MIR, we were actually quite inconsistent here, in that borrowck and trans disagreed on some of the particulars. MIR eliminated that inconsistency, but there are still some matters that we should try to resolve – and quick!
Case 1: assignments
As #27868 describes, it used to be that borrowck and trans disagreed about the execution order of a = b
. In particular, borrowck treated the execution order as evaluating b
first and then a
– right-to-left. (Oddly, I have a distinct memory of trans working this way as well at the time when borrowck was first written, but perhaps I am wrong, or else perhaps trans drifted over time and borrowck failed to keep up.)
In any case, while LTR ordering is definitely more consistent overall, there is a strong reason to keep the ordering around the =
operator somewhat different. Consider the expression vec[i] = vec[j]
. Currently, this desugars to something like:
// vec[i] = vec[j]
let a = {
let b = &vec[j]; // actually overloaded
*b
};
let c = &mut vec[i];
*c = a;
This borrow checks just fine because the first borrow, of b = &vec[j]
, is confined to the rhs computation, and is complete before we take the second borrow (&mut vec[i]
). (This error is similar to the limitations around self.foo(self.bar())
when foo
or bar
is an &mut self
method; that is not (entirely) an accident and I’ll come to that later.)
When MIR landed, it agreed with borrowck. That means that we currently execute RHS before LHS. I don’t know that we are at liberty to change this: without significantly improving the type system, that would break a lot of code!
Case 2: Augmented assignment
There is a similar question of execution order around a += b
. To make things a bit more interesting, when +=
is overloaded, the add_assign
method takes an &mut
borrow of a
– in other words, it is not actually equivalent to a = a + b
.
Unfortunately, MIR seems to be internally inconsistent here at present, as this example (from @eddyb) demonstrates. If the +=
is overloaded, we evaluate b
then a
. Otherwise, we evaluate a
and then b
! This seems no good.
Also, evaluating LTR is inconsistent with borrowck, which accepts things like this today:
fn foo(x: &mut [i32]) {
x[0] += x[1];
}
fn main() { }
I believe that if we evaluate LTR this gets desugared to roughly the following, and hence would be an error, right?
let p = &mut x[0];
let q = x[1];
*p += q;
Where to go from here?
I think we should keep a = b
evaluation order as it is now in MIR, because I don’t think we can make code like vec[i] = vec[j]
stop compiling. (We had to make MIR/trans consistent, obviously, for soundness.) It seems like a similar argument implies a += b
should be RTL. This is consistent in a certain way (if you see an =
, RTL).
I haven’t done any crater runs to test how prevalent such patterns are.
Appendix: Method call evaluation order
It is well-known that nested method calls and &mut self
methods don’t play nice. In particular, if you have self.set(self.get())
, those arguments are evaluated LTR, which means that this line gets desugared to something like this:
let _0 = &mut *self; // mutable borrow starts here
let _1 = &*self; // borrow of `self` for `self.get()`
let _2 = Type::get(_1);
Type::set(_0, _2);
You can see here that the borrow _0
is in scope when the first argument (self.get()
) is evaluated. This is what causes the problem.
There are a couple of ways we could fix this. One really drastic thing we could which might help would be to change the evaluation order of arguments (note: I am not actually proposing this, just exploring a hypothetical). For example, we could imagine that we evaluate the arguments in reverse order, and hence we would have:
let _1 = &*self; // borrow of `self` for `self.get()`
let _2 = Type::get(_1);
let _0 = &mut *self; // mutable borrow starts here
Type::set(_0, _2);
But this would be a massive breaking change.
I think we have to (and want to) keep the observable semantics of method calls to be evaluated left-to-right. But we could make the borrowck happy and improve everyday life by reordering those argument evaluations only when it is not observable. That would apply in this case, but would not apply in cases like self.foo().set(self.get())
.
You can probably see the analogy to the vec[i] = vec[j]
case. One important thing to note is that this would not be a legal case for re-ordering. This is because vec[i]
is going through the Index
and Deref
traits for Vec
, so the reordering would definitely be observable!
(There is also another way to think about making borrowck smarter without reordering; one could imagine that the _0
borrow is for a lifetime that includes only Type::set
, and not the let
itself. I don’t want to get distracted so I won’t go into detail here, but it works out pretty similar to the idea of reordering only when not observable in terms of what it can typecheck.)