Does the indirection of a pointer immediately create a reference?

Good to learn something new. I have no idea why or what practical purpose is.
Note the read() returns the ZST it is not addressed at 0.
I wonder if something (Option maybe) would break if references to ZST could be null. At least this is not allowed let _ = unsafe { &*std::ptr::null::<()>() };

A reference can never be null, even for ZSTs.

Seems true. I remembered the rules were adjusted to make null ZST read/writes non-UB, but when I looked at std::ptr - Rust I saw they still are. But it appears the rules got tweaked again (refactor 'valid for read/write' definition: exclude null by RalfJung · Pull Request #152615 · rust-lang/rust · GitHub), so that read() is not UB but the pointer is still not valid.

A solid chunk of this discussion seems to gloss over the basics: the CPU instructions that end up being executed. Your source code is lexed into tokens, parsed into an AST, lowered into an HIR, into THIR, into MIR, into LLVM's IR; codegen'ed/linked into an executable file you get to run. By the end, it has no "clue" what a "reference" even is, much less how to "create" it.

At assembly level, there is no concept of "taking a reference", or "dereferencing a pointer", or even "assigning" an x into the y via a let y = x or similar. You can only "copy" individual bytes: from the stack into a register, from the register back onto the stack, from the stack to a given address in memory (RAM), from memory back into the register.

A typical let x = y assignment translates into "copy all the bytes from the register/memory address 'assigned' to y into the register/memory address 'given' to x". That's it.

If the y is a ZST, no copying takes place. (Can you guess why?)

fn main() {
    type ZST = ();
    let ptr_zst = std::ptr::null() as *const ZST;
    unsafe {
        let zst = *ptr_zst; // no UB
        assert_eq!(zst, ());
    }
}

If the y is zero-length slice, no copying happens.

fn main() {
    let ptr = std::ptr::null() as *const String;
    let ptr_slice = std::ptr::slice_from_raw_parts(ptr, 0);
    unsafe {
        let slice = &*ptr_slice; // no UB
        let mut vec = Vec::new();
        vec.clone_from_slice(slice); // no UB either
        assert!(vec.is_empty());
    }
}

Any typical let x = y assignment is, for our intents and purposes, a plain std::ptr::copy:

fn main() {
    let x = 1234;
    let y = x;
    // translates into:
    let mut y: i32;
    unsafe {
        // conceptually speaking
        std::ptr::copy(&x, &mut y, 1);
    }
}

Any let x = *y no longer needs to take the address of y. It uses the value it holds:

fn main() {
    let x = *y;
    // translates into:
    let mut x;
    std::ptr::copy(y, &mut x, 1);
}

Any let _ = *x removes the destination from your std::ptr::copy entirely. Without knowing where the bytes located at the address in the x must go; no copying can take place. Thus:

fn main() {
    let ptr = std::ptr::null() as *const i32;
    unsafe {
        _ = *ptr;
        let _ = *ptr;
        // both of these *could* translate into
        // an invalid operation the compiler would need
        // to manually throw away in one of it's lowering steps
        std::ptr::copy(ptr, <n/a>, 1);
        // yet it is (usually) smart enough
        // to infer that `_ = x` means "evaluate x,
        // "then forget about it altogether". so it does 
        // *nothing* with the *address* it "reads from" the `ptr`,
        // and no UB can ever happen as a result of that
    }
}

And rightfully so, because this absolutely doesn't matter.

When talking about UB (or operational semantics in general), you are not programming for your CPU. You are programming for the Abstract Machine.

No AM exists in a vacuum. If you don't (care to) understand the underlying mechanics, you'll have little to no/context as to why the compiler's UB/semantics are defined the way they are.

Unless you happen to suggest we should rewrite the book from ground up to focus exclusively on steering the abstract machine away from any UB - since it's the AM and its own constraints we are programming for, and should therefore be concerned with - as opposed to talking about software design, architecture, the underlying OS abstractions/primitives, hardware constraints, cache lines/misses, as much as what the language allows for, and so on; saying:

when the original question asks about what's actually transpiring under the hood:

is ever so slightly dismissive, if not blindly dogmatic outright.

Operational semantics design very rarely stems only from machine semantics. More commonly it is impacted by compiler optimizations etc..

When talking about performance, for example, it absolutely makes sense to talk about the lowered machine code. But not when talking about UB.

I can somewhat understand how OP's question can be interpreted like that, but I'm sure it was not the meaning and frankly interpreting it like that is pretty far-fetched. A "reference" (in the Rust sense) is not something in machine code, even pointers are not really. You just load from an address. In machine code, there is no difference between creating a reference from a pointer (NOP) and reading from it, versus directly reading.

Select "Tools > Miri" in the top-right corner. I get this:

   Compiling playground v0.0.1 (/playground)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.46s
     Running `/playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/playground`
error: Undefined Behavior: constructing invalid value of type &[std::string::String]: encountered a null reference
 --> src/main.rs:5:21
  |
5 |         let slice = &*ptr_slice; // no UB
  |                     ^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

To clarify: UB does not necessarily mean it does something the CPU doesn't like. The Rust abstract machine defines its own rules, not tied to any particular hardware. Violating those rules is UB, regardless of the actual machine code generated.

I'm confused. Creating a null reference is UB, even ZST, like Miri says. Why does the comment say it's "no UB"?

I was demonstrating that the comment was incorrect, as you pointed out.

It doesn't matter what the CPU does. The code is first "run" by the optimizer on an abstract machine that does whatever it's defined to do. In practice a lot of LLVM's behavior is entirely CPU-independent (IR doesn't make programs portable, but handling of individual IR instructions can be implemented universally).

The feature set and capabilities of this abstract machine can significantly differ from what the CPU actually does, which is why you get seemingly random incoherent and paradoxical results from UB - because some of the code has been evaluated on the abstract machine by its rules, and some runs on the CPU, each behaving differently. You get coherent result only for the defined subset that LLVM can guarantee to be the same on the CPU and on the abstract machine.

So, what's the eventually conculusion here? Does *ptr create a reference?

Do you strictly mean reference, then probably not; the problem is we are saying value expressions are (mostly) just like references. On its own it may not be a value expression, instead being a place expression which allows more functionality. As linked by @theemathas it is dependent on how it is used.

it depends on the type of ptr.
if ptr is a &[mut] or * (const|mut) , then no, it simply creates a place expression to the referent.
if ptr is another type, that implements Deref[Mut], then it does create a reference to ptr, and as many such references as the Deref[Mut] impl dictates, before resolving to a place expression to the ultimate referent.
because Deref[Mut] returns references and not raw pointers, that does mean it always creates a reference to the ultimate referent.

Hi, can someone please explain why the first is not UB, but the second block is UB?

A block moves out of a place inside it, and unsafe { ... } is a block.

BTW you can see it without any unsafe:

fn foo(v: &String) {
    { _ = *v }; // Compiles
    _ = { *v }; // Does not compile, `cannot move out of `*v`...
}

When you do _ = ... or let _ = ... (or any other use of _ as a binding in a pattern, such as in match ... { _ => () }), it does precisely nothing with its value. let _ = x doesn't drop x, move x, or do anything else to x; it "evaluates" it, then ignores it.

So let _ = *ptr doesn't do anything; *ptr is "evaluated" to a place expression, but nothing reads from it. However, a block expression ({ ... } or unsafe { ... }) has to "evaluate" to a value, not a place expression. So unsafe { *ptr } is forced to dereference ptr, triggering UB if ptr isn't valid for reads.

A more visible example of place expressions "evaluating" to values is when you're trying to write to a reference:

let mut x = 1;
let y = &mut x;

// works just fine; `*y` here is a place expression, not a value
*y = 2;

// doesn't work; `{ *y }` is a value, not a place expression
{ *y } = 3;

(if any of this is technically incorrect, please do correct me)

Thank you for the short explanation. I should have read the linked blog article mentioned in one of the previous posts :slight_smile: