A statement is a component of a block. Rust has two kinds, declaration statements
(items and let) and expression statements. Control flow such as
if, match and the loops are expressions, so they produce
values too (Reference: Statements, Loop expressions, If expressions, Match expressions).
Statements
Statement:
;
| Item
| LetStatement
| ExpressionStatement
| OuterAttribute* MacroInvocationSemi
ExpressionStatement:
ExpressionWithoutBlock ;
| ExpressionWithBlock ;?
- An item declared in a block is scoped to that block. It has no
canonical path and does not capture the function’s parameters, generic parameters or
local variables. Associated items from an
implblock remain accessible wherever the type is. - An expression statement evaluates an expression and ignores its result.
- An expression with a block, such as
iformatch, may omit the semicolon when used as a statement. Then its type must be(). If that creates ambiguity, it is parsed as a statement.
let mut v = vec![1, 2, 3];
v.pop(); // the returned Option is ignored
if v.is_empty() {
v.push(5);
} else {
v.remove(0);
} // no semicolon needed
[1]; // a separate statement, not an index into the if
// if true { 1 } // error: type of the block is i32, expected ()
if true { 1 } else { 2 }; // ok: the semicolon drops the value
Outer attributes are allowed on statements; only cfg and the lint
attributes have a meaning there.
let statements
LetStatement:
OuterAttribute* let PatternNoTopAlt ( : Type )?
(
= Expression
| = Expression except LazyBooleanExpression or end with a }
else BlockExpressionNoInnerAttributes
)? ;
let x = 5; // immutable; the type is inferred
let mut count: u32 = 0; // mutable, with a type
let (a, b) = (1, 2); // a pattern
let y; // declared without an initialiser
y = x + 1; // assigned once before use
let x = x * 2; // shadows the earlier x
- Variables are visible from the declaration until the end of the enclosing block, unless shadowed.
- The pattern must be irrefutable unless there is an
else. - Top-level or-patterns are not allowed in
let. - A variable with no initialiser must be assigned before use.
let else
With an else block the pattern may be refutable. If it does not match,
the else block runs, and it must diverge, with for example
return, break, continue or panic!.
The bindings are then in scope after the statement. An irrefutable pattern with
else produces a lint.
fn first_word(text: &str) -> &str {
let Some(word) = text.split_whitespace().next() else {
return ""; // the else block must diverge
};
word
}
let else keeps the success path flatThe initialiser cannot be a lazy boolean expression, or end with },
without parentheses, because let x = if a { b } else { c } else { d }
would be ambiguous.
if and if let
IfExpression:
if Conditions BlockExpressionNoInnerAttributes
( else ( BlockExpressionNoInnerAttributes | IfExpression ) )?
Conditions:
Expression except StructExpression
| LetChain
The condition must have type bool; there is no implicit conversion. Its
parentheses are not required. Both branches must have compatible types, and if there
is no else the block must be of type ().
let n = 5;
if n > 0 {
println!("positive");
} else if n < 0 {
println!("negative");
} else {
println!("zero");
}
let label = if n % 2 == 0 { "even" } else { "odd" };
if let Some(x) = some_option {
println!("{x}");
} else {
println!("nothing");
}
Let chains
Conditions may be joined with &&, mixing boolean expressions and
let matches. They are evaluated left to right and stop at the first
failure. Bindings from each pattern are in scope for later conditions and the body.
Let chains are stable from Rust 1.88, and only in edition 2024.
LetChain: LetChainCondition ( && LetChainCondition )*
LetChainCondition: Expression except ExcludedConditions
| OuterAttribute* let Pattern = Scrutinee except ExcludedConditions
if let Some(inner) = outer
&& let Some(number) = inner
&& number == 1
{
println!("got one");
}
while let Some(x) = stack.pop()
&& x > 0
{
println!("{x}");
}
if and whileAn operand of a let chain may not be a lazy boolean, range, assignment or struct
expression unless it is parenthesised. If any condition is a let, no
condition may use || at the top level, because the operator would be
taken as part of the scrutinee. Add parentheses: if let Some(x) = foo && (a
|| b).
match
MatchExpression:
match Scrutinee { InnerAttribute* MatchArms? }
Scrutinee:
Expression except StructExpression
MatchArms:
( MatchArm => ( ExpressionWithoutBlock , | ExpressionWithBlock ,? ) )*
MatchArm => Expression ,?
MatchArm:
OuterAttribute* Pattern MatchArmGuard?
MatchArmGuard:
if MatchConditions
let message = match x {
0 | 1 => "not many",
2..=9 => "a few",
n if n < 0 => "negative",
_ => "lots",
};
match expression with an or-pattern, a range and a guard- The scrutinee and every pattern must have the same type. Patterns are tried in order, and the first arm that matches is taken.
- A
matchmust be exhaustive: its patterns must cover every possible value. - A value expression as scrutinee is put in a temporary. A place expression is not; bindings may copy or move from it.
- A
matchwith no arms is valid on a type with no values. It has type!. - Each or-alternative must bind the same names, with the same types and binding modes. Alternatives are tested left to right.
Guards
A guard is an additional bool condition after if. The
pattern is tested first. If it matches, a shared reference is taken to the matched
part, the guard is evaluated (so it cannot change the scrutinee), and only then
are the bindings moved or copied. If the guard fails, the next pattern is tried.
A guard may run more than once when the arm has several |
alternatives.
Since Rust 1.95 a guard may be a let match, and let
conditions may be joined with &&:
match cmd {
Command::Run(name) if let Some(first) = name.chars().next() => {
println!("{name} starts with {first}");
}
Command::Run(name) => println!("{name} is empty"),
_ => {}
}
if let guardLoops
LoopExpression:
LoopLabel? (
loop BlockExpression
| while Conditions BlockExpression
| for Pattern in Expression except StructExpression BlockExpression
| BlockExpression (labelled block)
)
LoopLabel: LIFETIME_OR_LABEL :
| Form | Value | break | continue |
|---|---|---|---|
loop | The type of its break operands, or ! if it has no break | Yes, with a value | Yes |
while | () | Yes, no value | Yes |
for | () | Yes, no value | Yes |
| Labelled block | The type of its break operands and final expression | Yes, with a value | No |
loop and break with a value
let (mut a, mut b) = (1, 1);
let result = loop {
if b > 10 {
break b; // loop has the value of break
}
let c = a + b;
a = b;
b = c;
};
assert_eq!(result, 13);
loop that produces a valueA break with no expression means break (). A
loop with no break has type !.
while and while let
The condition is tested before each pass. while let loops until its
pattern stops matching, and it is equivalent to loop { match EXPR { PATS => {
body }, _ => break } }.
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{top}");
}
for
for runs a block once for each value from an
IntoIterator. The pattern must be irrefutable. In detail,
for pat in expr { body } behaves as:
{
let result = match IntoIterator::into_iter(expr) {
mut iter => loop {
let mut next;
match Iterator::next(&mut iter) {
Option::Some(val) => next = val,
Option::None => break,
};
let pat = next;
let () = { body };
},
};
result
}
for loopThe outer match keeps temporaries in expr alive for the
whole loop. IntoIterator, Iterator and Option
always refer to the standard library items, whatever is in scope.
Labels
A loop or block may be labelled with a 'name: prefix, and
break and continue may name it. Labels follow the same
hygiene and shadowing rules as local variables. '_ is not a valid
label.
'outer: for i in 0..10 {
for j in 0..10 {
if j == 3 { continue 'outer; }
if i == 2 { break 'outer; }
println!("{i} {j}");
}
}
let value = 'found: {
for item in &items {
if item.matches() { break 'found Some(item); }
}
None
};
A break inside a labelled block must name the label. The block’s type
is the least upper bound of all its break operands and its final
expression.
continue
continue ends the current pass and returns to the loop head: the
condition of a while, or the iterator call of a for.
Without a label it applies to the innermost loop. It is not allowed in a labelled
block.
return
return expr; leaves the enclosing function or closure with a value, and
has type !. A bare return; returns (). Within a
function, the final expression of the body is the usual way to give the result; an
explicit return is mainly used for early exits.