Stream: t-compiler/wg-polonius

Topic: Will polonius help with cases like this?

panstromek (Jan 24 2021 at 10:46, on Zulip):

This code doesn't compile, but arguably it should (at least in my opinion). I wonder if this is the case where Polonius relaxes the analysis, because this specific case is annoying me a lot pretty often.

struct Point { x:i32, y:i32 }

fn accept(point: &mut Point) {}

fn main() {
    let mut p = Point { x:0, y:0 };
    let mut x = &mut p.x;
    // *x = 3; // it should be ok even if you uncomment this line
    accept(&mut p);
    // let mut x = &mut p.x;  //rebinding here fixes the problem
    *x = 3;

Interestingly, there's a lot of very similar cases that shouldn't compile, which probably makes this challenging. Even just adding one level of nesting breaks this. Maybe not, actually, I'll have to think about it more

RalfJ (Jan 24 2021 at 12:12, on Zulip):

If you make accept actually mutate the point, your code has UB under Stacked Borrows (and most aliasing models I can imagine). Which is to say, the code is wrong and should not be accepted by the compiler.

RalfJ (Jan 24 2021 at 12:13, on Zulip):

One key promise for mutable references like x is that if you use them twice, and there was no other use or reborrow of x in between, the pointee is unchanged. So in code like

*x = 3;
accept(&mut p);
*x = 3;

it is very important that accept cannot mutate what x points to.

RalfJ (Jan 24 2021 at 12:13, on Zulip):

If your code was accepted, it could violate this property, which would be bad.

RalfJ (Jan 24 2021 at 12:14, on Zulip):

(in fact, it is easy to change this example such that if very similar code was accepted we'd have use-after-free... just make p a Vec and x = &mut p[0])

panstromek (Jan 24 2021 at 12:52, on Zulip):

The key here is that when accept mutates the point, the reference in x is not "active" and accept cannot possibly change its value - because it is just a static offset. Rebinding it afterwards is just a no-op, which seems to me like the compiler should be able to reason about automatically - and it already does to some extent, because it "deactivates" the reference before calling accept. It could "just" (with a lot of caveats) reactivate it afterwards.

The example with a Vec is what is exactly what I had in mind with similar cases that shouldn't compile, but my example only cares about structs, ie. things that are know at compile time.

It's sort of a multi-phase borrow or something like that.

panstromek (Jan 24 2021 at 12:54, on Zulip):

I'd summarize this as if some borrow checker error can be fixed by let rebinding that is provably no-op, it shouldn't be a borrow checker error in the first place.

bjorn3 (Jan 24 2021 at 13:41, on Zulip):

Every time you make a new borrow you are asserting that you can read/write (depending on if the borrow is mutable) the pointed to memory, so borrowing isn't a no-op.

panstromek (Jan 24 2021 at 14:11, on Zulip):

I still don't understand why would that be a problem. This is code can easily be fixed by just re-binding x without any change in behaviour. Why couldn't this transformation be done automatically, when possible?

bjorn3 (Jan 24 2021 at 14:27, on Zulip):

Because it changes behavior. It invalidates other borrows. Drops are also not moved earlier when doing so would be necessary to fix a borrowck error. This no surprises behavior is essential when writing unsafe code.

panstromek (Jan 24 2021 at 14:43, on Zulip):

Drop is still observable behaviour, though. Borrow is just a semantic construct. I can't imagine a case where this could break something, if implemented. Can you give an example where this would cause a problem?

RalfJ (Jan 24 2021 at 18:10, on Zulip):

The key here is that when accept mutates the point, the reference in x is not "active" and accept cannot possibly change its value

It is active though. Any reference that will be used again is "active".
Not sure what notion of "active" you are talking about, but it does not seem to be the one used in Rust.

RalfJ (Jan 24 2021 at 18:12, on Zulip):

Also note that borrow checking is deliberately designed to work the same for all kinds of types -- your example is clearly wrong with Vec, so not accepting it for Pair is by-design. This has several advantages:

It has the disadvantage that there is some code that does not trigger use-after-free that is rejected by the compiler, such as your example. This price IMO is worth paying for the above benefits.

Last update: Jun 20 2021 at 00:45UTC