IS 4010: Application Development with Artificial Intelligence

Week 10: The soul of Rust, ownership and borrowing

Brandon M. Greenwell

Session 1: who owns this value?

We will follow one small piece of text: "Homer".

  • Who owns it?
  • Can we still use it after an assignment or function call?
  • When is its memory cleaned up?

Our routine: Predict → run → explain.

Who cleans up the memory?

Every program borrows memory to store values. Someone has to give it back.

Strategy Who cleans up? The catch
Manual (C, C++) You call free Forget, free twice, or free too early
Garbage collector (Python, Java, Go) A helper running alongside your program Extra runtime work, occasional pauses
Ownership (Rust) Rules the compiler checks before the program runs Learning the rules (this week!)

Why this matters: memory bugs are expensive

  • Microsoft: about 70% of the vulnerabilities it assigns a CVE each year are memory-safety issues.
  • Chromium: around 70% of its high-severity security bugs are memory-safety problems.

Rust’s pitch: Turn many of those bugs into compiler errors you fix before shipping.

Remember Python?

name = ["Homer"]
guest = name
guest.append("Simpson")
print(name)

Predict: What does this print?

Two places to store values: stack and heap

Stack: a stack of plates

  • Fixed, known size
  • Last on, first off
  • Fast
  • Cleared automatically when a function ends

let score = 5;

Heap: a restaurant host

  • Any size, can grow
  • The host finds space and hands you a table number (a pointer)
  • Slower to set up and to reach
  • Someone has to say “we’re done”

The text inside a String

The stack cleans itself up. Heap memory needs an owner.

Meet an owned string

fn main() {
    let name = String::from("Homer");
    println!("{}", name);
}
  • String stores owned text that can grow.
  • String::from("Homer") creates a String containing Homer.
  • Here, the variable name is the owner of that String.

Predict: What will this print?

Try this example in Rust Playground

Braces show where a variable is available

fn main() {
    {
        let name = String::from("Homer");
        println!("{}", name);
    } // name leaves scope; its String is cleaned up

    // println!("{}", name); // Try uncommenting this
}

Scope is the region of code where a variable can be used.

Predict: Can the final line use name outside its block?

Try this example in Rust Playground

Three rules for owned values

  1. Every value has an owner.
  2. There is one owner at a time.
  3. When the owner leaves scope, the value is dropped: Rust performs its cleanup.

Moving a value transfers ownership to a new owner.

Copying creates another value; it does not give one value two owners.

Assignment can move ownership

fn main() {
    let name = String::from("Homer");
    let guest = name; // The String moves to guest

    println!("{}", guest);
    // println!("{}", name); // Can we still use name?
}

Predict: Which variable owns the String after the assignment?

Try this example in Rust Playground

Try using the old owner

This example intentionally fails to compile.

fn main() {
    let name = String::from("Homer");
    let guest = name;

    println!("{}", guest);
    println!("{}", name); // name no longer owns the String
}

Predict: Which earlier line caused the problem?

Try this example in Rust Playground

Read the diagnostic as a timeline

The compiler identifies:

  1. let name = String::from("Homer"); — the value starts here.
  2. let guest = name; — ownership moves here.
  3. println!("{}", name); — the old owner is used here.
error[E0382]: borrow of moved value: `name`

Why “borrow”? println! borrows a value to display it. Session 2 explains borrowing.

Smallest fix: Remove the final use of name; guest already owns the text.

Why the old owner stops working

A String is a small header on the stack that points to its text on the heap:

Before: let guest = name;

name:  [pointer, length, capacity] ──→ [ Homer ]
           stack                        heap

After the move:

name:  unavailable
guest: [pointer, length, capacity] ──→ [ Homer ]
                                        same text

If name and guest both stayed valid, both would free the same text when main ends. Freeing twice is a classic C bug, so Rust retires name instead.

Integers copy instead

fn main() {
    let score = 5;
    let saved_score = score;

    println!("{} and {}", score, saved_score);
}
  • Integers implement Copy, which marks a type as safe to duplicate automatically. Assignment copies the value.
  • A number has no heap text to clean up, so duplicating it is cheap and safe.
  • Both variables remain usable. Output: 5 and 5.
  • bool, char, and floating-point numbers also implement Copy.

Check: Does String behave the same way?

Try this example in Rust Playground

Clone when you need independent text

push_str appends text. mut allows the owned string to change.

fn main() {
    let name = String::from("Homer");
    let mut guest = name.clone();
    guest.push_str(" Simpson");

    println!("{} and {}", name, guest);
}

Predict: Does changing guest also change name?

name:  [pointer, length, capacity] ──→ [ Homer ]
guest: [pointer, length, capacity] ──→ [ Homer Simpson ]

Try this example in Rust Playground

Function arguments can move too

fn welcome(guest: String) {
    println!("Welcome, {}!", guest);
} // guest's String is dropped here

fn main() {
    let name = String::from("Homer");
    welcome(name); // Ownership moves into the function
    // println!("{}", name); // Try uncommenting this
}

Predict: Who owns the String while welcome runs?

Try this example in Rust Playground

Returning an owned value

fn make_name() -> String {
    let name = String::from("Homer");
    name // No semicolon: return this value
}

fn main() {
    let guest = make_name();
    println!("{}", guest);
}

Predict: Is the text cleaned up when make_name finishes?

Try this example in Rust Playground

Checkpoint: move or copy?

fn main() {
    let visits = 2;
    let saved_visits = visits;
    let name = String::from("Homer");
    let guest = name;

    println!("{} and {}", visits, saved_visits);
    println!("{}", guest);
    // println!("{}", name);
}

Explain: Which assignment copies? Which moves? What happens if we uncomment the last line?

Try this example in Rust Playground

Ownership recap

Operation What happens?
Assign or pass a String Ownership moves
Assign or pass an integer Its value copies
Call .clone() on a String Independent text is created
Return a String Ownership moves to the caller
Owner leaves scope Its value is dropped

Next question: What if a function needs to use text while the caller keeps ownership?

Session 2: using a value without owning it

In session 1, passing name to welcome moved it. main lost the text.

  • Can a function use name without taking it?
  • Can a function change name while main stays the owner?
  • What stops two parts of a program from getting in each other’s way?

Our routine: Predict → run → explain.

We only wanted to read the name

This version intentionally fails to compile.

fn welcome(guest: String) {
    println!("Welcome, {}!", guest);
}

fn main() {
    let name = String::from("Homer");
    welcome(name);
    println!("See you soon, {}!", name);
}

Discuss: Does welcome need to own the text just to display it?

Try this example in Rust Playground

A reference points to a value someone else owns

guest ──→ name: [pointer, length, capacity] ──→ [ Homer ]
(reference)     (owner, lives in main)            heap
  • A reference lets code use a value without owning it.
  • Making a reference is called borrowing.
  • In Rust, the & symbol (an ampersand) means “a reference to.”
  • The owner keeps ownership and still handles cleanup. Nothing is copied.

& shows up in two places

Code Where it appears Read it as
guest: String Parameter type This parameter takes ownership of a String
guest: &String Parameter type This parameter accepts a reference to a String
welcome(name) Function call Pass the owned value; it moves
welcome(&name) Function call Make a reference to name and pass that

In a type, & describes what is expected. In an expression, & creates the reference.

Borrow for the function call

fn welcome(guest: &String) {
    println!("Welcome, {}!", guest);
}

fn main() {
    let name = String::from("Homer");
    welcome(&name);
    println!("See you soon, {}!", name);
}

Only two changes from the failing version: &String in the parameter and &name in the call.

Predict: Why can main still use name?

Try this example in Rust Playground

Predict: which of these compile?

Here input is a String in main.

Parameter Call
A name: String welcome(input)
B name: &String welcome(&input)
C name: String welcome(&input)
D name: &String welcome(input)
  • A compiles: ownership moves into welcome.
  • B compiles: welcome borrows, and main keeps input.
  • C and D fail with E0308, mismatched types.

The argument must match the parameter type. Rust will not add or remove the & for you.

Borrow to change the string

A plain & reference can only read. &mut is a reference that is allowed to change the value.

fn add_surname(guest: &mut String) {
    guest.push_str(" Simpson");
}

fn main() {
    let mut name = String::from("Homer");
    add_surname(&mut name);
    println!("{}", name);
}
Code Meaning
let mut name The owner allows its String to change (Week 09)
&mut name Make a reference that can change name
guest: &mut String This parameter accepts a changing reference

Predict: What prints? Which variable owns the changed text?

Why: guest points at main’s String; it has no text of its own. Changing through guest changes name.

Try this example in Rust Playground

The borrowing rules

For the same data, while borrows are in use:

  1. Allow many shared references (&String) or one exclusive reference (&mut String), never both.
  2. The borrowed value must remain valid for every use of its references.

Reading can be shared. Changing needs exclusive access.

  • Think of a shared document: many people can read it, one person can edit it, and nobody reads while an edit is in progress.
  • Compiler messages say immutable for shared references and mutable for exclusive ones.
  • The same rules apply to references to any type, not only String.

Two readers are fine

fn main() {
    let name = String::from("Homer");
    let first_reader = &name;
    let second_reader = &name;

    println!("{} and {}", first_reader, second_reader);
    println!("Owner: {}", name);
}

Predict: Does either reference take ownership or change the text?

Try this example in Rust Playground

Two overlapping mutable borrows fail

This example intentionally fails to compile.

fn main() {
    let mut name = String::from("Homer");
    let first_editor = &mut name;
    let second_editor = &mut name; // Overlaps first borrow

    first_editor.push_str(" J.");
    second_editor.push_str(" Simpson");
}
error[E0499]: cannot borrow `name` as mutable
more than once at a time

Why: first_editor is used again on a later line, so its borrow is still active when second_editor tries to start one.

Try this example in Rust Playground

Finish one edit before starting the next

fn main() {
    let mut name = String::from("Homer");
    let first_editor = &mut name;
    first_editor.push_str(" J."); // Last use of first_editor

    let second_editor = &mut name;
    second_editor.push_str(" Simpson"); // Last use of second_editor

    println!("{}", name);
}

Predict: Why do these two mutable borrows work now?

let first_editor = &mut name;   ┐ first borrow
first_editor.push_str(" J.");   ┘ ends at its last use
let second_editor = &mut name;  ┐ second borrow
second_editor.push_str(...);    ┘ ends at its last use
println!("{}", name);           no borrows active

A borrow lasts until its last use, not until the closing brace.

Try this example in Rust Playground

A reader and an editor can conflict

This example intentionally fails to compile.

fn main() {
    let mut name = String::from("Homer");
    let reader = &name;
    let editor = &mut name;
    editor.push_str(" Simpson");

    println!("{}", reader); // reader is still needed here
}
error[E0502]: cannot borrow `name` as mutable
because it is also borrowed as immutable

A shared reference comes with a promise: the value won’t change while you’re looking at it.

Why: reader is used on the last line, so editor changing the text would break that promise.

Try this example in Rust Playground

A borrow can end after its last use

fn main() {
    let mut name = String::from("Homer");
    let reader = &name;
    println!("Before: {}", reader); // Last use of reader

    let editor = &mut name;
    editor.push_str(" Simpson");
    println!("After: {}", name);
}

Why it works: reader’s borrow ends at its last use, before editor starts.

Ending a borrow does not drop the String. Only the owner’s scope decides when the text is cleaned up.

Try: Add println!("{}", reader); at the end. Will it still compile?

Try this example in Rust Playground

Declare now, assign later

So far every let gave the variable a value right away. You can also split that into two steps:

fn main() {
    let total; // Declare total now
    {
        let score = 5;
        total = score * 2; // Give total its value
    }

    println!("{}", total);
}
  • let total; creates the variable but gives it no value yet.
  • Rust works out the type from the later assignment, here an integer.
  • Reading total before it has a value is a compile error.
  • Without mut, the value can be assigned only once.

Why do this? total is declared outside the braces, so it is still available after them. This prints 10.

Try this example in Rust Playground

Borrowed data must still exist

This example intentionally fails to compile.

fn main() {
    let reader; // Declared now, given a value below
    {
        let name = String::from("Homer");
        reader = &name;
    } // name's String is dropped here

    println!("{}", reader); // Too late to read that String
}

Same shape as the last slide, but reader holds a reference to name, not its own value.

error[E0597]: `name` does not live long enough

Why: name’s text is cleaned up at the closing brace, so reader would point at text that no longer exists (a dangling reference).

Try this example in Rust Playground

Keep the owner available for the read

fn main() {
    let name = String::from("Homer");
    let reader;
    {
        reader = &name;
    }

    println!("{}", reader);
} // name's String is dropped here

What changed: The let name line now sits above the braces. name still owns the String, and it now lives past the final read.

Try this example in Rust Playground

Return owned data when you create it

fn make_name() -> String {
    String::from("Homer")
}

fn main() {
    let name = make_name();
    println!("{}", name);
}
  • The function creates text and transfers ownership to its caller.
  • Returning a reference to a local String would leave no owner to keep it alive.

Try this example in Rust Playground

Two useful text types: String and &str

fn main() {
    let name = String::from("Homer");
    let borrowed_text: &str = name.as_str();
    let literal: &str = "Simpson";

    println!("{} {}", borrowed_text, literal);
}
  • String: owned text that can grow.
  • &str: a shared view of text, called a string slice.
  • str is the text itself. You always reach it through a reference, so you will see it written &str.
  • .as_str() borrows the text in a String; it does not copy it.

Try this example in Rust Playground

&str accepts borrowed text and literals

fn show_size(text: &str) {
    println!("{} bytes", text.len());
}

fn main() {
    let name = String::from("Homer");
    show_size(name.as_str());
    show_size("Simpson");
    println!("Still owned here: {}", name);
}

Predict: Can the caller still use name?

Try this example in Rust Playground

Choose access to match the job

What does the function need? Parameter type Example argument
Own the text String name
Read an existing String &String &name
Read text from different sources &str name.as_str() or "Homer"
Change the caller’s String &mut String &mut name

Ask first: Do I need to own, read, or change this value?

Borrow or clone?

let name = String::from("Homer");
let reader = &name;       // Points at name's text
let copy = name.clone();  // Makes new text
reader ──→ name: [pointer, length, capacity] ──→ [ Homer ]
copy:          [pointer, length, capacity] ──→ [ Homer ]
&name name.clone()
Creates new text? No Yes, a full copy
Who owns it? name copy owns its own
Usable after name is dropped? No Yes
Cost Nearly free Grows with the text’s length

Borrow by default. Clone when you truly need independent text.

Your turn: fix the access

This example intentionally fails to compile.

fn add_surname(guest: &String) {
    guest.push_str(" Simpson");
}

fn main() {
    let name = String::from("Homer");
    add_surname(&name);
    println!("{}", name);
}

Repair: Change the original string through a borrow. Explain each edit.

Try this example in Rust Playground

Use the compiler, then ask AI to explain

  1. Run cargo check and read the first diagnostic.
  2. Find the owner, the move or borrow, and the later use.
  3. Predict a small fix; rerun the code to check it.
  4. Ask your AI assistant about the rule that remains unclear.

Example prompt:

Explain this diagnostic using the variable names in my code. Identify the conflicting uses, then give me one hint before showing a fix.

Your responsibility: Explain why the final change works.

Lab 10: apply these access patterns

Part 1: Repair seven borrow-checker puzzles and run them.

Part 2: Implement the four supplied functions; keep their signatures.

Function Access pattern
to_uppercase_owned String → owned String
string_length &String → usize
append_suffix &mut String and &str → change the original
concat_strings Two &str inputs → new owned String

Follow the Lab 10 instructions. Leave the supplied tests unchanged.

Check and submit Lab 10

Run these inside your fork’s week10/ directory:

cargo test
cargo fmt
cargo clippy -- -D warnings
  • CI checks formatting with cargo fmt --check.
  • Review git diff, commit week10/, and push.
  • Confirm that the Week 10 README badge is green.

Explain it in your own words

  • Move: Who owns the value now?
  • Copy or clone: Did we create a separate value?
  • Borrow: What access does the function need?
  • Validity: Will the borrowed data still exist when it is used?

Next week: Use these ideas with structs, enums, and pattern matching.

Resources and questions

Start here:

Practice: Rust Playground and Rustlings.

Optional reading: Rust Book: validating references with lifetimes.

Optional extension: named lifetimes

We already checked that borrowed data stays valid through each use.

Sometimes a function signature must also describe which inputs a returned reference can borrow from.

That is when a name such as 'a can help.

This section is optional; Lab 10 does not require named lifetimes.

Optional: which input is borrowed?

This version intentionally fails to compile.

fn longest(first: &str, second: &str) -> &str {
    if first.len() >= second.len() {
        first
    } else {
        second
    }
}

fn main() {
    println!("{}", longest("Homer", "Simpson"));
}

E0106: The signature needs to relate the returned reference to its inputs.

Try this example in Rust Playground

Optional: give the relationship a name

fn longest<'a>(first: &'a str, second: &'a str) -> &'a str {
    if first.len() >= second.len() {
        first
    } else {
        second
    }
}

fn main() {
    println!("{}", longest("Homer", "Simpson"));
}

<'a> after the function name declares the label, much like declaring a variable before using it.

'a says: the result can be used only while both inputs still exist.

Annotations describe a relationship; they do not keep data alive.

Try this example in Rust Playground

Optional: keep both inputs alive

This example intentionally fails to compile.

fn longest<'a>(first: &'a str, second: &'a str) -> &'a str {
    if first.len() >= second.len() { first } else { second }
}

fn main() {
    let name = String::from("Homer");
    let result;
    {
        let other = String::from("Simpson");
        result = longest(name.as_str(), other.as_str());
    } // other is dropped here
    println!("{}", result);
}

Try: Move the print inside the inner block. Why does that work?

Try this example in Rust Playground