Control Flow, Pattern Matching & Destructuring Mechanics
Master expression evaluation, compiler jump tables vs decision trees, exhaustive matching, destructuring, and let-else bindings.
Rust is fundamentally an expression-oriented language. In C-family languages, statements execute imperatively without returning values. In Rust, almost every construct (code blocks, if/else, match, and loop breaks) evaluates to a concrete value.
Paired with exhaustive pattern matching, control flow in Rust eliminates entire categories of branching and state handling bugs at compile time.
rustc compiles dense enum match arms into direct hardware jump tables for constant-time dispatch.
1. Expression-Oriented Control Flow#
In Rust, the presence or absence of a trailing semicolon determines whether a block produces a value or evaluates to unit ():
123456789101112131415161718192021222324fn compute_tier(score: u32) -> &'static str { // if/else is an expression that yields a value let tier = if score >= 90 { "Gold" } else if score >= 75 { "Silver" } else { "Bronze" }; tier } fn loop_with_value() -> u32 { let mut counter = 0; // loop can return a value via break let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; result // Evaluates to 20 }
2. Theoretical Mechanics: Exhaustiveness Invariants#
Pattern matching is governed by the mathematical requirement of exhaustiveness. Let be the set of patterns tested against an input domain value . The compiler enforces that the union of pattern domains covers the entire type domain:
Furthermore, optimal matching implies disjoint pattern partitions whenever possible:
If any possible state of is unhandled, compilation halts with an exhaustiveness violation error.
3. Hardware Lowering: Jump Tables vs Decision Trees#
How does rustc compile a match expression into CPU instructions? The strategy depends on discriminant density:
| Discriminant Pattern | Lowered Architecture | Time Complexity | Machine Instructions |
|---|---|---|---|
| Dense Discriminants (e.g. 0, 1, 2, 3) | Direct Jump Table | Single indirect jump via array of code offsets. | |
| Sparse Discriminants (e.g. 1, 1000, 99999) | Binary Decision Tree | Sequenced comparisons (cmp, je, jg) splitting the search range. |
Some(x) if x > 10) executes runtime logic. Because the compiler cannot statically predict runtime predicate outcomes, match arms with guards do not count toward exhaustiveness. You must provide an unguarded fallback pattern.4. Advanced Destructuring & let-else#
Modern Rust provides powerful constructs for extracting nested data:
12345678910111213141516171819202122232425262728enum Event { KeyPress(char), Click { x: i64, y: i64 }, Tick, } fn handle_event(event: Event) { match event { // Struct destructuring with renaming Event::Click { x, y: 0 } => println!("Horizontal axis click at x={}", x), Event::Click { x, y } => println!("Click at coordinates ({}, {})", x, y), // Pattern binding with @ operator (range + variable capture) Event::KeyPress(c @ 'a'..='z') => println!("Lowercase key: {}", c), Event::KeyPress(c) => println!("Other key: {}", c), Event::Tick => (), } } // Guarding with let-else (Idiomatic early exit) fn parse_port(input: &str) -> Option<u16> { let Ok(port) = input.parse::<u16>() else { // The else block is divergent: it MUST return, break, or panic! return None; }; Some(port) }
5. Concept Verification#
Test your understanding of Rust pattern matching and control flow lowering:
How does the Rust compiler lower a match expression when enum discriminant tags are sequential and dense?
Why does adding an 'if guard' to a match arm prevent it from fulfilling pattern exhaustiveness?