What Rust in Action says about flow control, run¶
Control flow › Flow control › The book's claims, run
Level: 101 · a companion to the section
One line: Twenty-one claims from §2.4 "Flow control" of Rust in Action (pp. 45–52), checked against rustc 1.98.0. Twelve hold as written and four hold with a condition. Five need a correction, and most of those are about why something happens rather than what happens.
The claims below are paraphrased. The section numbers and page numbers are the book's. Each row links to the lesson that teaches the construct in full.
The claims at a glance¶
| # | Where | Claim | Verdict | Taught in |
|---|---|---|---|---|
| 1 | §2.4.1, p. 45 | After for item in container, the container's lifetime has ended when the block finishes |
Correct it: the value is moved on the loop's first line, and only if it is not Copy |
for loops |
| 2 | §2.4.1, p. 46 | for item in &collection is collection.iter(), and &mut collection is iter_mut() |
Holds for the types that implement it | iter, iter_mut, into_iter |
| 3 | §2.4.1, p. 46 | n..m is exclusive and n..=m is inclusive; by convention _ stands for a value you don't use |
Holds, and more than a convention: an unused i draws a warning |
for loops |
| 4 | §2.4.1, p. 47 | Indexing in a loop costs a bounds check that direct iteration avoids | Holds in a debug build; the optimizer removes it from 0..v.len() |
Loops without an index |
| 5 | §2.4.1, p. 47 | A for over the collection guarantees nothing else changes it |
Holds | Loops without an index |
| 6 | §2.4.2, p. 47 | continue works as you would expect |
Holds; in a while it can also skip your increment |
continue |
| 7 | §2.4.3, p. 47 | A while condition is any expression that evaluates to true or false |
Holds, and nothing else is accepted | while loops |
| 8 | §2.4.3, p. 48 | Rust programmers avoid while true and write loop |
Holds, and the compiler has reasons of its own | loop |
| 9 | §2.4.4, p. 48 | loop runs until a break, or until the program is stopped from outside |
Correct it: return, ?, a panic and process::exit leave it too |
loop |
| 10 | §2.4.5, p. 48 | The (0..).zip(0..) loop with break stops once x + y > 100 |
Holds, at 51 and 51 | break |
| 11 | §2.4.5, p. 49 | break 'outer leaves all three nested loops |
Holds, at (0, 0, 1001) |
Loop labels |
| 12 | §2.4.5, p. 49, note 5 | Labels work with continue as well |
Holds | Loop labels |
| 13 | §2.4.5, p. 49 | Rust has no goto keyword |
Holds, and goto is not even reserved |
Loop labels |
| 14 | §2.4.5, p. 49 | For the C habit of jumping to cleanup code on an error, use loop labels | Correct it: cleanup is Drop, which runs on every exit without a jump |
Loop labels |
| 15 | §2.4.6, pp. 49–50 | if takes any expression that evaluates to a Boolean; there is no truthy or falsey |
Holds | if expressions |
| 16 | §2.4.6, p. 49 | The else block matches anything that has not already been matched |
Holds; later conditions are not even evaluated | if expressions |
| 17 | §2.4.6, p. 51 | Binding a name to a value with the assignment operator = is a statement |
Holds for let; an assignment x = 5 on its own is an expression of type () |
A block is an expression |
| 18 | §2.4.6, p. 50 | The break keyword also returns a value |
Holds in a loop (and a labelled block) only |
break |
| 19 | §2.4.7, p. 51 | match warns you when you have not considered an alternative |
Correct it: it is an error, E0004, as the book itself says a paragraph later |
match expressions |
| 20 | §2.4.7, p. 52 | Listing 2.8: 42 \| 132 matches both values, _ matches the rest |
Holds | match expressions |
| 21 | §2.4.7, p. 52 | Many control structures, looping among them, are built from match underneath |
Correct it: for and ? are; while is a loop around an if |
Printing the HIR |
Fifteen claims, run¶
Verified output of flow_control_claims_checked.rs — regenerated by tools/run_examples.py, never hand-typed.
1. after `for item in container`, the container's lifetime has ended
an array of i32 is Copy, so it is still here: [1, 2, 3]
a Vec was moved, but the name v takes a new value: [9]
2. `for item in &collection` is `for item in collection.iter()`
Vec: IntoIterator for &Vec gives [10, 20, 30], iter() gives [10, 20, 30]
HashMap: IntoIterator for &HashMap gives [("ada", 36)], iter() gives [("ada", 36)]
same items, because each type's impl calls iter(); &String has no such impl
3. `n..m` is exclusive and `n..=m` is inclusive
0..10 ran 10 times, 0..=10 ran 11 times
5. a `for` over the collection guarantees it is not changed during the loop
the index loop compiled, ran 3 passes, and never saw the pushed 4: [1, 2, 3, 4]
6. `continue` operates as you would expect
in a while loop, a continue before i += 1: i is still 1 after 10 passes
9. `loop` runs until a `break`, or until the program is stopped from outside
first_negative([4, -1, 7]) = Some(1) (left by return)
first_negative([4, 1, 7]) = None (left by return)
10. `for (x, y) in (0..).zip(0..)` with `break` when x + y > 100
stopped at x = 51, y = 51
11. `break 'outer` leaves all three loops once x + y + z > 1000
stopped at (x, y, z) = (0, 0, 1001); x took the values [0]
12. labels work with `continue` too
only row 3 finished its inner loop: [3]
13. Rust does not include the `goto` keyword
`let goto = ...` compiles: goto is not reserved
14. use loop labels for the jump-to-cleanup pattern
second step fails:
drop: config
-> Err("log could not be opened")
both steps succeed:
both open
drop: log
drop: config
-> Ok(())
16. the `else` block matches anything that has not already been matched
one-three-two; conditions evaluated: [42, 132]
17. assigning with `=` is a statement, so it has no value
`let assigned = total = 5;` compiles: assigned is (), total went 1 -> 5
18. the `break` keyword also returns a value
let n = loop { break 123; }; n is 123
20. Listing 2.8: `42 | 132` matches both, `_` matches the rest
42: hit!
132: hit!
Reading the ones that need more than the output:
- 1. The book explains the error as the container's lifetime ending "once the block finishes". The array line shows that nothing ends for a
Copyvalue, and thev = vec![9]line shows the name itself is still usable — what went away is the value, moved into the loop. The refusal below fires inside the loop body, before any block has finished. - 2. A
forloop always callsIntoIterator::into_iteron what you give it.&Vecand&HashMaphave anIntoIteratorimpl that happens to calliter(), so the table is right for them, but the equivalence is a promise each type makes, not a rule of the loop:&Stringmakes no such promise. - 5. The index loop compiled, ran three passes against a
Vecthat ended with four items, and never saw the 4.0..v.len()is evaluated once, before the first pass. - 6. In a
forloop,continuestill moves the iterator on. In awhileloop the increment is your own line, and acontinueabove it skips it:istays at 1 until the guard stops the loop. - 9.
first_negativeis aloopwith nobreakin it, left byreturnboth times.?is areturnin disguise, a panic unwinds out of it, andstd::process::exitends the process from inside it — without running destructors. - 11.
xtook the single value0. The innermost loop reached 1001 on the firsty, so the outer loops never advanced. - 13.
gotois an ordinary identifier. The words Rust keeps back for future use are a separate list in the Reference ↗, andgotois not on it. - 14. In C, several failure points jump to one
cleanup:label that frees what was acquired. Inopen_both, the earlyreturndropsconfig, and the success path dropslogthenconfig, in reverse order of creation. No label is involved. A labelled block withbreak 'labelis the nearest Rust spelling of the jump itself, and Loop labels shows it, but it is never needed for cleanup. - 16. The closure records each condition it is asked.
is(7)was never called: the chain stopped at the firsttrue. - 17. "Binding a name to a value" is
let, andletis a statement, as the refusal below shows. But the book names the=as the thing that makes it one, and an=withoutletis an assignment expression:let assigned = total = 5;compiles, andassignedis(). The book's third form, declarations, also covers more thanfn,structandenum: every item is one, includinguse,mod,const,static,implandtrait.
Claims the compiler settles¶
Each fence below is real rustc 1.98.0 output for the file it names. Lines that repeat the fence above them — the aborting summary and the rustc --explain pointer — are dropped.
1. The move is reported inside the loop, at the implicit into_iter() call:
error[E0382]: borrow of moved value: `container`
--> moved_inside_the_loop.rs:4:34
|
2 | let container = vec![1, 2, 3];
| --------- move occurs because `container` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | for item in container {
| --------- `container` moved due to this implicit call to `.into_iter()`
4 | println!("{item} of {}", container.len());
| ^^^^^^^^^ value borrowed here after move
|
help: consider iterating over a slice of the `Vec<i32>`'s content to avoid moving into the `for` loop
|
3 | for item in &container {
| +
2. &String has no IntoIterator impl, so for c in &s is refused. Ask for s.chars() or s.bytes() instead.
error[E0277]: `&String` is not an iterator
--> for_over_a_string_ref.rs:3:14
|
3 | for c in &s {
| ^^ `&String` is not an iterator
|
= help: the trait `Iterator` is not implemented for `&String`
= note: required for `&String` to implement `IntoIterator`
3. A loop variable nobody reads is a warning, and the fix it suggests is an underscore — _i, or _ alone, which binds nothing at all. The program still builds and prints 10.
warning: unused variable: `i`
--> unused_loop_variable.rs:3:9
|
3 | for i in 0..10 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
5. The for loop borrows v for its whole length, so a push inside it is refused:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> push_while_iterating.rs:5:13
|
3 | for x in &v {
| --
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | if *x == 2 {
5 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
7 and 15. An integer is not a condition, in a while or an if:
error[E0308]: mismatched types
--> while_on_an_integer.rs:3:11
|
3 | while n {
| ^ expected `bool`, found integer
error[E0308]: mismatched types
--> if_on_an_integer.rs:3:8
|
3 | if item {
| ^^^^ expected `bool`, found integer
8. The book gives the convention and no reason. rustc gives three. The first is a warning that is on by default. The second: initialization analysis does not evaluate the condition, so a variable assigned inside while true counts as possibly uninitialized after it.
warning: denote infinite loops with `loop { ... }`
--> while_true_is_not_loop.rs:3:5
|
3 | while true {
| ^^^^^^^^^^ help: use `loop`
|
= note: `#[warn(while_true)]` on by default
error[E0381]: used binding `x` is possibly-uninitialized
--> while_true_is_not_loop.rs:7:16
|
2 | let x: i32;
| - binding declared here but left uninitialized
3 | while true {
| ---- if this condition isn't met and the `while` loop runs 0 times, `x` is not initialized
...
7 | println!("{x}");
| ^ `x` used here but it is possibly-uninitialized
|
= note: when checking initialization, the compiler describes possible control-flow paths without evaluating whether branch conditions can actually have the values shown
The third: a while loop has type (), so a function cannot end in one, even one that can only leave by return. Write loop in either file and it compiles.
error[E0308]: mismatched types
--> while_true_returns_unit.rs:2:5
|
1 | fn first() -> i32 {
| --- expected `i32` because of return type
2 | / while true {
3 | | return 1;
4 | | }
| |_____^ expected `i32`, found `()`
|
= note: `while` loops evaluate to unit type `()`
17. let is the statement, and it cannot stand where a value is expected:
error: expected expression, found `let` statement
--> let_is_a_statement.rs:2:14
|
2 | let a = (let b = 1);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
18. break with a value is refused in a while loop:
error[E0571]: `break` with value from a `while` loop
--> break_value_from_while.rs:5:9
|
3 | let x = while n < 3 {
| ----------- you can't `break` with a value in a `while` loop
4 | n += 1;
5 | break n;
| ^^^^^^^ can only break with a value inside `loop` or breakable block
19. The book's own match from p. 51 — 0, 10..=20, 40 | 80 — without its _ arm, on a u8. Not a warning:
error[E0004]: non-exhaustive patterns: `1_u8..=9_u8`, `21_u8..=39_u8`, `41_u8..=79_u8` and 1 more not covered
--> match_missing_arms.rs:3:11
|
3 | match item {
| ^^^^ patterns `1_u8..=9_u8`, `21_u8..=39_u8`, `41_u8..=79_u8` and 1 more not covered
|
= note: the matched value is of type `u8`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown, or multiple match arms
|
6 ~ 40 | 80 => {},
7 + _ => todo!()
|
Claim 4, measured in assembly¶
The claim is about generated code, so it was checked in generated code: three functions summing a &[u64], one indexing 0..v.len(), one indexing 0..n with n a separate argument, one iterating directly. Each was compiled with rustc --edition 2024 -C opt-level=<0 or 3> --emit asm, and every panic_bounds_check call inside each function was counted. The source and the counting script are on Loops without an index.
opt-level=0 sum_by_len calls panic_bounds_check: 1
opt-level=0 sum_by_n calls panic_bounds_check: 1
opt-level=0 sum_by_iter calls panic_bounds_check: 0
opt-level=3 sum_by_len calls panic_bounds_check: 0
opt-level=3 sum_by_n calls panic_bounds_check: 1
opt-level=3 sum_by_iter calls panic_bounds_check: 0
So the check the book warns about is real in a debug build. With optimization on, it survives only where the compiler cannot prove the index is in range — and in sum_by_n even that one is a single comparison of n - 1 against the length before the loop starts, not a check per element. Iterating directly never has one, at any level. The safety argument (claim 5) is the stronger reason to prefer it.
Claim 21, read from the HIR¶
RUSTC_BOOTSTRAP=1 rustc --edition 2024 -Zunpretty=hir while_in_hir.rs prints the program after rustc has rewritten its loops, for a while n < 3 { n += 1; } followed by for i in 0..2 { n += i; }:
let mut n = 0;
loop { if n < 3 { n += 1; } else { break; } }
{
let _t =
match into_iter(Range { start: 0, end: 2 }) {
mut iter =>
loop {
match next(&mut iter) {
None {} => break,
Some { 0: i } => { n += i; }
}
},
};
_t
}
Every loop is a loop underneath. The for loop is built from match, and so is ?, but the while loop is built from if. Printing the HIR walks through the for and ? rewrites line by line.
Tests for anything else you read about flow control¶
- Does it say why a value is gone after a loop? The answer is a move into
into_iter, visible inside the loop body. An explanation in terms of lifetimes ending or blocks finishing will mislead you about arrays and references. - Does it treat
for x in &cas a language rule? It is an impl on&C, and a type can lack one. - Does it say
while trueandloopdiffer only in style? Try assigning a variable inside each and using it after. - Does it list
breakas the only way out of a loop? Countreturn,?, a panic andprocess::exit. - Does it call a missing
matcharm a warning? It isE0004. The unreachable-arm check is the one that is a warning. - Does it recommend labels or
goto-like jumps for cleanup? In Rust that isDrop.
See also¶
- Flow control: what each construct evaluates to — the map of the section, with the book's subsections beside the lessons
- Books — where Rust in Action sits among the other Rust books
- What
Cowexplanations get wrong, run — the same treatment for another topic - The never type
!— why aloopwith nobreakfits any return type - Drop and RAII — the cleanup that claim 14 is really about
- What Rust: The Practical Guide says about functions and code blocks, run — the same treatment for a second book's §2.3 "Functions" and §2.4 "Code Blocks"
Sources¶
- Tim McNamara, Rust in Action (Manning, 2021), §2.4 "Flow control", pp. 45–52, including listings 2.7 and 2.8
- The Rust Reference: loop expressions ↗, statements ↗, assignment expressions ↗, keywords ↗
std::process::exit↗ — "no destructors on the current stack or any other thread's stack will be run"
Po polsku¶
Sekcja 2.4 książki Rust in Action dobrze opisuje, co robią pętle i rozgałęzienia. Kiedy wyjaśnia, dlaczego tak się dzieje, bywa nieprecyzyjna. Sprawdziliśmy 21 twierdzeń z tej sekcji na rustc 1.98.0: dwanaście jest prawdziwych, cztery są prawdziwe pod warunkiem, a pięć wymaga poprawki. Najważniejsza poprawka dotyczy pętli for. Po for item in container wektora nie da się użyć nie dlatego, że po zakończeniu bloku „skończył się jego czas życia”. Pętla przenosi go do into_iter już w pierwszej linii, więc błąd E0382 pojawia się nawet w ciele pętli. Tablica [1, 2, 3] typu Copy zostaje nietknięta, a samej nazwie można później przypisać nową wartość.
Pozostałe poprawki są krótsze. loop kończy się nie tylko przez break, ale też przez return, ?, panikę albo process::exit. Sprzątanie w stylu C (skok do etykiety cleanup:) robi w Ruscie Drop, a nie etykieta pętli. Brakujące ramię match to błąd E0004, a nie ostrzeżenie. Wreszcie while pod spodem to loop z if, a nie z match. Do tego jedno zastrzeżenie: instrukcją jest let, ale samo przypisanie x = 5 (bez let) jest wyrażeniem typu (). Porada, żeby zamiast while true pisać loop, jest słuszna i nie chodzi tylko o styl. Kompilator ostrzega (while_true), nie uzna zmiennej przypisanej w while true za zainicjowaną (E0381) i nie pozwoli, by funkcja zwracająca i32 kończyła się taką pętlą (E0308).
Szukaj po polsku: Rust in Action errata · przeniesienie własności w pętli for · while true a loop · wyrażenie przypisania typu () · rust non-exhaustive patterns E0004