Skip to content

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 branchesif c { } else if d { } else { }
repeat while a condition holdswhile c { }
repeat until you breakloop { }
repeat a fixed number of timesrepeat(n) { }
count through a rangefor i in 1..10 { }, for i in 0..<n step 2 { }
visit every elementfor x in items { }, for (i, x) in items { }
leave or continue an outer loopouter@ for … { break@outer }

if, else if, else

prismio
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.

prismio
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.

prismio
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.

prismio
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.

prismio
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.

prismio
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:

prismio
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.

prismio
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:

SyntaxMeaningExample
a..bfrom a to and including b1..5 → 1, 2, 3, 4, 5; 5..1 → 5, 4, 3, 2, 1
a..<bfrom a to but not including b1..<5 → 1, 2, 3, 4; 5..<1 → 5, 4, 3, 2
… step kevery k-th value, in the range's direction1..10 step 2 → 1, 3, 5, 7, 9; 10..0 step 2 → 10, 8, 6, 4, 2, 0
prismio
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. A Vec of length n is indexed 0..<n, which is empty when n is 0. 0..n would visit one past the end.
  • The direction comes from the values. a..b counts down when a is greater than b, 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 is 0..<n when n is at least 1, but when n is 0 it counts down, visiting 0 and then -1, where 0..<n runs no times. The compiler warns about this shape; write 0..<n.
  • Either bound can be any integer expression, of one type. Parentheses are allowed but not required: for i in 1..x + n * 2 and for i in lo + 1..hi both 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.length does not recompute the length every iteration.
  • a..a runs once and a..<a runs no times, in either direction.
  • step is a distance, so it must be positive. The range already says which way to go; step says how far each iteration moves. A literal step 0 or step -2 is 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..2147483647 runs eight times and ends; a range down to the smallest Int ends there too. Neither wraps around.
  • The loop variable is immutable and scoped to the body. It has the type of the bounds, Int unless they are another integer type.
  • A range may count in any integer type. for i in 0..<MATRIX over a Usize bound gives a Usize variable, and U32, U64, I64, U8 and the rest work the same way, with the direction, .. and ..<, step and labels exactly as for Int. A bare literal bound takes the other bound's type; two variables of different types are an error (as converts one). The loop that runs is the Int loop 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:

prismio
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 infor x in … givesfor (a, b) in … gives
Stringeach byte, as a Charindex, Char
Vec<T>each elementindex, element
Slice<T>each element of the viewindex, element
Array<T, N>, [T] with a known lengtheach elementindex, element
Map<K, V>each key, in insertion orderkey, value
a type implementing Iteratoreach next()—
prismio
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() evaluates text.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 write for 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.

prismio
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:

prismio
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:

prismio
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@inner outside an inner@ loop);
  • a label that repeats one already used by an enclosing loop, since break@outer would 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.

prismio
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

MissingUse 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 loopassign into a let mut declared before the loop, then break
for over a [T] parameter, whose length is not knownpass the length and write for i in 0..<n
(key, value) from an Iteratorreturn 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