rust-job-prep / phase-01 / day-01.rs
PHASE 01  |  DAY 01 / 15
// rust job ready series — phase 01: core rust

Ownership
& Borrowing

$ rustc day01.rs  →  zero_cost_memory_safety
Day 01 move copy clone references borrow checker lifetime

Ownership is Rust's core memory management model — every value has exactly one owner, and when the owner goes out of scope, the value is automatically dropped (freed). No garbage collector. No manual free. Enforced entirely at compile time.

Borrowing lets you use a value without taking ownership. You can have either any number of immutable references (&T) or exactly one mutable reference (&mut T) — never both at the same time. The borrow checker enforces this statically.

RULE 01

Single Owner

Each value in Rust has exactly one variable that owns it at any given time.

RULE 02

Move on Assignment

When ownership is assigned to another variable, the original variable is invalidated — this is a move. Types that implement Copy are the exception.

RULE 03

Drop on Scope Exit

When the owner goes out of scope, Rust automatically calls drop(), freeing heap memory. Zero runtime cost.

USE CASE Heap data (String) cannot have two owners — assigning s1 to s2 transfers ownership instead of copying, so the original variable is invalidated.
day01_move.rs
MOVE
// String is heap-allocated → ownership MOVES, not copies
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;          // s1 is MOVED into s2
    // println!("{}", s1); // ❌ ERROR: value borrowed after move
    println!("{}", s2);   // ✅ s2 is the owner now
}
USE CASE Stack-only scalar types (i32, bool, f64, char) implement Copy — assignment silently duplicates the bits, so both x and y stay valid.
day01_copy.rs
COPY
// Scalar types (i32, bool, f64, char) implement Copy — stack-only
fn main() {
    let x: i32 = 42;
    let y = x;            // COPY — x is still valid
    println!("{} {}", x, y); // ✅ both valid
}
USE CASE When you genuinely need two independent copies of heap data, call .clone() explicitly — Rust never deep-copies silently, so the cost is always visible in the source.
day01_clone.rs
CLONE
// .clone() deep-copies heap data — explicit, intentional cost
fn main() {
    let s1 = String::from("world");
    let s2 = s1.clone();  // deep copy of heap data
    println!("{} {}", s1, s2); // ✅ both valid, both own separate memory
}
USE CASE Pass &String when a function only needs to read, and &mut String when it needs to mutate — neither takes ownership, so the caller keeps using the value afterwards.
day01_borrow.rs
BORROW
// Immutable borrow — many readers, no mutation
fn print_len(s: &String) {   // borrows, does NOT take ownership
    println!("len = {}", s.len());
}

// Mutable borrow — exclusive writer
fn push_str(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("hello");
    print_len(&s);           // immutable borrow
    push_str(&mut s);       // mutable borrow
    println!("{}", s);      // ✅ "hello, world"
}
USE CASE This snippet shows what the compiler rejects: an active mutable borrow (r3) cannot coexist with active immutable borrows (r1, r2) on the same value.
day01_conflict.rs
COMPILE ERROR
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;             // immutable borrow ✅
    let r2 = &s;             // another immutable borrow ✅
    let r3 = &mut s;        // ❌ cannot borrow as mutable — r1, r2 still active
    println!("{} {} {}", r1, r2, r3);
}
// my own examples — stack & heap visualizer
USE CASE — MY EXAMPLE Print the stack address of the String struct itself vs the heap address its internal pointer points to — proves a String is a stack-allocated struct (ptr, len, cap) pointing into the heap.
day01_my_stack_heap_look.rs
STACK vs HEAP
//  time for ownership
#[allow(dead_code)]
pub fn how_stack_heap_looks(){
    let s1 = String::from("hello");
    println!("address of s1 (stack struct)  : {:p}", &s1);
    println!("ptr inside s1 (heap address)  : {:p}", s1.as_ptr());
}
UNDER THE HOOD
  • &s1 prints the address of the String struct on the stack — this struct is just 24 bytes: a pointer, a length, and a capacity.
  • s1.as_ptr() prints the address that the struct's internal pointer holds — this is where the actual bytes h e l l o live, on the heap.
  • These two addresses will always be different — one is "where the box is", the other is "where the box points to".
USE CASE — MY EXAMPLE Side-by-side comparison: .clone() allocates a brand-new heap block (two different as_ptr() addresses), while a move (s2 = s1) transfers the same heap block — s2.as_ptr() equals what s1.as_ptr() was, proving no copy happened.
day01_my_clone_vs_move.rs
CLONE vs MOVE
#[allow(dead_code)]
pub fn compare_stack_heap_clone_version() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("=== CLONE VERSION (s2 = s1.clone()) ===");
    println!("STACK                              HEAP");
    println!("┌──────────────────────┐");
    println!("│ s1 @ {:p}  │", &s1 as *const String);
    println!("│  ptr  ──────────────┼──────► ┌──────────────────┐");
    println!("│  len  = {:<13} │        │ {:p}   │", s1.len(), s1.as_ptr());
    println!("│  cap  = {:<13} │        │ h e l l o        │", s1.capacity());
    println!("└──────────────────────┘        └──────────────────┘");

    println!("┌──────────────────────┐");
    println!("│ s2 @ {:p}  │", &s2 as *const String);
    println!("│  ptr  ──────────────┼──────► ┌──────────────────┐  (different heap block)");
    println!("│  len  = {:<13} │        │ {:p}   │", s2.len(), s2.as_ptr());
    println!("│  cap  = {:<13} │        │ h e l l o        │", s2.capacity());
    println!("└──────────────────────┘        └──────────────────┘");
}

#[allow(dead_code)]
pub fn compare_stack_heap_move_version() {
    let s1 = String::from("hello");

    let s1_stack_addr = &s1 as *const String;
    let s1_heap_addr  = s1.as_ptr();
    let s1_len = s1.len();
    let s1_cap = s1.capacity();

    let s2 = s1; // s1 moved, can't use s1 after this

    println!("=== MOVE VERSION (s2 = s1) ===");
    println!("STACK                              HEAP");
    println!("┌──────────────────────┐");
    println!("│ s1 @ {:p}  │  (s1 moved — no longer valid)", s1_stack_addr);
    println!("│  ptr  ──────────────┼──────► ┌──────────────────┐");
    println!("│  len  = {:<13} │        │ {:p}   │", s1_len, s1_heap_addr);
    println!("│  cap  = {:<13} │        │ h e l l o        │", s1_cap);
    println!("└──────────────────────┘        └──────────────────┘");

    println!("┌──────────────────────┐");
    println!("│ s2 @ {:p}  │", &s2 as *const String);
    println!("│  ptr  ──────────────┼──────► ┌──────────────────┐  (SAME heap block as s1)");
    println!("│  len  = {:<13} │        │ {:p}   │", s2.len(), s2.as_ptr());
    println!("│  cap  = {:<13} │        │ h e l l o        │", s2.capacity());
    println!("└──────────────────────┘        └──────────────────┘");
}

#[allow(dead_code)]
pub fn compare_stack_heap(){
    compare_stack_heap_clone_version();
    println!("------------------------------------------");
    compare_stack_heap_move_version();
}
UNDER THE HOOD
  • Clone: s1 and s2 each get their own 24-byte stack struct AND their own heap allocation — s1.as_ptr() != s2.as_ptr(). Two separate "hello" strings exist in memory.
  • Move: s2.as_ptr() equals the heap address that s1 used to point to. The bytes h e l l o never moved — only the ownership (the stack struct's bookkeeping) was transferred to s2.
  • This is why move is O(1) — it's just copying 24 bytes (ptr/len/cap) on the stack, never touching the heap data.
  • s1_stack_addr, s1_heap_addr etc. are captured before the move because s1 becomes invalid right after let s2 = s1;.
USE CASE — MY EXAMPLE A single function walking through all 3 ownership rules in order — single owner, move on assignment, and drop on scope exit — with the exact compile errors commented inline.
day01_my_three_rules.rs
3 RULES DEMO
#[allow(dead_code)]
pub fn ownership_with_3_rules() {

    // ---- Rule 1: Each value has one owner ----
    let s1 = String::from("hello");
    println!("Rule 1: s1 is the owner of \"hello\" -> {}", s1);

    // ---- Rule 2: Only one owner at a time ----
    let s2 = s1; // ownership moves from s1 to s2
    // println!("{}", s1); // ❌ ERROR: s1 was moved, no longer valid
    println!("Rule 2: ownership moved -> s2 = {}", s2);

    // ---- Rule 3: When owner goes out of scope, value is dropped ----
    {
        let s3 = String::from("world");
        println!("Rule 3: s3 is alive inside this scope -> {}", s3);
    } // s3 goes out of scope here -> heap memory dropped/freed
    // println!("{}", s3); // ❌ ERROR: s3 does not exist outside this scope

    println!("Rule 3: s3 has been dropped, cannot use it here");
}
UNDER THE HOOD
  • Rule 1: s1 is the single owner of the heap-allocated "hello" right after creation.
  • Rule 2: let s2 = s1; moves ownership — the compiler marks s1 as "moved out" in its borrow-check graph. Any later use of s1 is a compile-time error, not a runtime one.
  • Rule 3: the inner { } block creates a new scope. When it ends, s3's Drop::drop() runs automatically, freeing "world"'s heap memory — deterministically, with zero GC.
USE CASE — MY EXAMPLE Print the stack addresses of two i32 values after assignment — proves Copy types get their own independent stack slot, unlike a move where the original is invalidated.
day01_my_copy_vs_move.rs
COPY vs MOVE
#[allow(dead_code)]
pub fn copy_vs_move_demo() {

    // ---- COPY TYPES (i32, bool, f64, char etc.) ----
    let x = 5;
    let y = x; // COPY, not move — i32 is a Copy type

    println!("addres of both: {:p}\n{:p}", &x, &y);

    println!("Copy type: x = {}, y = {}", x, y); // ✅ both valid

    // ---- MOVE TYPES (String, Vec, etc.) ----
    let s1 = String::from("hello");
    let s2 = s1; // MOVE — s1 is no longer valid

    // println!("{}", s1); // ❌ ERROR: value borrowed here after move
    println!("Move type: s2 = {}", s2);
}
UNDER THE HOOD
  • &x and &y print two different stack addresses — even though y = x, Rust gave y its own 4-byte slot and bitwise-copied 5 into it.
  • Both x and y remain valid and independent — mutating one (if mutable) would never affect the other.
  • Contrast with s2 = s1 below it: no new heap allocation happens, ownership of the same heap block just transfers, and s1 becomes unusable.
// my own examples — borrowing & references
USE CASE — MY EXAMPLE Visualize what a reference (&String) actually is in memory: the reference r itself lives on the stack and stores the address of s1 — it's a pointer to a pointer.
day01_my_what_is_reference.rs
REFERENCE ANATOMY
#[allow(dead_code)]
pub fn step1_what_is_reference() {
    let s1 = String::from("hello");
    let r = &s1; // r is a REFERENCE to s1

    println!("=== WHAT IS A REFERENCE ===");
    println!("STACK                              HEAP");
    println!("┌──────────────────────┐");
    println!("│ s1 @ {:p}  │", &s1 as *const String);
    println!("│  ptr  ──────────────┼──────► ┌──────────────────┐");
    println!("│  len  = {:<13} │        │ {:p}   │", s1.len(), s1.as_ptr());
    println!("│  cap  = {:<13} │        │ h e l l o        │", s1.capacity());
    println!("└──────────────────────┘        └──────────────────┘");

    println!("┌──────────────────────┐");
    println!("│ r  @ {:p}  │  (r itself lives on stack)", &r as *const &String);
    println!("│  value = {:p} ───────┼──────► points to s1 (above)", r);
    println!("└──────────────────────┘");

    println!();
    println!("s1 still usable   : {}", s1); // ✅ s1 valid hai
    println!("r usable too      : {}", r);  // ✅ r se bhi access ho raha
}
UNDER THE HOOD
  • &r as *const &String prints the stack address of r itself — r is its own variable, occupying its own stack slot (8 bytes, a pointer).
  • r's value is the address of s1 — i.e. r doesn't store "hello", it stores "where s1 lives".
  • Both s1 and r remain valid afterwards — borrowing never moves or invalidates the original.
  • This is the core mental model: a reference is a non-owning pointer with compiler-enforced lifetime guarantees.
USE CASE — MY EXAMPLE Pass &s1 into a function and print addresses inside the function — proves the function receives a reference to the same heap data, not a copy, and ownership returns to the caller untouched.
day01_my_borrow_in_function.rs
IMMUTABLE BORROW
#[allow(dead_code)]
fn print_len(s: &String) {
    println!("--- inside print_len() ---");
    println!("address of s (param) : {:p}", &s as *const &String);
    println!("s points to          : {:p}", s);          // same as &s1 in main
    println!("s.as_ptr() (heap)    : {:p}", s.as_ptr());  // same heap as s1
    println!("len via reference    : {}", s.len());
}

#[allow(dead_code)]
pub fn step2_borrow_in_function() {
    let s1 = String::from("hello");

    println!("=== BEFORE FUNCTION CALL ===");
    println!("s1 @ {:p}", &s1 as *const String);
    println!("s1.as_ptr() = {:p}", s1.as_ptr());

    print_len(&s1); // borrow — ownership NOT moved

    println!("=== AFTER FUNCTION CALL ===");
    println!("s1 still usable: {}", s1); // ✅ s1 valid hai
}
UNDER THE HOOD
  • s.as_ptr() inside print_len prints the exact same heap address as s1.as_ptr() in main — no data was duplicated when passing &s1.
  • &s as *const &String is the address of the parameter s itself — a fresh stack slot holding a pointer to s1.
  • After the call, s1 is still printable in main — borrowing a function parameter never transfers ownership.
USE CASE — MY EXAMPLE Pass &mut s1 and call .push_str() inside the function — shows the heap pointer can stay the same or change (reallocation) depending on whether capacity was exceeded.
day01_my_mutable_borrow.rs
MUTABLE BORROW
#[allow(dead_code)]
fn push_world(s: &mut String) {
    println!("--- inside push_world() (BEFORE push) ---");
    println!("address of s (param) : {:p}", &s as *const &mut String);
    println!("s points to          : {:p}", s);
    println!("s.as_ptr() (heap)    : {:p}", s.as_ptr());
    println!("value via reference  : {}", s);

    s.push_str(", world");

    println!("--- inside push_world() (AFTER push) ---");
    println!("s.as_ptr() (heap)    : {:p}", s.as_ptr()); // same ya different?
    println!("value via reference  : {}", s);
}

#[allow(dead_code)]
pub fn step3_mutable_borrow() {
    let mut s1 = String::from("hello");

    println!("=== BEFORE FUNCTION CALL ===");
    println!("s1 @ {:p}", &s1 as *const String);
    println!("s1.as_ptr() = {:p}", s1.as_ptr());
    println!("s1.capacity() = {}", s1.capacity());
    println!("value = {}", s1);

    push_world(&mut s1); // mutable borrow

    println!("=== AFTER FUNCTION CALL ===");
    println!("s1.as_ptr() = {:p}", s1.as_ptr());
    println!("s1.capacity() = {}", s1.capacity());
    println!("value = {}", s1); // ✅ s1 modified
}
UNDER THE HOOD
  • "hello" has length 5 but String::from often allocates capacity 5 too — so push_str(", world") needs 12 bytes total, exceeding capacity.
  • When capacity is exceeded, Rust reallocates a bigger heap block, copies the old bytes over, frees the old block — so s.as_ptr() BEFORE and AFTER push can differ.
  • This is the hidden cost of push_str/push on a String that's at capacity — good to know for performance discussions (pre-allocate with String::with_capacity()).
  • Either way, the mutation is visible back in main through s1&mut gives exclusive write access to the same owner's data.
USE CASE — MY EXAMPLE Demonstrates Non-Lexical Lifetimes (NLL)r1 and r2 are immutable borrows that are "dead" after their last use, so r3 = &mut s1 is allowed even though r1/r2 are still in lexical scope.
day01_my_nll_borrow_rules.rs
NLL DEMO
#[allow(dead_code)]
pub fn step4_borrow_rule_violation() {
    let mut s1 = String::from("hello");

    let r1 = &s1;       // immutable borrow #1
    let r2 = &s1;       // immutable borrow #2 — ✅ OK, multiple & allowed
    //  let r3 = &mut s1;   // mutable borrow — ❌ ERROR expected

    println!("r1: {}, r2: {}", r1, r2);

    let r3 = &mut s1;   // mutable borrow — will pass by NLL (Non-Lexical Lifetimes)

    r3.push_str(", world");
    println!("r3: {}", r3);
    println!("s1: {}", s1);

    // let r3 = &mut s1; // ❌ ERROR expected
    // r3.push_str(", world");
    // println!("r1: {}, r3: {}", r1, r3);

    println!("s1: {}", s1);
}
UNDER THE HOOD
  • Old Rust (pre-2018, lexical lifetimes): r1 and r2 would be considered "alive" until the end of the enclosing { } block — so r3 = &mut s1 would be a hard error.
  • NLL (current Rust): the borrow checker tracks the actual last use of r1/r2 (the println! right after them). Once that line executes, their borrows end — r3 can now safely take a mutable borrow.
  • This is why println!("r1: {}, r2: {}", r1, r2); must come before let r3 = &mut s1; — reorder it after, and the compiler rejects it.
  • NLL makes Rust feel less restrictive without weakening the actual safety guarantee — it's a smarter scope analysis, not a relaxed rule.
USE CASE — MY EXAMPLE The two borrowing rules in one function: (1) many &T OR one &mut T, and (2) references must never danglevalid_ref cannot escape the scope of the data it points to.
day01_my_two_borrow_rules.rs
2 RULES DEMO
#[allow(dead_code)]
pub fn borrowing_reference_with_2_rules() {

    // ---- Rule 1: Many &T (immutable) OR one &mut T (mutable) at a time ----
    let mut s1 = String::from("hello");

    let r1 = &s1; // immutable borrow #1
    let r2 = &s1; // immutable borrow #2 -> OK, multiple immutable refs allowed
    println!("Rule 1: multiple immutable refs -> r1 = {}, r2 = {}", r1, r2);
    // r1, r2 are no longer used after this point (NLL marks them as dead)

    let r3 = &mut s1; // mutable borrow -> OK now, since r1, r2 are dead

    r3.push_str(", world");
    println!("Rule 1: one mutable ref -> r3 = {}", r3);

    println!("Rule 1: final value -> s1 = {}", s1);

    // ---- Rule 2: References must always be valid (no dangling references) ----
    let valid_ref;
    {
        let s4 = String::from("inner");
        valid_ref = &s4;
        println!("Rule 2: reference is valid inside this scope -> {}", valid_ref);
    } // s4 goes out of scope here -> heap memory dropped/freed
    // println!("{}", valid_ref); // ❌ ERROR: s4 does not live long enough, valid_ref would dangle

    println!("Rule 2: valid_ref cannot be used here, would be a dangling reference");
}
UNDER THE HOOD
  • Rule 1 reuses the NLL pattern: r1/r2 die after their println!, freeing s1 up for a mutable borrow r3.
  • Rule 2 is the dangling-reference check: valid_ref is declared in the outer scope but assigned inside an inner scope to &s4.
  • When the inner { } ends, s4 is dropped — if valid_ref were used afterward, it would point to freed heap memory (a dangling pointer / use-after-free).
  • The borrow checker rejects this at compile time with "s4 does not live long enough" — this is lifetime analysis, the foundation for Day 02.
🚫

Zero Memory Bugs at Runtime

No dangling pointers, no use-after-free, no double-free. The borrow checker catches them all at compile time — your production binary ships without a class of bugs.

No GC Pauses

No garbage collector means predictable, low-latency performance. Critical for real-time systems, game engines, trading systems, and high-throughput web servers.

🔒

Thread Safety Guaranteed

Ownership rules extend to concurrency. You cannot accidentally share mutable state across threads — the compiler prevents data races entirely.

🔧

Explicit Resource Management

RAII via Drop: file handles, DB connections, mutex locks — all released deterministically when the owner goes out of scope. No leaks by design.

REAL-WORLD USAGE
AWS Firecracker — microVM runtime; ownership ensures no memory leaks across VM lifecycle
Cloudflare Workers — Rust's zero-GC borrow model enables microsecond-cold-start edge functions
Discord — switched from Go to Rust; eliminated GC latency spikes using ownership + drop semantics
Linux Kernel (Rust modules) — ownership prevents kernel memory safety bugs without performance cost
Q1 What is ownership in Rust, and why does Rust need it? +
Ownership is a compile-time memory management system where each value has a single owner. When the owner goes out of scope, the value is freed. Rust needs it to achieve memory safety without a garbage collector — giving you C-level performance with freedom from memory bugs like use-after-free and double-free. The compiler's borrow checker enforces ownership rules at zero runtime cost.
Q2 What is the difference between move, copy, and clone? +
Move: transfers ownership of heap-allocated data. The original variable is invalidated. Default for types like String, Vec.

Copy: implicitly duplicates stack-only data. The original stays valid. Types like i32, bool, f64 implement Copy.

Clone: explicit, deep copy of heap data via .clone(). Both variables own independent data. Use it consciously — it has a runtime cost.
Q3 How many mutable references can you have at a time, and why? +
Exactly one mutable reference (&mut T) at any point in time, and zero immutable references when a mutable reference is active. This prevents data races at compile time — if multiple references could mutate simultaneously, you'd have undefined behavior. Rust makes the concurrency contract explicit and statically verified.
Q4 What is a dangling reference, and how does Rust prevent it? +
A dangling reference points to memory that has already been freed. In C/C++, this causes undefined behavior. Rust's borrow checker tracks lifetimes and ensures a reference can never outlive the data it points to. If you try to return a reference to a local variable, the compiler rejects it at compile time — not at runtime, not in production.
Q5 When would you use &str vs String? +
Use &str (a string slice / borrowed view) for read-only access to string data — function parameters, string literals, slices of a String. It's zero-copy and cheap.

Use String when you need to own the string data, grow it, or return it from a function. The general rule: prefer &str in function signatures for flexibility; use String when you need ownership or mutation.
Q6 What does the Drop trait do, and how does it relate to ownership? +
The Drop trait defines custom cleanup logic that runs automatically when an owner goes out of scope — like a destructor. Rust calls drop() in reverse order of creation. This is RAII (Resource Acquisition Is Initialization): resources (file handles, locks, heap memory) are deterministically released when the owning struct is dropped. No GC, no explicit free needed.
Pattern What It Solves Used In
Rc<T> / Arc<T> Multiple ownership via reference counting. Arc is thread-safe. Use when you genuinely need shared ownership. Graph structures, shared caches, async runtimes
Cell<T> / RefCell<T> Interior mutability — mutate data through an immutable reference. RefCell moves borrow checking to runtime. UI trees, mock objects, single-threaded shared state
Cow<'a, B> Clone-on-Write — borrow data until mutation is needed, then clone. Avoids unnecessary allocations. Parsers, serializers, string processing in hot paths
Pin<T> Prevents a value from being moved in memory. Required for self-referential structs and async/await state machines. async futures, tokio tasks, custom allocators
ManuallyDrop<T> Opt out of automatic drop(). Gives you manual control over destruction order. Used in unsafe code. FFI, custom allocators, arena memory management
day01_advanced.rs — Rc + RefCell interior mutability
ADVANCED
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    // Multiple owners + interior mutability without unsafe
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

    let clone_a = Rc::clone(&shared);
    let clone_b = Rc::clone(&shared);

    clone_a.borrow_mut().push(4);   // runtime borrow check
    clone_b.borrow_mut().push(5);

    println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
    // ⚠️  panics at runtime if you violate borrow rules
    // Use Arc> instead for multi-threaded scenarios
}