Relaxing the borrow checker for fn (&mut self) -> &T

So this issue comes up repeatedly. My take is this: In the current model, @kvijayan’s claims are not quite correct, but the issue he identifies is annoying, and it’d be nice to have a better solution.

First let me explain the model as I see it. I think this is quite similar to what @eddyb has been saying, though there are a lot of comments here and I’ve not parsed them all with 100% care. I think it’s easiest to think of the current model in terms of permissions. So when you do the borrow you give up your permissions for the lifetime of the borrow and you effectively transfer them into the reference. When the borrow expires (that is, when we exit the lifetime), you get those permissions back.

The current system was designed to make minimal assumptions. In particular, we wanted to avoid all kinds of alias analysis or reasoning about types and just focus on lifetimes. This is for two reasons: first, we know that people (particularly in unsafe code) will play games with types and transmutes and so forth. Second, those kinds of analyses are really brittle in the face of generics, closures, objects, etc (aka, existentials) where the types are unknown.

Furthermore, the borrow checker does not (and, at least in general, it cannot) assume that just because a function returned, it is finished using some data. Consider this signature:

impl<'scope> Scope {
    fn process(&self, x: &'scope mut T) -> &'scope U { ... }
}

In this case, it would be perfectly legal for unsafe code to take the x pointer and send it off to another thread. Or perhaps they have selected one or two fields from x to ship to another thread for processing, and returned the rest to you. This is legal so long as they know this thread will terminate before 'scope exits.

Of course, if you read that code above carefully, you will see that it is not quite @kvijayan’s example. His example – or at least the usual – was more like this (for clarify, I’m “eliding” elision):

impl Container { 
    fn insert_and_lock<'a>(&'a mut self, key: K) -> &'a Value { ... }
}

Now, in this example, the 'a is bound on the function itself (rather than on the impl. It only appears on exactly one argument (self). In such a case, I don’t as yet see how any safe abstraction could still be using parts of self after it returns. So in short it does seem plausible to me that we could “downgrade” the borrow in such a case. But it would have to be a pretty narrowly tailored rule.

This seems pretty related to the “rust memory model” question. Basically, it makes sense that we could potentially downgrade the borrow in precisely those cases where it makes sense for us to add “nocapture” annotations in LLVM, I suspect.

In this whole discussion, though, I’ve been assuming safe code. When you get to unsafe code, the picture is a bit murkier. When it comes to the memory model, for example, I’ve been envisioning rules that are focused on “safety boundaries”. When you enter into a function that contains unsafe code, the compiler would assume a lot more potential aliasing than in purely safe functions. And when a function is declared as unsafe (unsafe fn), it is considered effectively an extension of that unsafe region, and hence it may well not have “nocapture” annotations added to it. In such cases, should the borrow checker’s behavior also change?

I think the TL;DR then is that there may be indeed be something here, but it’s not an open-and-shut case. This case probably belongs to a list of things we should consider addressing in the borrow checker:

  • nested method calls
  • non-lexical lifetimes
  • “downgrading” borrows

The first two I have pretty solid plans for, though I’ve not been as good about communicating those plans as I’d like. Non-lexical lifetimes in particular has a lot of subtle points. Downgrading borrows I confess I haven’t thought as much about. But I think that, unlike the other two, it is at least partly about conventions and unsafe code as much as anything else – that is, the correctness of such a transform hinges on what the callee is allowed to do – and hence it should probably be considered together with a proposal for a Rust memory model.

1 Like