Am i right to say that in the case of reference, y points to x which sits in the stack while in the case of smart pointer y points to a copied value in the heap and not x that sits in the stack?
Is there any way in Rust that i can tell whether a value is in the heap or stack?
As a general rule of reference (no pun intended), if it's behind a memory structure of some sort (like a reference, or a smart pointer) but it has no lifetimes attached, or a 'static lifetime, then, when there is no unsafe code to possibly mangle lifetimes, you can safely assume that it is living somewhere that is not on the stack. Another thing to note, is that if there is simply no lifetime attached, it can be considered "owned", and whether it is on the stack of the heap is dependent on the structure itself and it's internal implementation (With the most basic example being Box<usize> vs usize.)
What does this mean?
Your both of your examples are correct, but there is a bit of an asterisk to attach, if you've got a &'a T, meaning a reference to a T where the underlying value lives for as long or longer than 'a, then you usually can't know if the T is in the stack or heap without interacting with the T (eg, adding a marker when it's created).
Some of my statements are a bit convoluted, or you might not have touched on them yet, so please let me know if you need some clarification.
I always thought that primitives like u32 exists on the stack because the memory requirement is known at compile time. A smart pointer like Box is to provide some kind of indirection to point to some value on the heap so that at compile time Rust knows the memory requirement of y. Hence I thought y reference is a pointer pointing to a value on a stack and y smart pointer is a pointer pointing to a value on the heap.
//x is on stack
let x = 32usize;
//x is moved to heap
let boxed = Box::new(x);
//we reference value in heap
let reference = &*boxed;
referenceandboxed both refer to the same value, which is on the heap, because it is owned by Box.
You can see that they both point to it by printing them out as values:
let x = 32usize;
let boxed = Box::new(x);
let referenced = &*boxed;
println!("{:p}\n{:p}", boxed, referenced);
Usually smart pointer's smartness happens on runtime, i.e unique_ptr deallocates itself on destructor, shared_ptr increase its reference counter on copy and decrease it on destruct etc. But for Rust's references, they're always represented as dumb pointer at runtime. Instead, all its smartness happens in compile time. The Rust compiler check lifetime of all those references and their referent and tries to mathematically prove 1. no reference outlives its referent(to prevent dangling reference) and 2. no other reference coexist while its unique reference(&mut) alive (to prevent mutable alias and thus data race). If it fails, that's just compile error. Why this matter? Because it's all happens on compile time, there's no runtime performance cost, while still provides memory safety like GCed languages.