Trace mutable borrows across function call boundaries


#1
One day on the #rust-beginners forum someone was having a hard time with the borrow checker (it was one of those special days, yes). I am sorry I cannot remember who the person was, but I found the problem to be very interesting and their intuition about the code was as far as I can tell sound. I would like to discuss the problem here to figure out
  • if the intuition about the soundness of the code is correct
  • if we can teach the borrow checker new tricks to accept this behavior (or if there is something in the works already that I couldn’t find in my searches)
Old example which is just a special case of what NLL already handles

The problem can be seen in the following piece of code:

struct Resource {
    accessed: usize
}

impl Resource {
    fn access(&mut self) -> &usize {
        self.accessed += 1;
        &self.accessed
    }
}

fn main() {
    let mut res = Resource { accessed: 0 };
    // {  // uncomment to appease the borrow checker
        let accessed = res.access();
        assert_eq!(*accessed, 1);
    // } // uncomment to appease the borrow checker
    assert_eq!(res.accessed, 1);
}

Method access mutably borrows self and leaks an immutable reference to one of the sub-fields. Now, my intuition would say that this particular method has no way to leak a mutable reference to self, so after calling the method self should be just borrowed immutably. However, the borrow checker is not convinced, since it requires the leaked reference to be dropped before ending the mutable borrow on self.

Now, if we just inline the body of access into main, the borrow checker is happy to notice where the mutability of the borrow ends:

struct Resource {
    accessed: usize
}

fn main() {    
    let mut res = Resource { accessed: 0 };
    let accessed =  {
        res.accessed += 1;
        &res.accessed
    };
    assert_eq!(*accessed, 1);
    assert_eq!(res.accessed, 1);
}

Would it be possible (feasible & desirable) to trace the mutability of the borrow across function call boundaries so that the first example would be accepted by the borrow checker as well?

Later edit: The initial example did not correctly present the core problem, as the upcoming non-lexical lifetimes feature would make that code compile. Thanks to the perceptiveness of the first three commenters in this thread (@Ixrec, @kennytm and @atagunov), this error was identified. Moreover, @atagunov provided an accurate example of the problem in a comment below.

After further research, I found that very problem presented in the Rustonomicon (if you are thinking “you should feel bad for not finding this before posting”, rest assured that I do :slight_smile:).

struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share();
}

fails with:

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/main.rs:11:5
   |
10 |     let _loan = foo.mutate_and_share();
   |                 --- mutable borrow occurs here
11 |     foo.share();
   |     ^^^ immutable borrow occurs here
12 | }
   | - mutable borrow ends here

As the Rustonomicon explains, the problem is that the lifetime of the borrow &mut self which takes place when mutate_and_share is called, must last as long as _loan in order to avoid _loan becoming a dangling reference. And that is great! What is not great is that the borrow stays mutable throughout, and we really don’t need it to be mutable for the lifetime of _loan because _loan is not mutable.

So I would like to open the discussion regarding the possibility to trace the mutability separately and convert the mutable borrow into an immutable one as soon as it no longer needs to be mutable.


#2

This seems like one of the simplest typical motivating examples for the enhancement we usually call “non-lexical lifetimes”, which is very much in the works. The tracking issue is https://github.com/rust-lang/rust/issues/43234. For a general introduction to what on earth “non-lexical lifetimes” means and what sort of code it’s supposed to affect, the RFC’s guide-level explanation is probably the best resource: https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md Your example is pretty much identical to “Problem Case #1” from that RFC.


#3

Thanks! I did read the nll RFC before posting, but I couldn’t convince myself that it would handle this case. Sorry for the noise if it already does. I can at least confirm that in its current (partial) implementation the nll feature on nightly does not yet solve this problem :slight_smile: I guess I could wait for the full implementation before bringing this up again, but I am sure curious :slight_smile:


#4

The original example does work when NLL is enabled. https://play.rust-lang.org/?gist=455e16d10ba405660dfda8e227e82607&version=nightly


#5

Hi, however this does not work

#![feature(nll)]

struct Resource {
    accessed: usize
}

impl Resource {
    fn access(&mut self) -> &usize {
        self.accessed += 1;
        &self.accessed
    }
}

fn main() {
    let mut res = Resource { accessed: 0 };
    let accessed = res.access();
    assert_eq!(res.accessed, 1);
    assert_eq!(*accessed, 1);
}

failing with

16 |     let accessed = res.access();
   |                    --- mutable borrow occurs here
17 |     assert_eq!(res.accessed, 1);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ immutable borrow occurs here

…and I think this is the example @Victor_Savu would have liked to enable


#6

@kennytm, @atagunov yes, you are both right. Sorry for the initial example. I modified it to make the former look more like the latter and forgot to test it with nll again.

In short, I don’t want the borrow to end, I just want it to become immutable. Thanks, @atagunov ! Your correction perfectly represents the issue. May I edit the original question with your version in order to give future readers an easier time?