1

I have seen cannot borrow as immutable because it is also borrowed as mutable and my question is not a duplicate, since my code has Non-Lexical Lifetimes enabled.

I'm wondering if there is a fundamental reason why the following code:

fn f1(a: &u32) {
  print!("{:?}", a);
}

fn main() {
  let mut a = 3;
  let b = &mut a;
  f1(&a);
  *b += 1;
  print!("{:?}", b);
}

must result in the following error:

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/bin/client/main.rs:91:6
   |
90 |   let b = &mut a;
   |           ------ mutable borrow occurs here
91 |   f1(&a);
   |      ^^ immutable borrow occurs here
92 |   *b += 1;
   |   ------- mutable borrow later used here

Now, I know that on the line f1(&a), we'll have one mutable reference (b) and one immutable reference (&a), and according to these rules this can't happen. But having 1 mutable and 1 immutable reference can only cause a problem if their usages are interleaved, right? That is, in theory, shouldn't Rust be able to observe that b is not used within &a's existence, and thus accept this program?

Is this just a limitation of the compiler? Or am I overlooking some other memory danger here?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • Out of curiosity, can you not move the `let b = &mut a` declaration below the `f1(&a)` call? Is there something preventing that in your real code? – John Kugelman Jun 21 '21 at 00:46
  • In this example, for sure. But in my project, I have some large functions with long scopes. I have some immediately evaluated lambdas in the function too (to leverage the `?` operator). Long story short, non-Lexical Lifetimes has enabled me to write code without ever really fighting the borrow checker, but this situation is the one exception where something overly-restrictive seems to happen. – Pasindu Muthukuda Jun 21 '21 at 01:01
  • @PasinduMuthukuda: "*Long story short, non-Lexical Lifetimes has enabled me to write code without ever really fighting the borrow checker*" I'm not an expert on the language, but isn't that like 80% of the point of Rust: that it syntactically stops you from doing potentially dangerous things? In your case, the safety of the code in `main` depends entirely on what `f1` is doing. And I'm not sure it's a good idea for the validity of syntax to be based on non-local information. – Nicol Bolas Jun 21 '21 at 01:39
  • @NicolBolas "In your case, the safety of the code in main depends entirely on what f1 is doing." I actually can't see how `f1` can break the safety of `main`; can you given an example of what `f1` can do that would mess up `main`? – Pasindu Muthukuda Jun 21 '21 at 02:02
  • @PasinduMuthukuda: It could start a thread and pass it the mutable reference you gave it. A thread that modifies the referenced object, thereby creating a data race. – Nicol Bolas Jun 21 '21 at 02:04
  • I'm... pretty sure you can't simply pass a parameter that's an immutable reference into a thread. That is, `fn f1(a: &u32) { thread::spawn(|| { print!("{:?}", a); }); }` doesn't compile for me. – Pasindu Muthukuda Jun 21 '21 at 02:10

1 Answers1

3

That is, in theory, shouldn't Rust be able to observe that b is not used within &a's existence, and thus accept this program?

Maybe, though it's possible that there are edge cases where this would be a problem. I would expect optimisations to be an issue here e.g. eventually Rust will be able to finally tag &mut as noalias without LLVMs immediately miscompiling things, and in that case your code would be UB if it were allowed.

Is this just a limitation of the compiler?

In this case no, it's literally a limitation of the language specification. There are situations which are limitations of the compiler like loop mutations, but here you're trying to do something the language's rules explicitely and specifically forbid.

Even polonius will not change that.

Masklinn
  • 34,759
  • 3
  • 38
  • 57