Skip to content

Latest commit

 

History

History
453 lines (287 loc) · 7.83 KB

File metadata and controls

453 lines (287 loc) · 7.83 KB

Below is a precise, compiler-level explanation of what is happening in your Rust program, specifically in terms of ownership, referencing, dereferencing, type inference, and what the compiler enforces.


The Code

fn main() {
    let x = 42;
    let y = &x;  // y is a reference to x
    let z = *y;  // dereferencing y to get the value of x
    println!("{}", z);  // Prints: 42
    println!("{}", y);
    println!("{}", x);
}

Line-by-Line Explanation

1. let x = 42;

What happens conceptually

  • x is a binding to the integer value 42.

Type

x: i32

(i32 is inferred by the compiler as the default integer type.)

Memory

  • x lives on the stack.
  • The value 42 is stored directly in x.

Ownership

  • x owns the value 42.

2. let y = &x;

What happens conceptually

  • You create an immutable reference to x.
  • No new integer is created.
  • No ownership is transferred.

Type

y: &i32

Memory

  • y contains the address of x, not the value 42.
  • x remains unchanged and still owns the data.

Borrowing rules enforced by the compiler

  • x is immutably borrowed.

  • While y exists:

    • Any number of immutable borrows are allowed.
    • No mutable borrow (&mut x) is allowed.

This is enforced at compile time, not runtime.


3. let z = *y;

What happens conceptually

  • *y dereferences the reference.
  • You are saying: “Go to the memory location that y points to and read the value.”

Type transformation

*y: i32
z: i32

Important detail

  • i32 implements the Copy trait.
  • So the value 42 is copied, not moved.

Memory

  • z gets its own independent copy of 42.

  • After this line:

    • x owns 42
    • z owns another 42
    • y still points to x

4. println!("{}", z);

Why this works

  • z is an i32.
  • It implements Display.

Ownership

  • z is passed by copy.
  • No ownership issues.

5. println!("{}", y);

This line often confuses people.

What is actually happening

  • You are not dereferencing explicitly.
  • Rust applies auto-dereferencing for formatting.

Mechanism

  • println! uses the Display trait.

  • There is no Display for &i32, but:

    • Rust sees &i32
    • Automatically dereferences it to i32
    • Prints 42

This behavior is part of Rust’s deref coercion system.

You could write this explicitly as:

println!("{}", *y);

Both are equivalent here.


6. println!("{}", x);

Why this is allowed

  • x was only immutably borrowed.
  • Immutable borrows do not restrict reads.
  • The borrow (y) is still valid here.

Compiler’s View (Key Points)

Types the compiler infers

x: i32
y: &i32
z: i32

Borrow checker guarantees

  • x is never mutated.
  • y never outlives x.
  • No data races are possible.
  • All borrowing rules are validated at compile time.

Memory Diagram (Conceptual)

Stack:

x ──► 42
y ──► &x (points to x)
z ──► 42 (copied value)

Why This Is Safe Without Runtime Cost

  • References are zero-cost abstractions
  • Dereferencing is a simple memory read
  • Borrow checking has no runtime overhead
  • No garbage collector
  • No reference counting

Key Takeaways

  1. &x creates a borrow, not a copy.
  2. *y reads the value behind the reference.
  3. i32 is Copy, so dereferencing produces a copy.
  4. println!("{}", y) works due to auto-dereferencing.
  5. All safety guarantees are enforced at compile time.

If you want, I can:

  • Rewrite this using &mut
  • Show what fails to compile and why
  • Compare this behavior to C++ pointers
  • Explain how this changes with non-Copy types like String

This example is fundamentally different from the previous one, and it does not compile. The reason is entirely about ownership, moves, and dereferencing non-Copy types. I will explain it from the compiler’s perspective.


The Code

fn main() {
    let x = "Hi".to_string();
    let y = &x;  // y is a reference to x
    let z = *y;  // dereferencing y to get the value of x
    println!("{}", z);
    println!("{}", y);
    println!("{}", x);
}

What the Compiler Sees, Step by Step

1. let x = "Hi".to_string();

Type

x: String

Memory

  • x lives on the stack
  • The actual string data ("Hi") lives on the heap
  • x owns the heap allocation

Ownership

  • String is not Copy
  • Ownership matters strictly

2. let y = &x;

Type

y: &String

What this means

  • y is an immutable borrow of x

  • x still owns the String

  • The borrow checker enforces:

    • x cannot be mutably borrowed while y exists
    • x cannot be moved while y exists

So far, everything is valid.


3. let z = *y;

This is the critical line.

What *y means

  • Dereference the reference
  • Attempt to extract the value of type String

So the compiler interprets this as:

let z: String = *y;

Why this fails

  • Dereferencing a reference to a non-Copy type attempts a move
  • Moving a value out of a borrowed reference is forbidden

The compiler error (simplified) is:

cannot move out of *y which is behind a shared reference

This is a compile-time error, not a runtime issue.


Why This Was Allowed for i32 but Not for String

Type Implements Copy Dereferencing Behavior
i32 Yes Value is copied
String No Move attempted → error

In your previous example:

let z = *y; // i32 → copied

Here:

let z = *y; // String → move (illegal)

Core Rule (Very Important)

You cannot move ownership out of a shared reference (&T).

This rule prevents:

  • Use-after-free
  • Double free
  • Dangling pointers

Why the Borrow Checker Rejects This

If Rust allowed this:

let z = *y;

Then:

  • z would own the String
  • x would be left in an invalid state
  • But y still exists and points to x

This would break Rust’s safety guarantees.


How to Fix This (Correct Approaches)

Option 1: Clone the value (explicit and safe)

let z = y.clone();

or

let z = (*y).clone();

Now:

  • Heap data is duplicated
  • Ownership is clear
  • Compiler is satisfied

Option 2: Work with references only

let z = y; // z: &String

No ownership transfer occurs.


Option 3: Deref coercion for read-only use

If you only want to read the value:

println!("{}", y);
println!("{}", x);

Rust automatically dereferences &String to &str.


Why println!("{}", y); Still Works

  • println! needs something that implements Display

  • &String does not implement Display

  • Rust automatically:

    &String → String → &str
    
  • This is deref coercion, not ownership transfer

No move happens here.


Summary in Compiler Terms

  • String is heap-allocated, non-Copy, ownership-based
  • &String is a shared borrow
  • *y attempts a move
  • Moving from a shared reference is illegal
  • The compiler rejects this at compile time

Mental Model to Keep

  • * on &T:

    • Copies if T: Copy
    • Moves if T is not Copy
  • Moves from &T are forbidden

  • Cloning must be explicit


If you want, I can next:

  • Rewrite this using &str instead of String
  • Show the exact compiler error and decode it
  • Compare this with C++ std::string&
  • Explain how Deref trait enables coercions here

Tell me how deep you want to go.