Control flow
Branch with if and else if, loop with while, loop, repeat and for over ranges and collections, and leave nested loops with labels.
Last verified
Control-flow constructs are statements. A condition is any Bool expression, with or without parentheses, and bodies are blocks. In 0.1, if, loops, and match do not produce values; assign into a mutable binding or return from a branch when a computation must select a result.
| To… | Write |
|---|---|
| choose between branches | if c { } else if d { } else { } |
| repeat while a condition holds | while c { } |
repeat until you break | loop { } |
| repeat a fixed number of times | repeat(n) { } |
| count through a range | for i in 1..10 { }, for i in 0..<n step 2 { } |
| visit every element | for x in items { }, for (i, x) in items { } |
| leave or continue an outer loop | outer@ for … { break@outer } |
if, else if, else
if (ready) {
println("ready")
} else {
println("waiting")
}The condition must have type Bool. Integers, pointers, strings, and optionals are not implicitly interpreted as truth values.
fn main() -> Int {
let count = 1
if (count) { return 0 }
return 1
}Compare explicitly, such as count != 0.
Parentheses are optional, and is and not are words for == and !
A condition ends at the { that opens the body, so it needs no parentheses. if (ready) { } still works: (ready) is an ordinary parenthesised expression.
import std.io
fn main() -> Int {
let mut x = 3
let none_int: Int? = none
if x == 3 { println("three") }
if x is 3 { println("also three") }
if x is not 4 { println("not four") }
if not (x == 4) { println("still not four") }
if none_int == none { println("nothing") }
while x < 6 { x = x + 1 }
println(x)
return 0
}a is b is a == b, a is not b is a != b and not a is !a: the parser reads the words into the same nodes as the symbols, so nothing after it can tell which was written. not binds exactly as ! does, to the next operand only, so not x == y negates x; write not (x == y) to negate the comparison. is and not are reserved words.
is and is not are operators that sit between their two operands, as in Python: x is 3, x is not none. A ( that touches the word reads as a call, which is is not, so x is(3) is an error. A grouped right operand takes a space: x is (y + 2).
One thing a bare condition cannot contain is a struct literal, because the { after a name would read as the body. Parenthesise it: if (Point { x: 1, y: 2 }).x == 1 { }. Inside any brackets, including call arguments, a struct literal is fine.
Chain as many conditions as you need with else if. They are tested in order and the first one that holds runs; the final else is optional. Any branch can hold further if statements.
import std.io
fn grade(score: Int) -> String {
if (score >= 90) {
return "A"
} else if (score >= 80) {
return "B"
} else if (score >= 70) {
if (score >= 75) { return "C+" }
return "C"
} else {
return "F"
}
}
fn main() -> Int {
println(grade(95))
println(grade(77))
println(grade(12))
return 0
}Each branch establishes its own lexical scope. Since if is a statement, syntax such as let sign = if (...) { ... } else { ... } is not supported.
while
while repeats while a Boolean condition is true. The condition is evaluated before every iteration, so a body whose condition begins false never runs.
fn main() -> Int {
let mut sum = 0
let mut value = 1
while (value <= 4) {
sum += value
value += 1
}
return sum - 10
}loop
loop has no condition. It repeats until a break or return, which makes it the loop to use when the test belongs in the middle of the body.
let mut value = 0
loop {
value += 1
if (value >= limit) { break }
}A loop with no break of its own never finishes, and the compiler knows it: it satisfies definite-return analysis, and code after it is rejected as unreachable. A break inside a nested loop leaves that loop, not this one, so it does not count — unless it names this loop, or one outside it, by its label:
import std.io
fn firstAtLeast(xs: [Int], floor: Int) -> Int {
let mut i = 0
loop {
while (i < 4) {
if (xs[i] >= floor) { break } // leaves the while, not the loop
i = i + 1
}
if (i < 4) { return xs[i] }
return -1
}
}
fn main() -> Int {
println(firstAtLeast([3, 8, 1, 9], 5)) // 8
return 0
}repeat
repeat(n) runs its body n times. The count is any Int expression, evaluated once before the first iteration; zero or less runs the body no times.
import std.io
fn main() -> Int {
repeat(3) {
println("Hello")
}
return 0
}A count of zero is an ordinary answer to "how many times", so repeat(0) simply skips the body. That is the difference from step 0 below, which asks a loop to move by nothing and is an error. A repeat count never runs backwards.
repeat has no loop variable. When you need the iteration number, count with a range instead: for i in 0..<n.
repeat is recognised only at the start of a statement followed by a parenthesised count and a block, so it remains an ordinary name everywhere else — "ab".repeat(3) is still the String method.
Ranges: for i in a..b
A range loop visits integers from its start towards its end, counting up or down to get there:
| Syntax | Meaning | Example |
|---|---|---|
a..b | from a to and including b | 1..5 → 1, 2, 3, 4, 5; 5..1 → 5, 4, 3, 2, 1 |
a..<b | from a to but not including b | 1..<5 → 1, 2, 3, 4; 5..<1 → 5, 4, 3, 2 |
… step k | every k-th value, in the range's direction | 1..10 step 2 → 1, 3, 5, 7, 9; 10..0 step 2 → 10, 8, 6, 4, 2, 0 |
import std.io
fn main() -> Int {
for i in 1..5 {
print(i)
}
println("")
let n = 4
for i in 0..<n {
print(i)
}
println("")
for i in 9..0 step 3 {
print(i)
}
println("")
return 0
}This prints 12345, 0123 and 9630.
..<is the one for indices. AVecof lengthnis indexed0..<n, which is empty whennis 0.0..nwould visit one past the end.- The direction comes from the values.
a..bcounts down whenais greater thanb, whether the bounds are literals or computed. A literal range is settled when the program is compiled; a computed one costs one comparison when the loop starts. - Watch
0..n - 1. It is0..<nwhennis at least 1, but whennis 0 it counts down, visiting 0 and then -1, where0..<nruns no times. The compiler warns about this shape; write0..<n. - Either bound can be any integer expression, of one type. Parentheses are allowed but not required:
for i in 1..x + n * 2andfor i in lo + 1..hiboth work, because..binds more loosely than arithmetic. - The start, the end and the step are each evaluated once, in that order, before the first test. Changing a variable used in the end inside the body does not change how many times the loop runs, and
for i in 0..<v.lengthdoes not recompute the length every iteration. a..aruns once anda..<aruns no times, in either direction.stepis a distance, so it must be positive. The range already says which way to go;stepsays how far each iteration moves. A literalstep 0orstep -2is a compile error, and a computed step that turns out zero or negative stops the program with an error naming the line.- Ranges stop at the ends of
Int.for i in 2147483640..2147483647runs eight times and ends; a range down to the smallestIntends there too. Neither wraps around. - The loop variable is immutable and scoped to the body. It has the type of the bounds,
Intunless they are another integer type. - A range may count in any integer type.
for i in 0..<MATRIXover aUsizebound gives aUsizevariable, andU32,U64,I64,U8and the rest work the same way, with the direction,..and..<,stepand labels exactly as forInt. A bare literal bound takes the other bound's type; two variables of different types are an error (asconverts one). The loop that runs is theIntloop over a count, so a range of more than 2,147,483,647 values stops with an error when it is entered, and a range up to the top of its type visits that value and ends without wrapping.
The header may be written in parentheses, which reads naturally with step:
for (x in 1..10 step 2) {
println(x)
}The same two range operators select part of a String, Vec or Slice: s[0..2] is the first three characters and s[0..<2] the first two. See slices.
Collections: for x in items
Without a range operator, for visits the elements of a collection:
After in | for x in … gives | for (a, b) in … gives |
|---|---|---|
String | each byte, as a Char | index, Char |
Vec<T> | each element | index, element |
Slice<T> | each element of the view | index, element |
Array<T, N>, [T] with a known length | each element | index, element |
Map<K, V> | each key, in insertion order | key, value |
a type implementing Iterator | each next() | — |
import std.io
import std.string
import std.vec
import std.map
fn main() -> Int {
let word = "prism"
for (i, c) in word {
if (c == 'i') { println(i) }
}
let scores: Vec<Int> = [70, 85, 92]
let mut total = 0
for score in scores { total += score }
println(total)
let ages: Map<String, Int> = { "ann": 31, "bob": 42 }
for (name, age) in ages {
println(name.concat(" is ", age.toString()))
}
for line in "alpha\nbeta".lines() {
println(line)
}
return 0
}- The collection is borrowed, not moved, so it is still usable after the loop.
- The collection can be any expression.
for line in text.lines()evaluatestext.lines()once, keeps the result for the loop, and releases it when the enclosing block ends. A name or a field (for b in shelf.books) is read in place. - Changing a Vec while iterating it is not an error, but the loop visits the length it had when the loop began.
- An array must have a known length. A
[T]parameter takes arrays of any length and does not know its own, so pass the length beside it and writefor i in 0..<n.
Your own types join in by implementing Iterator from std.iter — hasNext(self) and next(inout self). The iterator is advanced in place, so iterating a name requires a let mut binding; a call that returns an iterator (for x in countdown(5)) needs nothing.
break, continue and labels
break exits the nearest enclosing loop. continue skips the rest of the current iteration and begins the next one; in a range loop that means the next value. Neither carries a value, and both are errors outside a loop.
fn main() -> Int {
break
return 0
}To leave or continue a loop further out, label it by writing a name and @ in front of it, then name the label after the keyword:
import std.io
fn main() -> Int {
outer@ for row in 0..<3 {
for column in 0..<3 {
if (column == 2) { continue@outer }
if (row == 2) { break@outer }
println(row * 10 + column)
}
}
return 0
}This prints 0, 1, 10, 11. Any loop can carry a label — for, while, loop and repeat:
search@ while (pending > 0) {
loop {
if (found) { break@search }
pending -= 1
}
}Everything created inside the loops being left is released on the way out, exactly as for an unlabelled break. The compiler rejects:
- a label that no enclosing loop carries (
break@inneroutside aninner@loop); - a label that repeats one already used by an enclosing loop, since
break@outerwould then silently mean the nearer one; - a label in front of anything that is not a loop.
break@outer must be on the same line as break; a statement ends at the end of its line.
return
return expression exits a value-returning function. A function without a result may use return without an expression. Every reachable path through a value-returning function must return the declared type, or end in panic, unreachable or exit.
fn absolute(value: Int) -> Int {
if (value < 0) { return -value }
return value
}Ownership inside loops
The compiler rejects moving an outer move-only binding from inside a loop when a later iteration could observe it after the move. Borrow it with an ordinary parameter, mutate through inout, or arrange a single ownership transfer outside the repeating region.
This conservative restriction prevents a path that succeeds once and becomes a use-after-move on the next iteration.
Not available yet
| Missing | Use today |
|---|---|
if and match as expressions (let x = if (c) { 1 } else { 2 }) | assign into a let mut in each branch, or return from a helper |
A break that carries a value out of a loop | assign into a let mut declared before the loop, then break |
for over a [T] parameter, whose length is not known | pass the length and write for i in 0..<n |
(key, value) from an Iterator | return a struct from next() and read its fields |
defer, exceptions, and conditional binding syntax (if let) | release explicitly on each path; compare an optional with none and read it with expect |