Mutable and immutable borrowing

Hi there.
This code, from official doc, does not compile:

fn main() {    
    let mut v = vec![1, 2, 3, 4, 5];    
    let first = &v[0];    v.push(6);    
   println!("The first element is: {first}");
}

but if i delete the last line, printing command, it works. So, the problem is the USE not the simple definition of the double borrowing.... Why?
thx., cheers

The problem is with multiple borrows, one of them mutable, being active at the same time. And "being active" is, yes, essentially "from the point of creation to the point of last use".

It certainly is so. However, I wonder why the compiler doesn't already block at the second "borrow", since after all it is the possible reallocation that potentially makes the first invalid.

first is valid until v.push(6). Why should the compiler reject that if first is never used after if becomes invalid?

If the reference is not used it doesn't matter that is becomes invalid, it's still safe. Making it a compile time error would just decrease the set of programs that are accepted by the compiler, forcing people to write workarounds to get the same programs accepted, making for a worse coding experience without any gains.

Note that it used to work like you said, but then the borrow checker was updated to allow for this behaviour. The feature in particular that lets this compile is called Non-Lexical Lifetimes (NLL).

1 Like

In practice it makes no difference. Mine was perhaps a more "philosophical" approach considering the extreme discipline with which the compiler tackles the safety problem

There's no philosophical difference, either. The code is not "kinda-sorta-OK-but-technically-still-unsound" when the last use is before the end of the lexical scope.

In fact the compiler's old behavior used to be based on strictly lexical scopes, but this has been (and is being continuously) relaxed, always respecting the boundaries of soundness and memory safety.

I understand your point but I'm not entirely convinced. If my code when used for practical purposes is wrong, in my opinion it is wrong from the beginning. I'm not sure... at this point could we accept "dead code" that is written completely incorrectly but has no effect? Ok, ok, I know it's not the same thing but it is, like I said, pure philosophy, I have no problem accepting standard behavior of compiler.

I have no idea of language/compiler design/construction much but I guess we could. However I also guess there is a limit to how much analysis of code is possible for practical use. People already complain enough about Rust compile times.

The compiler actually already does. For example, this code compiles, but would not if you removed the panic!(), because y would be used after it is no longer valid:

fn main() {
    let mut x = 1;
    let y = &x;
    x = 2;
    panic!();
    dbg!(y);
}

This particular case isn't greatly desirable, but it's a consequence of letting borrow checking understand control flow and be flexible about other cases where we actually care, where we want it to not complain about cases that can't happen. Usually, those cases would involve loops or branches rather than fully dead code, but there's not much point in spending effort on rejecting code that's dead “and also wrong” since dead code is usually a mistake itself.

And, when code like this does happen, well, the usual reason why a program might be compiled with dead code like this panic in the middle is that the code is being edited, and the user running it would like the rest of the code to be mostly ignored while they see what the part before the panic does.

3 Likes

It used to work like this until 2018 (in the scope based borrow checker, before non-lexical lifetimes rewrite), and everyone hated it.

It was super annoying. It made it even more difficult to learn Rust. The compiler was nitpicky, requiring declaring variables in a specific order. It prevented many straightfoward obviously correct programs, and required adding pointless extra scopes {} inside functions to explicitly limit scopes of mutable loans. It was busywork with no benefit.

2 Likes

OMG, I would have written a hateful compiler :-). Thanks you all for the chat

1 Like

Your false premise is that "the code when used for practical purposes is wrong". It isn't. The code is sound and correct, both theoretically and practically.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.