Hello, I wrote a simple program:
struct Arr;
impl core::ops::Index<usize> for Arr {
type Output = Self;
fn index(&self, _index: usize) -> &Self::Output {
&self
}
}
impl core::ops::IndexMut<usize> for Arr {
fn index_mut(&mut self, _index: usize) -> &mut Self::Output {
self
}
}
impl Arr {
fn len(&self) -> usize {
1
}
}
fn main() {
let mut arr = [0, 1, 2];
increment(&mut arr[arr.len() - 1]); // line 26
let mut arr = Arr;
increment(&mut arr[arr.len() - 1]) // line 28
}
fn increment<T>(_arr: &mut T) {}
I think the compiler would fail at line 26 since I tried to get an immutable reference for arr
while holding a mutable reference.
However, what happened is the compiler only fails at line 28. I cannot tell any difference between the two invocations. I wonder if there is a special case in borrow check for mutably indexing an array.
You can try it at Rust Playground
Thank you.
Edit 1:
As suggested by @trentj . I checked the MIR generated for each invocation for increment. Here is what I found:
The generated MIR for line 26 looks like this:
bb0: {
_1 = [const 0_i32, const 1_i32, const 2_i32]; // scope 0 at src/main.rs:27:19: 27:28
_8 = &_1; // scope 1 at src/main.rs:28:24: 28:33
_7 = move _8 as &[i32] (Pointer(Unsize)); // scope 1 at src/main.rs:28:24: 28:33
_6 = Len((*_7)); // scope 1 at src/main.rs:28:24: 28:33
_9 = CheckedSub(_6, const 1_usize); // scope 1 at src/main.rs:28:24: 28:37
assert(!move (_9.1: bool), "attempt to compute `{} - {}`, which would overflow", move _6, const 1_usize) -> bb1; // scope 1 at src/main.rs:28:24: 28:37
}
bb1: {
_5 = move (_9.0: usize); // scope 1 at src/main.rs:28:24: 28:37
_10 = const 3_usize; // scope 1 at src/main.rs:28:20: 28:38
_11 = Lt(_5, _10); // scope 1 at src/main.rs:28:20: 28:38
assert(move _11, "index out of bounds: the length is {} but the index is {}", move _10, _5) -> bb2; // scope 1 at src/main.rs:28:20: 28:38
}
// LOOK AT HERE: We take the mutable reference until now
bb2: {
_4 = &mut _1[_5]; // scope 1 at src/main.rs:28:15: 28:38
_3 = &mut (*_4); // scope 1 at src/main.rs:28:15: 28:38
_2 = increment::<i32>(move _3) -> bb3; // scope 1 at src/main.rs:28:5: 28:39
}
The compiler choose to take the mutable reference just before the invocation of increment
.
However, as for line 28, we have:
// LOOK AT THIS (corresponds to line 28)
bb3: {
_15 = &mut _12; // scope 2 at src/main.rs:30:15: 30:43
_18 = &_12; // scope 2 at src/main.rs:30:29: 30:38
_17 = Arr::len(move _18) -> bb4; // scope 2 at src/main.rs:30:29: 30:38
}
bb4: {
_19 = CheckedSub(_17, const 1_usize); // scope 2 at src/main.rs:30:29: 30:42
assert(!move (_19.1: bool), "attempt to compute `{} - {}`, which would overflow", move _17, const 1_usize) -> bb5; // scope 2 at src/main.rs:30:29: 30:42
}
bb5: {
_16 = move (_19.0: usize); // scope 2 at src/main.rs:30:29: 30:42
_14 = <Arr as IndexMut<usize>>::index_mut(move _15, move _16) -> bb6; // scope 2 at src/main.rs:30:15: 30:43
}
bb6: {
_13 = &mut (*_14); // scope 2 at src/main.rs:30:15: 30:43
_0 = increment::<Arr>(move _13) -> bb7; // scope 2 at src/main.rs:30:5: 30:44
}
We can see the mutable reference is taken firstly, and then the immutable reference used by calculating length, thus leads to a compilation error.
As is also mentioned by @trentj , the reason of the difference is so called "intrinsic indexing", which means the compiler already knows how to index an array so it does not go through the one in library. I also found another two topics related to it:
Edit 2:
I originally came up to this question from this meme:
I found that the range operator for array is not intrinsic. This explains why there is a compilation error in the final frame.