The borrowing made by local reference variables should be considered relinquished after their last use of the lender


#1

In other words, you should return the book if you’re not reading it anymore:

struct S { a: i32, b: i32 }

fn ref_a(s: &mut S) -> &mut i32 { &mut s.a }
fn ref_b(s: &mut S) -> &mut i32 { &mut s.b }

fn foo(s: &mut S) {
    let mut ra = ref_a(s);
    *ra = 11; // [0]
    // [1]
    
    let rb = ref_b(s); // [2]
    *rb = 22;
    
    let mut n: i32 = 1;
    ra = &mut n; // [3]
}

[0] This is the last time ra uses its lender. [1] I would expect ra to stop its borrowing here. [2] Passing s to ref_b causes the compile-time error: cannot borrow *s as mutable more than once at a time. But I would expect this to compile because *s shouldn’t be borrowed by ra here anymore. [3] Reusing ra to borrow something else should be fine. But dereferencing ra here would cause a compile-time error on the line labeled [2].


#2

If I understand it right, this idea was proposed and rejected for inclusion in Rust. See discussion here and here. To do what you want, enforce the lifetime of ra (for example) by enclosing it in its own lexical scope:

struct S { a: i32, b: i32 }

fn ref_a(s: &mut S) -> &mut i32 { &mut s.a }
fn ref_b(s: &mut S) -> &mut i32 { &mut s.b }

fn foo(s: &mut S) {
    { // [0]
        let mut ra = ref_a(s);
        *ra = 11;
    };

    { // [1]
        let rb = ref_b(s);
        *rb = 22;
    };

    // re-using `ra` is impossible, since the variable was local to
    // scope [0] above.
}

#3

It’s something that Rust can add to improve in the future, it hasn’t been ruled out.


#4

The idea you linked to is totally different. I only propose that the borrow checker should be a little bit smarter to realize that my example code is perfectly safe and just compile it. The reason that it is safe to compile it is that the i32 which ra initially references through s is not used by ra after the statement on the line labeled [0] has been executed. Currently the compiler complains that you can’t borrow the s reference (or through it) on the line labeled [2] because ra might still be using s. This error message by the compiler is simply not true. The compiler should be able to determine that ra doesn’t read anything through s any more at that point in time nor later.


#5

This is part of the postponed SEME regions RFC. In particular, see bullet point #1 of the motivation section of the RFC, which states that the borrow checker should take the liveness of borrows into account.


#6

Great read. It seems someone has thought about this a lot further than I did.


#7

For what it’s worth, I operated under the assumption that the borrow should live for as long as the variable in which the borrow is stored. (I still think this is the natural mental model for the lifetime of a borrow.) Eager drop would have the effect that the borrowing variable could be dropped sooner, such that the code you posted would compile. I was not aware of the SEME regions RFC, but having read it now, I still don’t see the essential difference with eager drop semantics, other than that this RFC restricts eager-drop semantics to the borrow checker…


#8

I think the best mental model is that a certain reference variable borrows something potentially as long as the reference variable lives, but given that the reference variable doesn’t use what it borrows after a certain point in time (nor pass the thing it’s borrowing to anywhere else), then the borrowing should end right after that point in time. Just like with library books, you can hold on to them until they expire, but really, you should return the books already when you’re done with them so that others can borrow them.

Maybe you use the word differently, but I’ve been using the word “drop” in the meaning of “run the destructor defined by the implementation of the Drop -trait” (and I’ve been assuming that others use it in that meaning too). I don’t see how anything could be eagerly dropped safely during the execution of any of the functions in my example.


#9

I’m not sure how much sense it makes to keep arguing this point, since I doubt it’s going to change any minds, and in any event it seems like the project has made a decision that likely won’t be re-litigated (or at least not before a significant time passes), but I use the word “drop” in the sense of “variable’s resources can be reclaimed”, which is (I think) the same formulation adopted by the eager drop RFC. That is, for non-copy types, a “drop” can be caused by passing the variable to std::mem::drop, whether or not the variable’s type has a Drop implementation.

The relevance here is that I’d consider “drop”, generally, to be the natural converse of RAII, which could be called RRID (or, Resource Release Is Destruction). Viewed this way, a “drop” of a borrow pointer currently has the effect of releasing the borrow. I still think that eager drop on borrow pointers is semantically indistinguishable from the scheme described by the SEME RFC (though that RFC does goes into more detail regarding the eager drop mechanism, and into treatment of temprorary values). In other words, I can’t think of a case that the SEME RFC covers that wouldn’t be automatic if eager drop (using the SEME mechanism for determining the last use of a value, and with the SEME enhancements regarding pattern matching on enums) had been incorporated.

I understand that you and others view borrowing as a separate concern from other types of resource release, which is reasonable considering the importance of borrow semantics in the language. Fixing the borrow checker to allow more borrows does not have nearly the backwards-compatibility risk that a generalized eager drop would, and seems likely to be accepted in the near-ish term. I don’t intend to keep fighting a lost battle, I just hoped to communicate and refine my understanding of the situation.