Description
@RalfJung and I were talking and we realized that -- with a bit of work -- we could make 2-phase borrows be something fully handled during MIR lowering. This is nice because it means that the rest of the model (e.g., the Unsafe Code Guidelines aliasing model) doesn't have to be aware of it.
There are basically two places we use 2PB today. Let me break down at a high-level how to handle each of them.
Method calls, operators, etc
The first place is for a case like self.push(self.len())
. Today, we lower to the push
call to:
TMP0 = &mut2 self
TMP1 = lower<self.len()>
push(TMP0, TMP1)
The borrow checker finds the use of TMP0 on the last line and considers that the activation point. In particular, for each 2PB, there must be an exactly one use which is dominated by the borrow and which postdominates the borrow. This use is the activation point.
The idea would be to change to instead lower as:
TMP0 = &mut self
TMP1 = &*TMP0;
TMP2 = lower<self.len() where self=*TMP1>
push(TMP0, TMP2)
Here, we borrow self
mutably as normal but then reborrow it to create a shared reference TMP1
. Then, when we lower the arguments, any reference to self
is rewritten so that it does not refer to self
but rather *TMP1
(which has the same type as self
). Then the normal NLL mechanisms come into play.
We'd obviously have to tweak our error messages here so that *TMP1
is displayed as "self" to the end-user.
The key observation here is that we can statically see all references to self
while lowering the arguments and rewrite them to reference *TMP1
instead. (In fact, we already use a mechanism like this when lowering match guards, see below.)
Matches
We also rely on 2PB to handle variables in match pattern guards. The basic idea of how we lower something like this:
match foo {
Some(ref mut x) if G => ...
}
is that we introduce various artificial borrows. The first is a borrow of the place being matched (foo
) -- that borrow begins when the match begins and ends upon entering some arm. The second is for bindings that are accessed during match arms. These are each implicitly mapped to a dereference of a shared borrow. So (today!) you wind up with something like:
// Start of match
B0 {
TMP0 = &foo // the match borrow
TMP1 = DISCRIMINANT(foo)
if TMP1 == Some { goto B1 } else { B3 }
}
// We found `foo` is a `Some`
B1 {
TMP2 = &mut2 foo.as<Some>.0
TMP3 = &TMP2
TMP4 = <lower G where x=*TMP3>
if TMP4 is true { goto B2 }
}
// Corresponds to the `Some` arm:
B2 {
DROP(TMP0) // borrow of `foo` ends as we enter the arm
<lower arm body>
}
B3 { ... }
Again, this is what we do today. For this to work, we rely on TMP2 = &mut2 foo.as<Some>.0
being a 2-phase borrow that never gets activated, since otherwise it would overlap with the TMP0
borrow that is definitely still in scope.
Instead of that, we will create a shared borrow and then use some kind of case from &&T
to &&mut T
:
B1 {
TMP2 = &foo.as<Some>.0
TMP3 = &TMP2
TMP4 = TMP3 as &&mut T
TMP5 = <lower G where x=*TMP4>
if TMP5 is true { goto B2 }
}
This cast is provably safe (@RalfJung has done it!), so there is no unsafe code or anything else involved here.
UPDATE: Lightly edited for clarity.