Borrow checker treating logically immutable reference as mutable

Is there a good reason for the following code to not compile, or is it just a limitation of the borrow checker?
And if it is a limit of the borrow checker, is it feasible to un-limit it at some point or should i rely on unsafe blocks?

fn main() {
    let mut x: i32 = 5;
    let x_ref: &i32;
    {
        let x_ref_mut = &mut x;
        x_ref = &*x_ref_mut;
    }
    &x;
    x_ref;
}
error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
 --> src/main.rs:8:5
  |
5 |         let x_ref_mut = &mut x;
  |                         ------ mutable borrow occurs here
...
8 |     &x;
  |     ^^ immutable borrow occurs here
9 |     x_ref;
  |     ----- mutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

(Rust Playground)

The above code is a simplified form of a situation where i needed to read a mutable reference to an object's Vec<SomeIterator>, and reuse that reference later in case the data is good. Problem is, while that reference (x_ref) should be immutable, the borrow checker treats it as mutable, presumably because it takes the same lifetime as the now out-of-scope mutable reference (x_ref_mut).

I can work around the problem while keeping performance by using raw pointers, or in my case, manually-checked array indices, but it would be nice if rust could recognize this situation for me. Assuming i'm not wrong about the logic.

The inner block in the minimization doesn't do anything borrow-checker wise; it doesn't matter that the variable goes out of scope. The problem is that x gets exclusively borrowed,[1] and that exclusive borrow gets kept alive by the reborrow stored in x_ref. I don't think that will change or have a good workaround any time soon for the minimization.

There may be workarounds for your specific case, like split_at_mut or such, if you'd like to share more context.


  1. that's what &mut _ really are -- exclusive borrows ↩︎

you cannot re-"use" a reference. you need to re-"borrow" the data. rust references are more than just a pointer (as pointers in C) to the data.

we don't have discontinous borrows (this is a lie if you consider disjoint flow control paths), i.e. between the creation of the reference and the point of "reuse later", the reference is live and must not overlap with other exclusive reference. in other words, there's no "gap" in the lifetime.

the variable x_ref_mut can get out of scope, but the lifetime in the type of the reference is NOT tied to the scope of the variable. this is the premise of NLL: Non-Lexical Lifetimes.

if you could provide more details about "this" situation. but the borrow checker does consider control flow paths.

this modified version should compile, if you can fit your use case to this pattern:

fn main() {
    let mut x: i32 = 5;
    let x_ref: &i32;
    {
        let x_ref_mut = &mut x;
        x_ref = &*x_ref_mut;
    }
    let some_condition: bool = todo!();
    if some_condition {
        &x;
    } else {
        x_ref;
    }
}

specifically, this works:

// "merge" two control flow paths
// similar to the "phi" node in the SSA form IR
let x_ref = if some_condition {
    &x
} else {
    x_ref
};
// use the merged `x_ref` like normal
println!("{}", x_ref);

Ok, so it was my misunderstanding of the language, that makes sense.

For curiosity's sake here's what i was working with:

let name;
let dir;
{
	let mut name_mut;

	'next_dir: loop {
		match self.dirs.last_mut() { // &mut
			Some(d) => {
				loop {
					match d.dir1.next() { // iterator using &mut
						Some(Ok(item)) => {
							name_mut = item.file_name();
							if name_mut == c"." || name_mut == c".." { continue; }
							name = name_mut.to_owned();
							dir = &*d; // assigning immutable reference from mutable
							break 'next_dir;
						},
						Some(Err(e)) => {
							self.dirs.pop();
							return Some(Err(e));
						},
						None => {
							self.dirs.pop();
							continue 'next_dir;
						}
					}
				}
			},
			None => return None
		}
	}

}
// the correct thing to do:
// let dir = self.dirs.last().unwrap();

&self.dirs; // self has a mutable reference carried from dir, so this fails.

Basically making an iterator to do depth-first search.
As you can see the problem with simply getting another reference is the runtime cost of needlessly checking self.dirs's length twice.

Right now i'm just solving this by manually getting and checking self.dirs.len() once and using self.dirs.get_unchecked() with unsafe blocks.

Thanks for clearing up the confusion.

I see. self.dirs is the stack. I think an index is more idiomatic than a direct reference in this use case.

personally, I think to avoid calling len() does not justify an unsafe operation. this is premature optimization.

the length of the Vec is (practically) guaranteed to be in L1 cache, and the bound check branch should be almost perfectly predicated, you are just imagining the runtime "cost".

Rust conflates mutability and exclusive access vs shared access “modes” and the name &mut strongly suggests mutability matters more, but that's a lie: uniquiness matters more.

There was even attempt to use &uniq, but it went nowhere and that means confusion like yours is normall step in the rust learning…

Have you benchmarked?

Please don't. Compiler is does pretty decent (even if not perfect) job eliminating checks like these. Most of the time calling .len two or ten times is Ok and optimizer would only call it for you once.

In some rare cases that's benefitial, but please, don't do such optimiations without justifications, you are just asking for trouble, when you disable checks that Rust tries to push on you for minor unproven gain.