Closed
Description
@RalfJung raised this example in which the "two-phase" borrow of x
is compatible with a pre-existing share:
fn two_phase_overlapping1() {
let mut x = vec![];
let p = &x;
x.push(p.len());
}
This poses a problem for stacked borrows, as well as for the potential refactoring of moving stacked borrows into MIR lowering (#53198) -- roughly for the same reason. It might be nice to change this, but -- if so -- we've got to move quick!
Activity
nikomatsakis commentedon Nov 26, 2018
(It's actually not clear if we would want to backport this -- ideally we would, but it's probably a corner case.)
Centril commentedon Nov 27, 2018
Nominated for discussion on the next T-lang meeting since this seems to a affect the type system in observable ways and because I'd like to understand this better... provided that we can wait until Thursday... ;)
nagisa commentedon Nov 27, 2018
I only have theoretical knowledge of NLL’s implementation but it seems extremely hard to forbid this…?
RalfJung commentedon Nov 28, 2018
From what I hear it's actually easy, we just have an additional constraint that such that when the two-phase borrow starts, all existing loans for that ref get killed (like they usually would for a mutable ref).
The problem is the "fake read" desugaring we do to make sure that match arms cannot mutate the discriminee:
Becomes something like
When
s_for_guard
is created, we create a new mutable ref to something that has outstanding shared refs.RalfJung commentedon Nov 28, 2018
I once proposed an alternative to this desugaring that avoids 2-phase-borrows. I was told (by @arielb1 and probably others) it doesn't work because it doesn't preserve pointer identity (if the guard compares addresses it'd notice), but I actually don't see why: I think all pointers are the same as in the desugaring above. Namely, we should do:
where
fake_mut
isfake_mut
is actually safe to call with any possiblex
. And the pointers are exactly the same as in the desugaring above. So why does this not work?arielb1 commentedon Nov 28, 2018
@RalfJung
In this translation,
addr_of(s_for_guard) != addr_of(s)
, while in the previous translation it can be. However, I'm not sure how important this property is, and in any case,addr_of(s_for_guard) != addr(s)
today.And if we really wanted to preserve this property, we could have
s
be a union between&T
and&mut T
.RalfJung commentedon Nov 28, 2018
Okay, so we agree that my proposal doesn't break more than what we currently do -- but it might be harder to fix (if we care).
It would however still be the case that the mutable reference was created after the guard runs, which could be observable in terms of Stacked Borrows / LLVM noalias.
arielb1 commentedon Nov 28, 2018
Sure enough. So I think @RalfJung's solution (having an
&&mut T -> &&T
transmute, 2 addresses forref/ref mut
bindings in guards, and 2-phase borrows rejecting existing borrows) is actually fine.57 remaining items
RalfJung commentedon Apr 15, 2019
EDIT: Nvm, someone added the
#[allow]
exactly because of this and I am blind.^^ Srroy for the noise.Are they really all prohibited? The following function is accepted, and this does look like an overlapping 2PB to me:RalfJung commentedon Apr 17, 2019
FWIW, I was able to support the two-phase-borrows test cases with outstanding lones that @matthewjasper wrote in a refurbished version of Stacked Borrows (Miri PR: rust-lang/miri#695).
There'll be a blog post about this soon (TM). I am not sure if this is the model we want (in fact I think it is not, but for reasons mostly unrelated to two-phase borrows), and I have little idea of the consequences this will have on optimizations.
EDIT: The blog post is out
RalfJung commentedon May 19, 2019
@Manishearth discovered an intersting piece of code that was not accepted by Stacked Borrows 2.0:
I have annotated what happens with the stack. The issue is that we add the
Unique
tag "in the middle" of a bunch of existingSharedRW
, and that's bad -- a block of consecutiveSharedRW
should be treated together, almost as if it was a single item.I tried to fix this by supporting two-phase borrows "for real": I added a new
TwoPhase
kind of permission that, on the first write, turns into aUnique
. I changed creating two-phase borrows such that the item gets added on top of a block of consecutiveSharedReadWrite
. That makes the above example pass. Unfortunately, it breaks another example involving two-phase borrows of interior mutable data (example courtesy of @matthewjasper):What happens here is that after creating the two-phase borrow, we end up with a stack like
[x: Unique, x_for_do_the_thing: TwoPhase, l: SharedRW]
. Then we do another (anonymous) shared reborrow forx
(forx.set(3)
), which gets added just abovex
, and we write to it, which killsx_for_do_the_thing
andl
because neither are part of the same block of consecutiveSharedRW
. I think @matthewjasper's scheme described above has the same problem.The issue here is (and this kind of came up before already) that the stack just does not encode enough information about which pointer is derived from which. We'd want to know that
l
andx_for_do_the_thing
are both direct children ofx
, such that adding moreSharedRW
-children does not affect any of the existing children (unless they areUnique
).So, for now I basically gave up and made two-phase borrows carry
SharedRW
permission. That's slightly better than theRaw
proposal I made back with Stacked Borrows 1, because at least we still properly track the pointer and do not confuse it with raw pointers, but it does mean that you can read from the parent pointer even after the "activation point" (which is not a special point any more really) and still use the child two-phase pointer afterwards.Manishearth commentedon May 20, 2019
Nightly miri now passes all of the
elsa
tests except forsync.rs
(which uses threads, which miri doesn't like because of the dynamic loading)pnkfelix commentedon Jul 10, 2019
Just a quick note: If we are still interested in gathering data about how much code was affected by adding the restriction to two-phase borrows, one source of data I had not considered is the set of commits that land that reference the lint's tracking issue #59159
(I count thirteen commits as of today that reference the lint issue.)
It won't be a complete list, of course, but it is a different data source than say crater.
mutable_borrow_reservation_conflict
lint to deny by default #76104Auto merge of rust-lang#96268 - jackh726:remove-mutable_borrow_reserv…
vec.swap(0, vec.len() - 1)
, whilevec.push(vec.len())
works. #74319