Control Flow
This chapter covers what decides which code runs next: conditionals, loops,
break and continue, labeled blocks, match and matches, branch hints, and
error handling. The patterns these statements take apart are in
Patterns.
Conditional Statements
A condition is a bool. No other type converts to one.
let x = 7;
let mut sign = "";
if x < 0 {
sign = "negative";
} else if x == 0 {
sign = "zero";
} else {
sign = "positive";
}
assert sign == "positive";
If Expression
let x = -3;
let abs = if x < 0 { -x } else { x };
assert abs == 3;
let score = 85;
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" };
assert grade == "B";
If Let Pattern Matching
let opt: Option<i32> = Option::<i32>::Some(42);
let mut got = 0;
if let Some(x) = opt {
got = x;
} else {
got = -1;
}
assert got == 42;
Let Else
let PATTERN = EXPR else { ... }; binds a refutable pattern whose bindings
escape into the enclosing scope. On a match they cover the rest of the block.
Otherwise the else block runs, and it must diverge (return, break,
continue, panic, …). The else block does not see the bindings.
fn parse_port(s: String) -> i32 {
let Ok(port) = i32::from_str(&s) else {
return -1;
};
return port; // `port` is in scope here
}
test {
assert parse_port("8080") == 8080;
assert parse_port("http") == -1;
}
An irrefutable pattern is an error, since its else could never run. Write a
plain let instead.
While Loop
let mut i = 0;
let mut sum = 0;
while i < 10 {
sum += i;
i = i + 1;
}
assert sum == 45;
While Let Pattern Matching
while let allows iterating while a pattern matches:
let items: List<i32> = [1, 2, 3];
let mut iter = items.into_iter();
let mut seen: List<i32> = [];
while let Some(x) = iter.next() {
seen.push(x);
}
assert seen == [1, 2, 3];
The loop runs while the pattern matches, and ends the first time it does not.
For Loop
C-style for loop with initialization, condition, and update. Parentheses are optional:
let mut sum = 0;
for let mut i = 0; i < 10; i = i + 1 {
sum += i;
}
assert sum == 45;
// With parentheses (also valid)
for (let mut i = 0; i < 10; i = i + 1) {
sum += i;
}
assert sum == 90;
// All parts are optional
let mut n = 0;
for ;; {
n += 1;
if n == 3 { break; }
}
assert n == 3;
Semantics
for init; cond; update { body } runs init once. Each iteration then checks
cond, runs body if it holds, and runs update. continue ends the body and
still runs update, as in C.
Each iteration has its own copy of the bindings init declares, as ECMA-262's
for (let …) does. Before update runs, the next iteration's bindings are
created holding the current values, and update and the next cond act on
them. So a closure or a reference taken in one iteration keeps that iteration's
binding, and later iterations do not change it:
let mut fs: List<fn() -> i32> = [];
for let mut i = 0; i < 3; i += 1 {
fs.push(|| i);
}
assert fs[0]() == 0 && fs[1]() == 1 && fs[2]() == 2; // not 3, 3 and 3
For with Pattern Condition
The condition part of a C-style for loop can use let pattern matching:
let items: List<i32> = [10, 20, 30];
let mut iter = items.into_iter();
let mut sum = 0;
for ; let Some(x) = iter.next(); {
sum += x;
}
assert sum == 60;
// With update expression
let mut again = items.into_iter();
let mut count = 0;
for ; let Some(x) = again.next(); count += 1 {
assert x == items[count];
}
assert count == 3;
The loop runs while the pattern matches, as while let does, and the update
runs after each iteration.
For-Of Loop
for-of walks any value whose type implements
IntoIterator:
let numbers: List<i32> = [1, 2, 3, 4, 5];
let mut sum = 0;
for let n of numbers {
sum += n;
}
assert sum == 15;
// With mutable binding
for let mut item of numbers {
item = item * 2; // Can modify the local binding
sum += item;
}
assert sum == 45 && numbers[0] == 1;
// Custom types that implement IntoIterator also work
let my_collection = Countdown { n: 3 };
let mut order: List<i32> = [];
for let x of my_collection {
order.push(x);
}
assert order == [3, 2, 1];
Semantics
for let item of collection { body } calls collection.into_iter() once,
before the first iteration. Each iteration then calls next() on the result,
binds the element to item and runs the body. The loop ends when next()
returns None.
The binding is a copy of each element
(Value Semantics), so changing it does not
change the collection. for let x of &xs and for let x of &mut xs bind
references instead, as
iteration by reference states.
Each iteration binds it anew, so a closure or a reference taken in one iteration
keeps that iteration's element.
Iterating a List reads the list's own storage, so a body that shrinks the
list it walks breaks the contract a shrinking list puts on its
views.
The binding must match every element, as a let pattern must (see
Patterns That Cannot Fail). A
pattern that can fail (for let Some(x) of xs, a narrowing type pattern) is a
compile error. Match on the element in the body instead.
Tuple for-of (compile-time expansion)
When the iterable is a tuple, the body runs once per element, and each run types the binding as that element's type. So one loop walks elements of different types, and each call in the body dispatches on its element's type.
let t = [42, "hello", true];
let mut out = "";
for let v of t {
out.push_str(`${v} `); // expanded to three blocks, each with the correct type
}
assert out == "42 hello true ";
break and continue are errors inside a tuple for-of body, since the runs are
not iterations of one loop.
.enumerate() binds the index beside the element:
for let [i, v] of t.enumerate(). The index is a compile-time constant, so it
is the one non-literal a tuple accepts as a subscript (t[i]), for reads and
writes alike. A mut index can change, so it is not a subscript.
Tuple comprehension
Wrapping the same walk in [...] collects one result element per source
element. The braces hold a single expression, the element's value, since every
position of the result tuple has one.
impl<..T: Doubled> Doubled for [..T] {
fn doubled(&self) -> [..T] {
return [for let v of *self { v.doubled() }];
}
}
test {
assert [1, "ab"].doubled() == [2, "abab"];
}
The .enumerate() form works here too:
[for let [i, v] of t.enumerate() { ... }].
The source must be a variadic tuple ([..T]). A concrete tuple cannot be
walked this way.
Infinite Loop
let mut n = 0;
loop {
// runs forever until break
n += 1;
if n == 5 {
break;
}
}
assert n == 5;
Break and Continue
break exits the innermost enclosing loop. continue skips to the next iteration.
// break example
let mut i = 0;
while i < 100 {
if i == 10 {
break; // exit the loop
}
i = i + 1;
}
assert i == 10;
// continue example
let mut kept: List<i32> = [];
for let mut j = 0; j < 10; j = j + 1 {
if j == 5 {
continue; // skip 5
}
kept.push(j);
}
assert kept.len() == 9 && kept[5] == 6;
Both work with while, for, and loop, and are errors outside a loop. A loop
carries no label, and continue never takes one. break LABEL is a different
statement: it leaves an enclosing labeled block, and is how
to leave more than the innermost loop. A closure body starts its own loop scope,
so a loop around a closure binds nothing written inside it.
Labeled Blocks
A labeled block is a named block that break LABEL leaves from anywhere inside
it. It is Wado's only non-local jump, and it replaces loop labels, labeled
continue, and goto. It is also the block form that carries a value.
Syntax
LABEL: { ... }, in statement or expression position.
The label is an identifier followed by a colon. break LABEL; leaves the
block, and break LABEL: expr; leaves it with a value. () is a value like any
other, so break LABEL: () says what break LABEL says, and break () says
what break says. Nested blocks may reuse a label, and a break targets the
innermost block with that label. The block opens a new scope, and a name
declared inside may shadow an outer one.
The label is required because { field: value } on its own could be a block or
a struct literal. An unlabeled { ... } block is a parse error.
Escaping and Early Exit
break LABEL inside a loop nest leaves all of it at once, and the block's tail
is the path no break took. Inside a block, break LABEL skips the rest, so a
chain of guards stays flat instead of nesting one inside the next. A break
may also leave an effect handler's do block (see
Handler Scope).
let grid: List<List<i32>> = [[1, 2], [3, 4]];
let needle = 3;
let mut hit = [0, 0];
search: {
for let r of 0..<grid.len() {
for let c of 0..<grid[r].len() {
if grid[r][c] == needle {
hit = [r, c];
break search; // leaves both loops
}
}
}
hit = [-1, -1]; // reached only when no break was taken
}
assert hit == [1, 0];
As an Expression
A labeled block used as a value yields two ways: through break LABEL: expr,
and through its trailing statement on the path that reaches the end. Both are
branches, so every path must agree on one type. They unify against the type
expected where the block sits, so a literal coerces to that rather than to its
own default. Any expression position takes one, not only the right-hand side of
a let.
let items: List<Entry> = [{ key: "a", value: 1 }, { key: "b", value: 2 }];
let key = "b";
let found = search: {
for let item of items {
if item.key == key {
break search: item.value;
}
}
-1 // the value when no break is taken
};
assert found == 2;
A trailing statement that is not a value yields (). A block whose branches all
yield () has the type (), and one mixing () with a value is the same type
error as any other disagreement.
A path that cannot reach the end is no branch at all. When the trailing
statement is a loop that only break LABEL leaves, nothing reaches the tail, so
the breaks alone type the block. This is how a loop computes a value:
let mut n = 0;
let found = scan: {
loop {
n += 1;
if n * n > 50 { break scan: n; }
}
};
assert found == 8;
Match Expression
A match tests its scrutinee against each arm's pattern in order and runs the
first arm that matches. Its arms must be
exhaustive. A match produces a value, or
stands as a statement:
// Match expression (produces a value)
let result = match opt {
Some(x) => x * 2,
None => 0,
};
assert result == 42;
// Match with custom variants
let area = match shape {
Circle(r) => 3.14159 * r * r,
Rectangle([w, h]) => w * h,
Point => 0.0,
};
assert area == 6.0;
// Match statement (no value produced)
match command {
Start => engine.start(),
Stop => engine.stop(),
}
assert engine.running;
Matches Operator
The infix matches operator answers whether a value matches a pattern, as a
bool:
// Basic usage
let is_some = opt matches { Some(_) };
let is_circle = shape matches { Circle(_) };
assert is_some && is_circle;
// With guard
let is_large = shape matches { Circle(r) && r > 10.0 };
assert !is_large;
// In conditions
let mut has_value = false;
if opt matches { Some(_) } {
has_value = true;
}
assert has_value;
Scope
Pattern bindings are scoped to the guard only and do not escape, so x is not
in scope after the pattern:
if opt matches { Some(x) } && x > 0 { }
Use a guard inside the pattern instead:
assert opt matches { Some(x) && x > 0 };
Branch Hints
builtin::cold_path() marks the path that contains it as rarely run, so the
other side of its branch is the likely one. It does not change what the program
computes.
It is a statement rather than a condition wrapper, so it works anywhere a branch
body does. That includes an if let or match arm, where no boolean condition
is available:
impl Buffer {
// Error/abort guard: the taken branch is cold.
fn get(&self, i: i32) -> i32 {
if i >= self.len {
builtin::cold_path();
panic("index out of bounds");
}
return self.data[i];
}
}
// `match` arm with no boolean condition.
fn run(command: Command) -> i32 {
return match command {
Command::Run => execute(),
Command::Crash => {
builtin::cold_path();
panic("crashed");
},
};
}
test {
let b = Buffer { data: [1, 2], len: 2 };
assert b.get(1) == 2 && run(Command::Run) == 1;
}
Placed on the fall-through after a guard whose taken branch diverges, it hints that the guard is likely taken:
impl Cache {
fn lookup(&self, key: String) -> i32 {
if let Some(v) = self.fast_path(key) {
return v;
}
builtin::cold_path(); // the slow path below is rarely reached
return self.slow_path(key);
}
}
test {
let cache = Cache { hot: "a" };
assert cache.lookup("a") == 1 && cache.lookup("bc") == 3;
}
Optimization Barrier
builtin::black_box(value) returns value unchanged but never as a
compile-time constant, so the compiler does not fold away a computation reading
it. Use it to keep a test or benchmark measuring the
work it names:
test "sign extension folds the redundant mask" {
assert to_i8(builtin::black_box(300)) == 44;
}
Without it, to_i8(300) folds to 44 and the test no longer reaches to_i8.
The barrier binds the Wado compiler only. The Wasm engine that runs the program is still free to fold the value.
Error Handling
An error either traps, which ends the program, or is a value the caller handles.
Unrecoverable Errors (Traps)
A trap ends the program on a fatal error. Nothing recovers from it.
Two prelude functions trap on purpose:
panic(message)writesmessageto standard error, then traps.unreachable()traps.
#[expect_trap]
test "panic logs the message to stderr, then traps" {
panic("Fatal error");
}
#[expect_trap]
test "unreachable traps" {
unreachable();
}
#[expect_trap]
test "a failed assert traps" {
let condition = false;
assert condition;
}
Recoverable Errors (Result Type)
A function that can fail returns a Result, and its caller decides what an
Err means:
variant ConfigError {
Io(FsError),
Parse(ParseIntError),
}
impl From<ParseIntError> for ConfigError;
fn parse_config(text: String) -> Result<Config, ParseIntError> {
return Ok(Config { port: i32::from_str(text.trim())? });
}
fn read_config(path: String) -> Result<Config, ConfigError> with Preopens {
let content = fs::read_to_string(path)
.map_err(|e| ConfigError::Io(e))?;
let config = parse_config(content)?;
return Ok(config);
}
test {
// Handle with pattern matching
let message = match read_config("missing.toml") {
Ok(config) => `port ${config.port}`,
Err(Io(e)) => `cannot read: ${e}`,
Err(Parse(e)) => `bad port: ${e}`,
};
assert message.starts_with("cannot read");
}
Error Propagation
The postfix ? operator unwraps a Result or an Option. Where there is
nothing to unwrap, it returns from the enclosing function:
- On
Ok(v),expr?isv. OnErr(e), the function returnsErr(From::from(e)), so the error converts to the function's own error type. Above,parse_config(content)?turns aParseIntErrorinto aConfigErrorthrough theFromimpl. - On
Some(v),expr?isv. OnNone, the function returnsNone.
? on a Result needs a function that returns a Result, and ? on an
Option one that returns an Option. Any other pairing is an error:
fn outer() -> Result<i32, String> {
let x = inner()?;
return Result::<i32, String>::Ok(x);
}
Inside a closure, ? returns from the closure
(Closures).
Rationale: WEP: Conversion Traits.
