Reading Rust

Part 1 Β· The runtime machine

Reading a stack frame

All of Part 1 applied to one real function, line by line β€” then a mixed set that refuses to tell you which idea it is testing.

45 minfaded worked exampleinterleavingself-explanation
What this lesson is trying to break (2)
  • I understand each rule separately, so I understand them together.
  • Practising one topic at a time is the efficient way to study.

Part 1 gave you five separate rules. This lesson checks whether they work together, which is a different thing β€” and then deliberately mixes them up.

The function

Read it once, normally, the way you would in a pull request.

use std::collections::HashMap;

#[derive(Debug)]
struct Config {
    name: String,
    retries: u32,
}

fn load(raw: HashMap<String, String>) -> Option<Config> {
    let name = raw.get("name")?.clone();

    let retries = raw
        .get("retries")
        .and_then(|s| s.parse().ok())
        .unwrap_or(3);

    Some(Config { name, retries })
}

Now answer these before opening any of the steps below.

Predict firstcommit in writing, then look
  1. Does load own raw, or borrow it?
  2. Why is the .clone() on line 1 of the body necessary, given the answer to question 1?
  3. At what point is the HashMap’s memory released?

Line by line

Open one step at a time. Try to answer each before you expand it.

Step 1 β€” `fn load(raw: HashMap<String, String>) -> Option<Config>`

raw is a place owned by this frame. The caller wrote load(m) and m is now dead at the call site.

On the stack, raw is three words: pointer, length, capacity β€” the same shape as a Vec. Every key and value String is itself three words living in the map’s heap allocation, each with its own separate buffer.

Cost of the call: three words copied. Not the map contents.

Step 2 β€” the name binding, and why it must clone

Four things happen on this line:

  1. raw.get("name") takes &raw β€” a shared borrow β€” and returns Option<&String>.
  2. ? unwraps it. On None it returns None from load immediately. Note what this means for drop: the early return still drops raw at the exit point.
  3. .clone() on the &String produces an owned String β€” a new heap allocation, bytes copied.
  4. The shared borrow of raw ends here, because nothing keeps it alive past this statement.

The alternative worth knowing: raw.remove("name")? returns Option<String> β€” owned, no clone, no allocation. It requires raw to be mut and it mutates the map. Which is right depends on whether the map is used again. Here it is, on the next line, so get + clone is defensible.

Being able to say β€œyou could use remove here and skip the allocation, but then the map is modified” is exactly the level of comment that is useful in a Rust review.

Step 3 β€” the `retries` chain
raw.get("retries")               // Option<&String>
   .and_then(|s| s.parse().ok()) // Option<u32>
   .unwrap_or(3)                 // u32

The closure takes s: &String. s.parse() works because of deref coercion: &String becomes &str, and parse is a method on str. You have not been taught that yet β€” it is 3.4 β€” but note that it happened silently, because that silence is exactly what makes Rust hard to read at first.

parse is generic over its return type: fn parse<F: FromStr>(&self) -> Result<F, F::Err>. Nothing on this line says u32. The compiler infers it backwards from unwrap_or(3) combined with the retries: u32 field in the struct literal two lines later.

No allocation happens in this chain. Every step is a borrow or a Copy type.

Step 4 β€” `Some(Config { name, retries })` and the end of the frame

name is moved into the struct’s field place. The String is not copied; its three words are written into Config.name and the local name is dead.

retries is u32, which is Copy, so the local stays valid β€” irrelevant here since nothing uses it again, but the mechanism differs.

The Config is moved into Some(...), then moved out as the return value. Written out like that it sounds expensive; it is not. These are stack writes the optimiser almost always collapses into constructing the value once, in place.

Then the frame ends and raw is dropped: every String key and value freed, then the map’s allocation. The cloned name is not affected β€” it was a separate allocation from the start, and it now lives in the returned Config, owned by the caller.

Explain it to yourselfwrite it β€” thinking it does not count

Rewrite load to take &HashMap<String, String> instead of owning it. What changes, and what does the caller gain and lose?

Then answer the harder version: is there a signature that avoids the clone and leaves the caller’s map intact? Say why or why not.

Interleaved practice

Now the part that feels worse and works better.

The questions below are not in lesson order and are not labelled by topic. Before answering each one you have to work out which idea it is about β€” and that sorting step is the thing that actually transfers to reading unfamiliar code. Blocked practice (all the Drop questions together, then all the move questions) produces better scores today and worse understanding later. This is one of the most robust findings in the study of practice, and one of the least used.

Predict firstcommit in writing, then look
let a = vec![1, 2, 3];
let b = a;
let c = b.len();
println!("{c}");

Compiles?

Predict firstcommit in writing, then look
struct D(&'static str);
impl Drop for D { fn drop(&mut self) { println!("{}", self.0); } }

fn main() {
    let _a = D("a");
    let _ = D("b");
    let _c = D("c");
}
Predict firstcommit in writing, then look
fn main() {
    println!("{}", size_of::<Option<Box<[u8]>>>());
}
Predict firstcommit in writing, then look
#[derive(Clone, Copy)]
struct Handle { id: u64, name: String }

Does this compile?

Part 1 checkpoint

Say each of these out loud, without notes. If any of them stalls, the lesson to reread is named.

  1. What does a let binding create? (1.1)
  2. What exactly is copied in let b = a when a: String? (1.1)
  3. What are the two words in a &str? In a &dyn Trait? (1.2)
  4. What claim does impl Copy make? (1.3)
  5. How does drop(x) work when its body is empty? (1.3)
  6. Which drops first β€” the first local or the last? The first field or the last? (1.4)
  7. Where does a temporary die, and what is the exception? (1.4)

If all seven are fluent, you have the runtime machine. Part 2 will now argue with you about which of these programs you are allowed to write β€” which is a completely separate machine, and the hardest part of the course.

Next: 2.1 β€” Aliasing XOR mutability.

In the wildgo read the real thing now, while it is fresh
  • std::env::args_os β€” a short, real, readable functionSearch for `pub fn args`. Small enough to hold in your head, real enough to count. Do the same line-by-line pass on it that this lesson does.
  • ripgrep β€” src/flags/hiargs.rsWidely considered some of the most readable Rust in the wild. Do not try to understand what it does yet. Scroll it and count how many signatures you can now decode purely from Part 1 β€” ownership, borrows, String vs &str.
Scheduled for recall3 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course β€” not grouped by topic. Sorting out which idea applies is the part that transfers.