Understanding what happens when initialize a variable as a reference to the return value of a function

I want to make sure I correctly understand what happens when you initialize a variable as a reference to the returned value from a function (i.e. when we prepend & to a function invocation):

fn main() {
    let s1 = &String::from("string1"); // What's happening here?
    let s2 = String::from("string2");

    print_type_of(&s1);
    print_type_of(&s2);
}

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

(Playground)

I've read through a couple different docs on references, but I haven't found anything that directly confirms what happens here.

My understanding is that s1 is initialized on the stack as a pointer to a String struct. Structs are by default created on the stack, so is s1 a reference to a location on the stack? For some reason, this feels strange to me, so I think that's why I'm questioning this and what's happening for s1 feels fuzzy.

For s2, it is just the String struct on the stack, (this struct of course holds a pointer for the UTF-8 bytes on the heap, same as s1).

Is this correct? What are situations where you might want to initialize a variable like how s1 is initialized, versus just using the direct initialization like for s2 and then later using & to make a reference to it as needed?

Why? References don't care where the referent is. It can live on the stack, or on the heap, or in static memory, wherever. Memory is just memory, and a reference is really just a pointer to whatever memory you got access to (albeit with static guarantees around validity).

By the way, your first example is equivalent to

let s1_value = String::from("string1");
let s1 = &s1_value;

This is pretty much a special case or a "superpower" of let statements; in other contexts, a reference to a temporary doesn't outlive the enclosing expression/statement, so this normally wouldn't be allowed. I don't find it particularly useful or clear (if I were to design the language, this wouldn't be permitted), but apparently some people thought that it makes some kinds of code (marginally) easier to write, so this special case is now permitted.

Appreciate the response. Your code snippet clears up that ambiguity I was unsure of. I think I just needed someone to confirm it to be comfortable with it.

Yeah, honestly, there's no logical reason. It's probably some old hold-over reaction from my limited experience with C where if you did this, you would have to worry about whether the stack memory is still valid. Makes no sense to introduce this kind of complexity when you could just copy by value and let compiler scope rules give you compile time guarantees. This intuition just doesn't carry over to Rust because you get those compile time checks around pointers anyway. So like you said, a bit weird to do this in practice, but harmless.

Note that you often need to reference values on the stack in C as well. The most trivial example is multiple return values, which are emulated using "out arguments". There's a lot of code out there that does something like this:

int x, y;
get_coordinates(&x, &y);

simply because there is no way to return a tuple of two ints (and perhaps they were too lazy to declare a struct for this sole purpose). Here x and y live on the stack, and so &x and &y point to the stack, and it's perfectly correct and idiomatic to do this in C.

C++ have similar lifetime extension and Rust is capable of making the code which uses it safe thus it was added to make switch from C++ a tiny bit easier.

But I don't use it often and wouldn't have minded if it were forbidden.