Control Flow
Control flow statements determine the execution path of your program.
If Expressions
Basic If
if condition {
// Execute if condition is true
}If-Else
if condition {
// Execute if condition is true
} else {
// Execute if condition is false
}Else-If Chains
if condition1 {
// Execute if condition1 is true
} else if condition2 {
// Execute if condition2 is true
} else if condition3 {
// Execute if condition3 is true
} else {
// Execute if all conditions are false
}Conditional Requirements
Conditions must be boolean expressions:
// Valid conditions
if x > 0 { }
if flag { }
if a == b { }
if is_valid && is_ready { }
// Invalid conditions
// if x { } // Error: x is i32, not bool
// if 1 { } // Error: 1 is i32, not boolIf in Functions
Early Return
func clamp(x: i32, min: i32, max: i32) -> i32 {
if x < min {
return min
}
if x > max {
return max
}
return x
}Conditional Return
func sign(x: i32) -> i32 {
if x > 0 {
return 1
} else if x < 0 {
return -1
} else {
return 0
}
}Expression-Based Return
An if/else is an expression: its value is the trailing expression of the taken
branch. It can be a function's implicit return value or be bound to a variable. Both
branches must yield the same type, so an else is required when the value is used.
A branch that leaves the scope instead of producing a value (return, break,
continue, panic(...), unreachable()) is exempt from that agreement: it never
reaches the point where the if has a value, so the other branch decides the type.
if n > 0 { return 1 } else { 2 } is an i32. The same rule applies to match arms.
func abs(x: i32) -> i32 {
if x >= 0 {
x // Implicit return
} else {
-x // Implicit return
}
}
func clamp_low(x: i32) -> i32 {
val y = if x < 0 { 0 } else { x } // bound to a variable
y
}Block Scopes
Variables declared in if/else blocks are scoped to that block:
func scoped() -> i32 {
val x: i32 = 10
if true {
val y: i32 = 20 // y only exists in this block
// Both x and y are accessible here
}
// Only x is accessible here
// return y // Error: y not in scope
return x
}Pool Blocks
A pool block is a scope whose allocations come from one contiguous bump arena
instead of the heap. Leaving the block moves the bump pointer back, releasing
everything the body allocated in a single step, whatever it allocated and however
much of it there was.
pool {
val label = "batch " + "one"
val scratch = Tensor::<f32, [16, 16]>::zeros()
println(label)
}
// the arena is back to its mark hereA pool may carry a label, which names the arena in a diagnostic and nothing else:
a pool is not a value, and nothing can refer to its label or break out of it.
Labels earn their keep once pools nest, which is the loop shape they exist for:
pool training {
for epoch in 0..100 {
pool batch {
// every intermediate here is gone at the closing brace, so
// the loop's memory use does not grow with the epoch count
}
}
}The arena captures the allocations the block itself writes. One a called function makes belongs to whoever that function hands it to, which the compiler cannot prove here, so it stays an ordinary heap allocation: the arena's speed is lost on that path and never its safety.
Three rules keep arena memory from outliving the block, all checked at compile time:
- Nothing the block already allocated may be written into something that outlives
it. Storing a
string, a collection, a tensor or a struct that the block took from the arena into a binding declared before thepoolwould leave it addressing bytes the block's exit reclaims. Scalars cross freely: ani32carries no address. A value the statement itself builds crosses too, because the compiler routes it off the arena (see below). - Nothing the block allocated may be handed to a callee that can store it past
the block. The rule above watches stores written inside the block; this one
watches the store a callee makes on your behalf. A
&mut selfreceiver and a&mutparameter are the two channels through which a callee reaches memory the caller holds, so passing an arena value through either (to a place declared before thepool) is refused. - A
returnorbreakthat leaves the block carries a number,boolorchar, or nothing. Leaving the block byreturn,breakorcontinueruns its release on the way out, so anything that could point into the arena may not travel with the jump. Abreaktargeting a loop opened inside the block stays in it, and carries what it likes. A?leaves the same way, so it may propagate anOption'sNoneor aResultwhose error is a number,boolorchar, and no other error.
mut total: i32 = 0
mut name: string = ""
pool {
val joined = "a" + "b" // the block's own: allocated in the arena
total = total + 1 // fine: a scalar carries no arena address
name = "c" + "d" // fine: routed off the arena, because `name` outlives it
name = joined // error: `joined` was already taken from the arena
}Routing
The compiler decides where an allocation comes from by asking who ends up owning it.
Where the owner is a place that outlives the block (a binding declared before it, a
field or element reached through one, or the referent of a reference) the allocation
is routed to the ordinary heap and the arena never sees it. That is why name = "c" + "d" above compiles: the whole statement is emitted with the arena switched off, so
the text it builds survives the release and reads correctly afterwards.
Routing applies to the value the statement builds, never to one built earlier. By the
time name = joined runs, joined's buffer is already in the bump region and no
choice of allocator can move it, so that store is refused.
The compiler has to prove a routed value holds nothing the block built earlier, and it
proves it through a struct or tuple literal, an array literal, and an if / match whose
every arm is a single expression (a match also needs its scrutinee proven). So
entry = Entry { label: "a" + "b", n: 1 } and name = if c { "x" } else { "y" } cross
the boundary. An arm that declares a binding of its own is not looked into, and is refused.
An allocation the block keeps for itself is unaffected: val joined = "a" + "b" is
owned by a binding that dies at the closing brace, so it takes the bump path, and a
reassignment of such a binding does too. So does a store or a call through a reference
the block takes to such a binding (val r = &mut local): it writes local, which dies
with the block. Any other reference the block holds (a mut one, one a call returned,
one to a binding declared before the pool) is assumed to reach past it.
Values a callee builds
A function's body is emitted outside every arena, so what it returns is heap memory the block's release never touches. A call to a function you declared therefore crosses the boundary, as long as every argument and the receiver cross it too: otherwise the callee could be handing back the very pointer the block gave it.
func render(n: i32) -> string {
"row {n}"
}
mut kept: string = ""
pool {
val step = 7
kept = render(step) // fine: `render`'s allocation is heap memory
kept = render(step).clone() // fine too: the store is routed off the arena
}
println(kept) // still valid after the releaseThe same test decides the second rule, because the question is the same one: whether the value being handed over is arena memory. What differs is only where the store lands.
struct Box { s: string }
impl Box {
func stash(&mut self, v: string) { self.s = v }
}
mut b = Box { s: "" }
pool scratch {
b.stash(render(7)) // fine: `render`'s allocation is heap memory
b.stash("a" + "b") // error: `b` outlives the arena that built the text
}A callee's write access is read from its signature, not from its body: a method taking
&mut self, or a function taking &mut T, is assumed to keep what it is given. A
&self receiver and a shared &T parameter have no way to write back, so they take
arena values freely.
The two rules part company over a builtin method such as .clone(). It is not a
function the backend emits somewhere else; it is instructions placed where you wrote
them. In a routed store that is harmless, because the arena is switched off across
the whole statement: kept = render(step).clone() compiles for the same reason
name = "c" + "d" does. Passed to a callee it is not harmless, because the argument
is emitted where it stands, in the arena, which is why b.stash("a" + "b") is
refused.
A call through a trait object (&dyn Renderer) is refused under both rules, and for
a different reason: the implementation behind the vtable is not known until the
program runs, so neither is what it allocates. Where the compiler cannot prove the
owner, the value stays in the block. That costs the arena's speed on such a path and
never its safety.
Destructors and PoolAware
A third rule is about what the block owns rather than about what escapes it. The
arena is released in one store, so it cannot run a destructor per object without
giving up the thing it exists for. A value of a type that implements Drop is
therefore refused wherever the block would own it, whether it is written in the
block or handed back by a function the block calls:
struct Handle {
id: i32
}
impl Drop for Handle {
func drop(&mut self) {
println("released {self.id}")
}
}
pool scratch {
val h = Handle { id: 1 } // error: 'Handle' implements 'Drop' but not 'PoolAware'
}PoolAware is how a type says its release can be batched instead. Implementing it
registers the external resource the value holds with the arena, and the arena
releases every registered resource in one sweep at the block's exit:
impl PoolAware for Handle {
func register_with_pool(&self, arena: &PoolHandle) {
}
func bulk_release(&mut self) {
println("swept {self.id}")
}
}With that impl in place the same pool block compiles. Inside the block the two
methods are what run, and the Drop above is not: register_with_pool is called
where the value is constructed, and bulk_release at the closing brace. Outside a
pool the opposite holds, and the destructor runs as it always did.
The sweep walks the registrations in reverse order of construction, the same order stack values are destroyed in. That is what makes a resource safe to depend on another: a view is released before the tensor it borrows from, and an operation before the stream it was queued on, because each registered after the thing it depends on.
pool scratch {
val first = Handle { id: 1 }
val second = Handle { id: 2 }
}
// prints: swept 2, then swept 1A type with no destructor at all needs neither method: its memory goes back with the
bulk arena free and nothing else is owed. Building the value before the pool is
the other way out: it never comes from the arena, so its ordinary destructor still
applies.
| Type | In a pool? |
|---|---|
No Drop | yes, reclaimed by the bulk arena free |
PoolAware (with or without Drop) | yes, bulk_release in reverse construction order at the block's exit |
Drop only | no, a compile error naming the pool |
See examples/ownership/pool_arena.nr
for a runnable program, and
examples/showcase/batch_arena.nr for a
batched forward pass built inside one.
Nested If Statements
func nested(a: i32, b: i32) -> i32 {
if a > 0 {
if b > 0 {
return a + b
} else {
return a - b
}
} else {
if b > 0 {
return b - a
} else {
return 0
}
}
}While Loops
Use while to repeat a block while a boolean condition is true:
while condition {
// loop body
}While Requirements
Loop conditions must be boolean expressions:
mut i: i32 = 0
while i < 10 {
i = i + 1
}
// Invalid
// while 42 { } // Error: expected bool conditionprefer-loop-over-while-true lint
while true { ... } compiles and runs, but the compiler emits a warning:
warning[prefer-loop-over-while-true] at 23..27: `while true { ... }` shouldbe written as `loop { ... }`; silence with `@allow(prefer_loop_over_while_true)`on the enclosing functionThe motivation is style, not safety; both forms produce identical machine
code. To silence the warning on a function (typically when transcribing code
from C, Python, or JavaScript), attach the @allow attribute:
@allow(prefer_loop_over_while_true)
func main() -> i32 {
mut i: i32 = 0
while true {
if i == 7 { break }
i = i + 1
}
return i
}The lint only triggers on the bare literal true. Parenthesised
while (true) { ... } is treated as an explicit escape hatch and is not
flagged. The recommended replacement is the loop { ... } statement below.
Loop (Infinite)
Use loop for an infinite loop. Unlike while, it has no condition: the only
way out is a break, and continue re-enters the body from the top. This is
the canonical infinite-loop form the prefer-loop-over-while-true lint
suggests.
mut attempts: i32 = 0
loop {
attempts = attempts + 1
if attempts > 5 {
break
}
}break and continue behave exactly as they do in while and for bodies.
Loop as a value expression
Because blocks are expressions, a loop can produce a value: break v exits
the loop and makes v the value of the whole loop expression.
mut i: i32 = 0
val first_even = loop {
i = i + 1
if i % 2 == 0 {
break i // the loop expression evaluates to i
}
}All value-carrying breaks for one loop must agree on type. With a label,
break outer value carries the value out of an outer loop, and the labeled loop
may itself be used in value position (val x = outer: loop { ... }).
Only loop can yield a value: it is the one loop guaranteed (by the absence of
a fall-through exit) to leave solely via a break. while and for always
evaluate to unit (), so a break value targeting one is a compile error.
For Loops
Use for to iterate over an integer range. Ranges can be exclusive (..) or inclusive (..=):
// Exclusive range: 0, 1, 2, ..., 9
for i in 0..10 {
// i takes values 0 through 9
}
// Inclusive range: 1, 2, ..., 5
for j in 1..=5 {
// j takes values 1 through 5
}For-Range Requirements
- Range bounds must be integer-compatible expressions.
- The iteration variable is implicitly declared and its type is inferred from the range bounds.
func sum_first_five() -> i32 {
mut sum: i32 = 0
for i in 0..=5 {
sum = sum + i // 0 + 1 + 2 + 3 + 4 + 5 = 15
}
return sum
}Iterating Backwards: .rev()
.rev() on a range walks the same bounds from the top down. Like .enumerate(),
a range must be parenthesised, because .. binds looser than a method call:
for i in (0..5).rev() {
// i takes values 4, 3, 2, 1, 0
}
for i in (0..=5).rev() {
// i takes values 5, 4, 3, 2, 1, 0
}The two spellings differ where they always do: 5 is not in 0..5, so reversing
it starts at 4; it is in 0..=5, so reversing that starts at 5. An empty
range is empty in either direction.
.rev() applies to a range only: it reorders the bounds themselves, not an
element stream, so xs.rev() over an array or a Vec<T> is a parse error naming
the spelling that works. It composes with everything else a head may carry, and
sits innermost, beneath the adapters and .enumerate():
for (rank, value) in (10..14).rev().enumerate() {
// rank: 0, 1, 2, 3 value: 13, 12, 11, 10
}The position still counts iterations upward while the value descends, for the same reason it counts from zero over any other head: it is a position, not a value the range holds.
A range index into a tensor takes .rev() too, reading that axis back to front;
see Slicing and indexing.
Striding: .step(n)
.step(n) on a range keeps every nth value of the walk. Like .rev() it applies
to a parenthesised range only, and .rev() goes beneath it: the reversed walk
starts at the last value and strides down.
for i in (0..10).step(3) {
// i takes values 0, 3, 6, 9
}
for i in (0..=12).step(4) {
// 0, 4, 8, 12: an inclusive range still yields a bound the stride lands on
}
for i in (0..10).rev().step(3) {
// 9, 6, 3, 0
}The stride is an integer of the range's own type and must be positive. A constant
stride below one is a compile error; a stride computed at run time is checked when
the loop starts, and a zero one panics rather than looping forever. The loop never
adds past its bound, so (0u8..255u8).step(10) ends at 250 instead of wrapping.
The other order, (a..b).step(n).rev(), walks different values and is rejected;
write .rev().step(n).
.step(n) sits beneath the adapters and .enumerate() exactly as .rev() does,
and a tensor index takes it too (t[(0..n).step(2)]).
Iterating with a Position: .enumerate()
A for head may bind a pair instead of a single variable when the iterable ends
in .enumerate(). The first binding is the position (a u64 counting from
zero), and the second is the element:
val scores: [i32; 3] = [5, 4, 3]
for (place, score) in scores.enumerate() {
println("place {place} scored {score}")
}The position is a u64 because that is what an index expression and .len()
use, so it reads back into the sequence it walks without a cast:
for (i, score) in scores.enumerate() {
val same = scores[i] == score // true, every iteration
}.enumerate() applies to a fixed-size array, a Vec<T>, a borrow of either, and
a range. A range must be parenthesised, because .. binds looser than a method
call: 0..n.enumerate() would enumerate n:
for (step, value) in (10..13).enumerate() {
// step: 0, 1, 2 value: 10, 11, 12
}The position is a count of iterations, not a value the sequence holds, which is why the two columns above differ.
The pair head and .enumerate() imply each other: a pair head over a plain
iterable, or .enumerate() under a single-variable head, is a parse error. Both
bindings live in the loop's own scope, so they shadow any outer name of the same
spelling for the loop's duration and disappear at its exit; naming them alike
(for (i, i) in ...) is a redefinition error rather than a shadow.
The iteration protocol
for is not limited to the built-in sequences. for x in e means:
- call
e.into_iter()once, producing an iterator; - call
.next()on that iterator repeatedly; - bind each
Some(v)to the loop variable and run the body; - stop when
.next()answersNone.
Two prelude traits state the contract, so no import is needed:
trait Iterator {
type Item
func next(&mut self) -> Option<Self::Item>
}
trait IntoIterator {
type Item
type Iter
func into_iter(self) -> Self::Iter
}Any type implementing either one may stand in a for head. A type that
implements Iterator is its own iterator: no second IntoIterator impl is
needed, and the loop uses the value directly:
@derive(Copy, Clone)
struct Countdown { remaining: i32 }
impl Iterator for Countdown {
type Item = i32
func next(&mut self) -> Option<i32> {
if self.remaining <= 0 { return Option::None }
self.remaining = self.remaining - 1
Option::Some(self.remaining)
}
}
val c = Countdown { remaining: 3 }
for n in c {
println("{n}") // 2, 1, 0
}Implement IntoIterator when the container and the cursor are different types,
which is what lets a container be walked more than once, since each for head
asks it for a fresh cursor.
The loop variable's type is the Item the iterator's own impl Iterator bound,
break and continue work as they do in any other loop (a label lands on the
for and reaches out of a nested one), and .enumerate() applies here too, its
position counting the loop's own steps.
Adapters. An adapter is an iterator that wraps another one, so it composes in
a single for head. The Iterator<Item = T> bound on its type parameter is what
lets its body call .next() on what it holds:
@derive(Copy, Clone)
struct Doubling<S> { inner: S }
impl<S: Iterator<Item = i32>> Iterator for Doubling<S> {
type Item = i32
func next(&mut self) -> Option<i32> {
match self.inner.next() {
Option::Some(v) => Option::Some(v * 2),
Option::None => Option::None
}
}
}
val doubled = Doubling { inner: Countdown { remaining: 3 } }
for n in doubled {
println("{n}") // 4, 2, 0
}Nothing between the source and the loop is materialized: each step pulls one element through the whole chain.
What is not covered. The built-in heads (a range, a fixed-size array, a
Vec<T>, a borrowed slice) do not go through the protocol. They compile to
counted loops directly, which is a lowering choice, not a difference in meaning.
Adapter methods: .map(f) and .filter(p)
A for head may end in a chain of .map(f) / .filter(p) calls, which need no
adapter type of their own:
.map(f)replaces each element withf(element);.filter(p)drops each element for whichp(element)is false.
for value in [1, 2, 3, 4].map(|x| x * 2) {
println("{value}") // 2, 4, 6, 8
}
for value in (0..12).filter(|x| x % 4 == 0) {
println("{value}") // 0, 4, 8
}A closure literal's parameter takes the type of the element it is handed, so it needs no annotation.
The chain runs left to right (a filter sees what the map before it produced) and composes to any depth. Each element is pulled through the whole chain one at a time; nothing in between is materialized:
for value in [1, 2, 3, 4, 5]
.map(|x| x * x)
.filter(|x| x > 4) {
println("{value}") // 9, 16, 25
}.map may change the element type, so the loop binding is whatever the function
returns. The function is an ordinary expression of function type (usually a
closure literal) evaluated once, before the loop, in the scope around it: it
cannot name the binding it feeds, and a closure binding used by a head is still
usable afterwards.
Like .enumerate(), these apply to every head: a range (parenthesised), a
fixed-size array, a Vec<T>, a borrow of either, and any type that implements
IntoIterator or Iterator. .enumerate() stays outermost, and its position
counts what the chain yielded, so it stays dense however much a filter drops:
for (rank, value) in [10, 3, 20, 4, 30].filter(|x: i32| -> bool { x >= 10 }).enumerate() {
println("{rank}: {value}") // 0: 10 1: 20 2: 30
}A .filter predicate must return bool and a .map function must return a
value; a function that does not take the element type is rejected where the head
is written.
Head form, not a value. .map / .filter are part of the for head, exactly
as .enumerate() is: val m = xs.map(f) is not a method call that resolves.
Write the adapter type, as above, when the pipeline has to be stored or returned.
Walking text: .chars() and .char_indices()
A string is UTF-8, so its characters are not its bytes. .chars() is an ordinary
iterator over Unicode scalar values and stands in a for head like any other, with
.enumerate() and the adapters composing over it. .char_indices() is the second
pair-yielding head form: it binds the byte offset of each scalar beside it, which
is the offset .slice(range) takes.
for c in "héllo".chars() { } // 5 scalars, 6 bytes
for (offset, c) in "héllo".char_indices() {
// offset: 0, 1, 3, 4, 5 ('é' occupies two bytes)
}A .char_indices() head binds a pair and only a pair, and it takes no .enumerate()
and no adapters: it already carries a position. See
Strings for the method surface behind both.
Break and Continue
Use break to exit the nearest loop and continue to skip to the next iteration:
while condition {
if should_stop {
break
}
if should_skip {
continue
}
// normal loop body work
}Break/Continue Requirements
Both statements are only valid inside loops:
func valid() -> i32 {
mut i: i32 = 0
while i < 10 {
i = i + 1
if i == 5 {
break
}
}
return i
}
// Invalid
// func invalid() -> i32 {
// break // Error: break used outside of a loop
// return 0
// }Loop Labels
A for, while, or loop may be prefixed with a label, an identifier
followed by a colon (outer:). break label and continue label then target
the labeled loop rather than the innermost one, so an inner loop can exit or
re-enter an outer loop directly:
func count() -> i32 {
mut total: i32 = 0
outer: for i in 0..5 {
for j in 0..5 {
total = total + 1
if i + j >= 3 {
break outer // exits BOTH loops
}
}
}
return total
}continue label re-enters the labeled loop's next iteration:
outer: for i in 0..3 {
for j in 0..3 {
if j == 1 {
continue outer // skip to the outer loop's next i
}
}
}A label on break / continue must name an enclosing loop; an unknown label
is a compile error (use of undefined loop label).
Examples
Range Check
func in_range(x: i32, min: i32, max: i32) -> bool {
if x >= min && x <= max {
true
} else {
false
}
}Maximum of Three
func max3(a: i32, b: i32, c: i32) -> i32 {
if a >= b && a >= c {
a
} else if b >= c {
b
} else {
c
}
}Grade Calculator
func letter_grade(score: i32) -> i32 {
if score >= 90 {
return 4 // A
} else if score >= 80 {
return 3 // B
} else if score >= 70 {
return 2 // C
} else if score >= 60 {
return 1 // D
} else {
return 0 // F
}
}Best Practices
Prefer Early Returns
// Good: early returns
func process(x: i32) -> i32 {
if x < 0 {
return 0
}
if x > 100 {
return 100
}
return x * 2
}
// Less clear: nested conditions
func process_nested(x: i32) -> i32 {
if x >= 0 {
if x <= 100 {
return x * 2
} else {
return 100
}
} else {
return 0
}
}Simplify Boolean Conditions
// Good: direct boolean return
func is_positive(x: i32) -> bool {
x > 0
}
// Unnecessary: explicit true/false
func is_positive_verbose(x: i32) -> bool {
if x > 0 {
return true
} else {
return false
}
}Pattern Matching
match selects a branch by deconstructing a value, and is itself an expression.
The first arm whose pattern matches (and whose optional if guard holds) runs:
func classify(n: i32) -> i32 {
match n {
0 => 1, // literal
1 | 2 => 2, // or-pattern
3..=9 => 3, // inclusive range
n if n < 0 => 4, // bare binding + guard
_ => 9 // wildcard
}
}An arm body is an expression, and may also be a bare return, break, or
continue: the statement that leaves the enclosing function or loop instead of
producing a value for the arm:
func first_even(limit: i32) -> i32 {
mut i: i32 = 0
loop {
i = i + 1
match i % 2 {
0 => break i,
_ => continue
}
if i > limit { return 0 - 1 }
}
}Enum variants deconstruct and bind their payloads (E::Tuple(a),
E::Struct { field }). A match must be exhaustive. See
Expressions → Match Expressions for the full pattern
grammar, exhaustiveness rules, and current limits.
val-else, Unwrap or Leave the Scope
val PATTERN = value else { ... } binds a refutable pattern and hands the failure to
an else branch. The bindings are live for the rest of the enclosing block, not
just one arm, which is what makes it the straight-line alternative to a match whose
success arm would otherwise swallow the whole function:
func doubled_or_error(raw: i32) -> i32 {
val Result::Ok(value) = parse(raw) else |err| { return err }
value + 1 // `value` is in scope from here on
}The else branch must exit the scope: return, break, continue,
panic(...), or unreachable(). A branch that can fall through is rejected, so the
binding is guaranteed initialized on the path that continues:
error: the `else` branch of a `val-else` can fall through: it must exit the scope with `return`, `break`, `continue`, `panic(...)`, or `unreachable()`The else |binding| form
Writing else |name| after else binds the value the failure carried. This is a
dedicated val-else production, not a closure literal. What name refers to is
decided by the scrutinee's type:
| Scrutinee type | else |name| binds |
|---|---|
Result<T, E> | the Err payload (Result::Err(e) → e: E) |
Option<T> | nothing: None is empty, so only |_| (or a bare else) is accepted |
| any other enum | the original scrutinee, unmodified, for a nested match |
Option and Result have exactly one "other" variant, so the payload to unwrap is
unambiguous. A general enum has several, so |name| hands back the whole value and the
branch discriminates further:
enum Shape { Circle { radius: i32 }, Square(i32), Dot }
func area(s: Shape) -> i32 {
val Shape::Circle { radius } = s else |other| {
match other {
Shape::Square(side) => { return side * side },
_ => { return 0 }
}
}
radius * 3
}Naming an Option's binding is rejected, since there is nothing to name:
error: `else |e|` has nothing to bind: `Option::None` carries no payload; write `else` or `else |_|`Because break counts as leaving the scope, val-else is also the natural drain loop:
loop {
val Option::Some(v) = next(i) else { break }
total = total + v
i = i + 1
}Runnable program: examples/control_flow/val_else.nr.
References
- Expressions: boolean expressions
- Operators: comparison and logical operators
- Variables: variable scope in blocks