Confusing Rust Magic around Indexing operation

To quote the Rust Reference:

For other types an index expression a[b] is equivalent to *std::ops::Index::index(&a, b) , or *std::ops::IndexMut::index_mut(&mut a, b) in a mutable place expression context.

Okay, so Rust auto-dereferences index operations. Cool. This fails expectedly:

fn main() {
    let s = String::from("abc123");
    
    let a = s[3..]; // "str is unsized" error
}

Throws:

error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:6:9
  |
6 |     let a = s[3..];
  |         ^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `str`
  = note: all local variables must have a statically known size
  = help: unsized locals are gated as an unstable feature
help: consider borrowing here
  |
6 |     let a = &s[3..];
  |             ^

No surprises there! Okay, let's fix it and also make this example a little bit more interesting:

fn main() {
    let s = String::from("abc123");
    
    let a = &s[3..]; // fixed
    let b = a.parse::<i32>();
    assert_eq!(Ok(123), b); // passes
}

But it gets weird if I try to combine statements a and b into a single statement c:

fn main() {
    let s = String::from("abc123");
    
    let a = &s[3..]; // fixed
    let b = a.parse::<i32>();
    assert_eq!(Ok(123), b); // passes
    
    let c = &s[3..].parse::<i32>();
    assert_eq!(Ok(123), c); // fails!?
}

Throws:

error[E0308]: mismatched types
  --> src/main.rs:11:5
   |
11 |     assert_eq!(Ok(123), c);
   |     ^^^^^^^^^^^^^^^^^^^^^^^ expected enum `Result`, found reference
   |
   = note:   expected enum `Result<{integer}, _>`
           found reference `&Result<i32, ParseIntError>`

"Expected Result, found reference"? What reference? Where? After some trial and error I managed to fix the issue problem by dropping the leading & from statement c to create the correct statement d below:

fn main() {
    let s = String::from("abc123");
    
    let a = &s[3..]; // fixed
    let b = a.parse::<i32>();
    assert_eq!(Ok(123), b); // passes
    
    let c = &s[3..].parse::<i32>();
    // assert_eq!(Ok(123), c); // fails!?
    
    let d = s[3..].parse::<i32>(); // why no more "str is unsized" error!?
    assert_eq!(Ok(123), d); // passes again
}

The above code compiles and passes all assertions but why is there no "str is unsized" error in statement d like there was in our very first example of statement a back at the top of this post!?

Can someone please help make this make sense? Thank you.

To start off, &s[3..].parse::<i32>() is parsed as this:

let c = &(s[3..].parse::<i32>());

If you actually want the & applied directly to the s[3..], you have to do it like this:

let c = (&s[3..]).parse::<i32>();

This will compile.

As for why s[3..].parse::<i32>() is ok, I would like to first point your attention to this similar question:

Why doesn't &s[3..] give an "str is unsized" error? That expression first dereferences the s to get an str, then takes a reference to it, so why isn't that intermediate dereferenced value a problem?

The answer to this similar question is that, yes, the expression refers to an unsized value by dereference, but that unsized value is used in a way that only needs to immutably borrow it, so the str is only immutably borrowed it, not copied.

It's the same in your case. The s[3..] expression refers to an unsized value of type str, but you then proceed to use it in a way (call parse on it) that only needs an immutable borrow of it, so only an immutable borrow is taken.

2 Likes

By the way, this is for the same reason as why writing e.g. v.len() (with a local variable v: Vec<…>) doesn’t try to move out of the varliable v. Writing v in and by itself does only refer to v, it doesn’t immediately try to take ownership of the value of v.

Sometimes you want to take ownership explicitly, this is when blocks can come in handy, since the return value of a block is always moved. You can write {v}.len() and it will move out of v and drop it at the end of the statement. Similarly, if you write {s[3..]}.parse::<i32>() your example won’t compile anymore. And writing {s}[3..].parse::<i32>() would move out of s.


In general, the technical distinction is between so-called “value expressions” and “place expressions”. Certain kinds of expressions are “place expressions” [link to reference] (also sometimes or historically called “lvalue expressions”),

A place expression is an expression that represents a memory location. These expressions are paths which refer to local variables, static variables, dereferences ( *expr ), array indexing expressions ( expr[expr] ), field references ( expr.f ) and parenthesized place expressions. All other expressions are value expressions.

and certain contexts have the ability to treat place expressions differently without requiring the expression to evaluate to an owned value before

The following contexts are place expression contexts:

One of these contexts requires a place expression, namely the assignment and compound assignment expression’s left operand. All the others work with value expressions, too, which are placed in a temporary and that temporary is referenced, if necessary. Some of these cases only work with place expressions that you can’t move out of under certain circumstances. For example not all of the expressions supporting implicit borrowing always do the implicit borrowing, and the let or match or if let statements can have patterns that require evaluation to an owned value anyways. (The most common example would be let x = … where the pattern x does require a value. OTOH, e.g. let _ = … works with any place expression, e.g. let _ = s[3…]; in the context of your example.)

In your concrete case then, there’s the array indexing expression s[3..] which is a place expression, and you use it in a method call expression which does support implicit borrowing (unless the method in question doesn’t take &self or &mut self argument). Similarly the expression s itself is a place expression and array indexing is also a place expression context, so that s[3..] itself doesn’t try to move out of s either.

And if you want to understand the array indexing expression in terms of the *Index::index(&s, ix) or *IndexMut::index_mut(&mut s, ix) then place expression context for s is a borrow operator and the whole expression itself is a place expression because it’s a dereference.

3 Likes