I am interested in understanding the fundamentals of rust. For this question I use a native array [i32;3]. My question is not about using an array, it is about what the compiler does.
let mut a = [1, 2, 3];
let a1 = a[1];
//no type coercion seems involved...
println!("a1 is a {}", std::any::type_name_of_val(&a1));//a1 is a i32
println!("a[1] is a {}", std::any::type_name_of_val(&a[1]));//a[1] is a i32
a[1] = 3;//finally, how is this legal ?
When used as a right-side expression, a[1] is not a reference, it is the target type.
What is the difference for the rust compiler when using a[1] as a left-side expression ? Which rule is applied to mean that we are modifying a reference to the emplacement 1 of a ?
The general answer is that a[i] desugars (essentially) to *a.index(i) or *a.index_mut(i) -- note the * there, which makes it a place instead of just an rvalue.
(For arrays and slices, indexing via usize is actually built-in, so doesn't go through the trait, but that's how it works for the general ones.)
I think you figured it out since you cited the reference, but indexing, like dereferencing, results in a place expression. That's why you can assign to it, copy out of it, etc.
For both indexing and dereferencing, there are some types where the operations are built-in (and somewhat magical), and for all other types, it's via a trait.
@chacha21 note that the "somewhat magical" properties of the built-in indexing operator on arrays are irrelevant for the kinds of example code that you've shown. This example code works just as well e.g. with a Vec<i32> which does not use any "somewhat magical" built-in implementation anymore. (Here's the behavior after "desugaring".)
The "somewhat magical" properties of built-in indexing do mainly come in the form of some subtle extra information the borrow checker can know when no custom code is involved. In particular partial borrows don't need access to the whole array, so e.g. a borrow of a[i].field1 and a[j].field2 is not in conflict (even if i and j might be the same index).