Reference shenanigans with String -> &str

Since not that long ago rust-analyzer has added implicit calls to their hints, i have noticed something puzzles me a bit. When i write something like let foo: &str = &format!("bar") i think i understand what's going on - a reference to String is a &str, makes sense.
At the point where i am i also understand that in reality there's some dark dereference magic happens under the hood and in reality it's more like let foo: &str = &*format!("bar"), where String is getting dereferenced into str, of which i then take reference.

What puzzles me, however, is that the hint that rust-analyzer shows is... let foo: &str = &**&format!("bar"). What... is even going on here? What the heck is &**&?

Trying to go step by step and looking at the type hinds shows:
let foo: String = format!("bar")
let foo: &String = &format!("bar")
let foo: String = *&format!("bar") with a "cannot move out of a shared reference" error. What? Why?
let foo: str = **&format!("bar") also with a much more understandable error of str having an unknown size so i can't just plop into into a stack stored variable. But also thinking about it, how does it know to dereference it like this here?
let foo: &str = &**&format!("bar") and then we arrive to this.

BUT, as soon as i thought i reached the bottom, i thought "hang on, of i get str from **&String, and *& is a moot operation that just takes a reference of value and then gets the value back by dereferencing said value... shouldn't the *String also give me the str, and, by extension, shouldn't &*String also result in &str?
let foo: &str = &*format!("bar") and it does! In fact, let foo: &str = &*format!("bar") and let foo: &str = &**&format!("bar") seem to all have an identical result, except second one being more verbose. And both result in a suggestion to simplify it to let foo: &str = &format!("bar"). With one minor difference - when i write let foo: &str = &*format!("bar") - i do not get an implicit hint telling me that in reality it's a &**&, not just &* like it does with just &.

So... what exactly is going on here? Why two different explicit ways of writing the same reference cast? What's the deal with *& error and why is it a part of that conversion chain? Why isn't the hint about what compiler really is doing the &* since it seem to result in the same outcome as &**& and doesn't produce a hint telling me that in reality there's also this confusing *& happening before the &*? I'm completely and utterly lost.

2 Likes

The first thing you need to understand is places. (If you've heard of “lvalues” from other languages, it's the same idea.) A place is some piece of memory designated by some Rust expression. The thing on the left side of = is a place expression, so the most common situation where you are doing things with places is assignments:

let mut x = 1;
x = 2;     // x is a place

let mut x = 1;
let xref = &mut x;
*xref = 2;     // *xref is a place

*"bar" is a place of type str. You can't store it in a variable because it doesn't have a fixed size, but you can still take a reference to it — putting & or &mut before any place gives you a reference to it.

The reason you're getting weird-seeming results with * is mostly that some places are unsized and so &*foo is valid in some situations where *foo isn't.

let foo: String = *&format!("bar") with a "cannot move out of a shared reference" error. What? Why?

Because using * on a place in a regular value context, like the right side of =, means to move or copy out of the place. String isn't Copy so this isn't a copy, and you can't move out of an &, so this is an error.

In general, *&foo is the same as foo if it succeeds. But there are reasons it can fail.

4 Likes

You are likely confusing Deref coercions with regular referencing/dereferencing.

In the absence of Deref coercions, & and * are inverses of each other: *&value designates the value itself, and &*reference is the same as reference. If Deref coercions come into play, then *reference might actually mean *reference.deref() (and so &*reference might mean reference.deref()) sometimes. It's a "sometimes" because explicit type annotations (among other things) can influence whether the compiler inserts a Deref coercion.

As you observed, it's just a completely normal chain of reference and de-reference operations.

You are not allowed to move out of a reference, because then the reference would point to invalid memory, which isn't allowed by the language.

What do you mean by "shouldn't it"? It totally does.

I don't think anything like that is happening. Rust-analyzer is not the compiler, and its analyses are markedly not identical to those of the compiler. It's likely being overzealous in this case. In a simple deref coercion, the conversion &String -> &str happens automatically, which involves no dereferencing of a "real" reference, it's desugared to something like Deref::deref(&the_string) or the_string.deref().

3 Likes

Oh, so it's just rust-analyzer being weird? I was under impression they use the data provided by the compiler to do things type hints and coercion hints

They might in fact be using the compiler. I'm not too familiar with the exact internals and implementation details of rust-analyzer, but it is my impression that type mismatches, incorrect suggestions, and a whole range of other typing-related bugs are significantly more frequent than what I would expect from software that is not re-implementing type checking.

It might "only" be an issue with presentation and/or parsing the compiler's output, however I regularly encounter questions on URLO whereby people who use rust-analyzer are confused, because it suggests/shows complete bollocks, whereas suggestions and errors coming from a command line invocation of cargo check (on identical code) are at least technically correct (even if not always perfectly helpful).

The fact that I have never seen &**& suggested by the compiler before corroborates my claims.

1 Like

No, that was RLS.

As I understand it, R-A is a separate implementation designed to be more incremental and more tolerant of incorrect code, so that it works better for update-as-you-type than rustc does.

1 Like