7.xChapter 7 summary and quiz

Last updated June 12, 2026

This was the chapter where your programs stopped reading like scripts and started reading like decisions: branching, repeating, and ending on their own judgment. It's also the chapter where the course's first complete program shipped. The review, then a quiz with two programs worth keeping.

Quick review

A program's execution path is the sequence of statements it actually runs, and a straight-line program has only one. Control flow statements create alternatives: when execution goes somewhere other than straight ahead, it has branched. Rust's toolkit: conditionals (if, match), loops (loop, while, for), jumps (break, continue, return), and halts. What the toolkit deliberately omits (goto, do-while, switch, and exceptions) all reduce to the same trade: delete the bug-prone construct, keep its one good job (7.1).

An if/else if chain takes its first true arm and ignores the rest; separate if statements each get evaluated. Early returns flatten functions by handling the rejections first, and logical operators flatten nested conditions. A variable born inside an arm dies at the arm's closing brace; when arms exist to produce a value, the if-as-expression delivers it past the braces (7.2).

A match evaluates its scrutinee once and tests it against each arm's pattern, top to bottom, first fit wins, no fallthrough. _ matches anything; | combines patterns into one arm. A match must be exhaustive: the compiler proves your arms cover every possible value (E0004 names exactly what you missed), which means a match doesn't just check the cases you thought of, it checks the completeness of your thinking. Patterns that bind, guard, and destructure arrive with chapter 11 (7.3).

A while checks before every iteration and runs zero times if the condition starts false. loop is the deliberate infinite loop (the compiler will tell you to use it if you write while true), escaped by break, and loop plus a conditional break replaces do-while. On unsigned counters, while i >= 0 is always a bug, and rustc labels it (7.4). A for loop walks a sequence with no exposed bookkeeping: 0..n is half-open (n iterations), 0..=n inclusive, .rev() reverses, .step_by(k) skips; the loop variable is a fresh binding each pass. Default to for when the values are known up front, while for re-evaluated conditions, loop for "until I say" (7.5).

break ends its loop, continue ends its iteration, and both can target an enclosing loop by label (break 'outer), the civilized replacement for goto's last legitimate job. From a loop (only), break can carry a value out, making the loop an expression and retiring a whole genre of flag variables (7.6).

A recursive function calls itself, shrinking toward a base case; without a reachable one, each unfinished call holds its ledge of memory until the stack runs out (a stack overflow). Prefer loops where they're equally clear, which for plain counting they always are; recursion shines when the problem is self-similar, and Rust doesn't promise tail-call optimization (7.7).

Programs report an exit code at the end: 0 for success, nonzero for failure, read by scripts and CI rather than humans. std::process::exit(code) halts immediately from anywhere, skipping cleanup and unflushed output; panic! crashes on purpose with a message addressed to the programmer, for states that should be impossible (expected failures are chapter 12's Result business) (7.8).

A PRNG is deterministic scrambling: the seed sets its state, and same seed means same sequence, forever. Real randomness-shaped variety comes from OS-seeded generators, via your first dependency: cargo add rand writes one semver line into Cargo.toml, Cargo.lock pins exact versions for reproducible builds, and rand::random_range(1..=100) (or a reused rand::rng() generator) does the rolling (7.9). All of it converged in the guessing game, whose optimal strategy you already knew from debugging: halve the range every guess. Its name is binary search, and 2⁷ = 128 is why 7 guesses crack 100 numbers (7.10).

Quiz time

Question #1

Three quick ones, one sentence each:

a) loop { ... } and while true { ... } run identically. Why does the compiler (and this course) still insist on loop? b) You're summing user inputs until the user types 0. Which loop, and why? c) You're printing a 12-row multiplication table. Which loop, and why?

Show solution

a) loop declares there's no exit condition, so the compiler treats it specially: only loop can break with a value, because every exit from a loop is a break. (while true additionally draws a warning.) b) A loop with a sentinel break (or break-with-value): iteration count depends on a decision made inside the loop. c) A for over 1..=12: the values to visit are known up front, and the loop does the counting bookkeeping for you.

Question #2

What does this print?

fn main() {
    'outer: for i in 1..=3 {
        for j in 1..=3 {
            match (i * j) % 4 {
                0 => break 'outer,
                1 => continue,
                _ => print!("{} ", i * j),
            }
        }
        println!("row {i} done");
    }
    println!("end");
}
Show solution
2 3 row 1 done
2 end

Row 1: products 1, 2, 3; the 1 hits continue (skipping its print), 2 and 3 print, the row completes. Row 2: product 2 prints, then product 4 hits break 'outer, abandoning both loops at once, so neither "row 2 done" nor row 3 happens; execution lands on the final println!. (Note continue targeted the inner loop, its innermost enclosing one, and that a match arm can hold a jump statement just fine.)

Question #3

Predict the compiler's reaction to each, precisely:

// Program A
fn main() {
    let on = true;
    match on {
        true => println!("lights on"),
    }
}
// Program B
fn main() {
    let result = for i in 1..=10 {
        if i * i > 50 {
            break i;
        }
    };
    println!("{result}");
}
Show solution

Program A: E0004, non-exhaustive patterns: false not covered. A bool has exactly two values and the match handles one; exhaustiveness is checked against the type, and the fix is a false arm (or _).

Program B: E0571, break with value from a for loop. A for can end by exhausting its range without ever reaching the break, so the loop can't promise a value; only loop can. (Rewrite with let mut/plain break, or a loop with its own counter.)

Question #4

Chapter 4's falling-ball program (4.x, question #3) had a hardcoded flaw: it reported the ball's height at t = 0 through 5 because we worked out that a 100-meter drop takes about 5 seconds. Replace the six report calls with a loop that keeps reporting, one second at a time, until the ball has landed, for any tower height. (Reuse ball_height, report, and read_number from chapter 4 unchanged.)

Show solution
fn ball_height(start: f64, seconds: f64) -> f64 {
    let fallen = 9.8 * seconds * seconds / 2.0;
    let height = start - fallen;
    if height < 0.0 { 0.0 } else { height }
}

fn report(start: f64, seconds: f64) {
    let height = ball_height(start, seconds);
    if height > 0.0 {
        println!("At {seconds} seconds, the ball is at height: {height} meters");
    } else {
        println!("At {seconds} seconds, the ball is on the ground.");
    }
}

fn read_number() -> f64 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    input.trim().parse().expect("that wasn't a number")
}

fn main() {
    println!("Enter the height of the tower in meters:");
    let start = read_number();
    let mut seconds = 0.0;
    loop {
        report(start, seconds);
        if ball_height(start, seconds) <= 0.0 {
            break;
        }
        seconds += 1.0;
    }
}
Enter the height of the tower in meters:
100
At 0 seconds, the ball is at height: 100 meters
At 1 seconds, the ball is at height: 95.1 meters
At 2 seconds, the ball is at height: 80.4 meters
At 3 seconds, the ball is at height: 55.9 meters
At 4 seconds, the ball is at height: 21.599999999999994 meters
At 5 seconds, the ball is on the ground.

Same output as chapter 4's version for a 100-meter tower, but try 500, or 2: the program now notices the landing instead of being told when it is, which is this chapter's whole thesis in one diff. The loop is a loop-with-break rather than a while, so the landing line itself still prints (check the condition after reporting, not before); a while ball_height(...) > 0.0 would stop one line early. Why not a for? Because the number of iterations is exactly what we don't know anymore. (The digit eruption at t = 4 is still chapter 4's rounding error; chapter 5 gave you {:.1} if it offends you.)

Question #5

Write is_prime(x: u32) -> bool (a prime is a number ≥ 2 divisible only by 1 and itself), then use it to print every prime up to 30. Checking divisibility by every number from 2 to x−1 is fine for full credit. Extra credit: it's enough to check odd divisors whose square doesn't exceed x (any divisor pair has one member at or below the square root). Use test * test <= x so you don't need a square-root function.

Show solution

Full credit:

fn is_prime(x: u32) -> bool {
    if x < 2 {
        return false;
    }
    for divisor in 2..x {
        if x % divisor == 0 {
            return false;
        }
    }
    true
}

fn main() {
    for n in 1..=30 {
        if is_prime(n) {
            print!("{n} ");
        }
    }
    println!();
}
2 3 5 7 11 13 17 19 23 29 

Early-return style at work: each divisor found is an immediate false, and surviving the whole loop is the proof of primality (note 2 survives trivially: the range 2..2 is empty). Extra credit, same main:

fn is_prime(x: u32) -> bool {
    if x < 2 {
        return false;
    }
    if x % 2 == 0 {
        return x == 2;
    }
    let mut test = 3;
    while test * test <= x {
        if x % test == 0 {
            return false;
        }
        test += 2;
    }
    true
}

Evens are dispatched in one line (only 2 among them is prime), then only odd divisors are tried, and only while test * test <= x. For x = 29: tests 3 and 5, done; the naive version tried 27 divisors. The square trick matters more than it looks: for a ten-digit number, it's the difference between ~50,000 checks and ~5,000,000,000. A while loop, note, because the stopping condition (test * test <= x) isn't a simple range of values.

That's control flow: your programs now branch, repeat, recurse, quit early, and roll dice. Every value in them still obeys one unexamined rule, though: it lives exactly as long as its scope and vanishes at a closing brace. Chapter 8 finally asks the question this course exists to answer gently: who owns the data, and what does it mean to give it away? See you there.