Control flow

for

The for-loop iterates over each element in the range expression. The range may be anything that has an iterator() method that returns an iterator.

void main() {
    println("the range can be any collection");
    for element in [1, 2, 3] {
        println(element);
    }

    println("or an exclusive numeric range");
    for element in 1..3 {
        println(element);
    }

    println("or an inclusive numeric range");
    for element in 1...3 {
        println(element);
    }
}

Bind a second loop variable to also get the zero-based iteration index.

Loop index semantics

The index is an int that counts iterations from 0 even for ranges that do not start at 0, and each iteration gets a fresh copy, so assigning to it does not affect iteration.

void main() {
    for element, index in ["a", "b", "c"] {
        print(index);
        println(element); // prints 0a 1b 2c
    }
}

while

The while-loop loops until its condition evaluates to false.

void main() {
    int i = 0;
    while i < 3 {
        println(i); // prints: 0 1 2
        i++;
    }
}

break and continue

break exits the innermost enclosing loop or switch statement, and continue skips to the next iteration of the innermost enclosing loop:

void main() {
    for i in 0..10 {
        if i == 3 {
            continue; // skips 3
        }
        if i == 5 {
            break; // stops the loop
        }
        println(i); // prints: 0 1 2 4
    }
}
break and continue in switch

Since switch cases don't fall through, break is only needed in a switch to skip the rest of the matched case body. A continue inside a switch applies to the enclosing loop.

if

void main() {
    var answer = 42;

    if answer == 0 {
        println("answer is zero");
    } else if answer < 0 {
        println("answer is negative");
    } else {
        println("answer is ", answer);
    }

    // If-expression, aka conditional expression, aka ternary expression.
    println("answer is ", answer == 42 ? "right" : "wrong");
}

An arm that diverges (calls a never-returning function like abort) contributes no value: the ternary takes the other arm's type.

The is operator tests whether an enum value is a specific case. Appending a name binds the case's payload in the then-branch, using the same semantics as switch case bindings. The binding borrows the payload rather than copying it, so the payload cannot be moved out of the binding:

enum Outcome {
    Ok(int value),
    Err(string error),
}

string describe(Outcome& outcome) {
    if *outcome is Err e {
        return e;
    }
    if *outcome is Ok {
        return "ok";
    }
    return "unknown";
}

void main() {
    var ok = Outcome.Ok(value = 1);
    println(describe(ok)); // prints "ok"
}

switch

Case bodies don't fall through to the next by default, so you don't have to manually break after each case. Variables can be declared directly in a case body without wrapping it in a block.

void main() {
    var value = 0;

    switch (value) {
        case 0:
            println("value is zero");
        case 1:
            println("value is one");
        default:
            println("value is something else");
    }
}
Exhaustive-switch safety check

When switching over an enum without a default, the compiler inserts a safety check that aborts with an error if the value is somehow not one of the enum's cases. Valid but unhandled cases still fall through to the code after the switch. The check is emitted in every build mode except release mode (--release), where the compiler instead assumes the value is always a valid case.

A switch can also be used as an expression, in which case each arm is a single expression and the whole switch evaluates to the matched arm's value. Unlike statements, switch expressions must handle every case (or have a default), and the arms must produce a value:

enum Outcome {
    Ok(int value),
    Err(string error),
}

string describe(Outcome& outcome) {
    return switch *outcome {
        case Ok: "ok",
        case Err error: error,
    };
}

void main() {
    var ok = Outcome.Ok(value = 1);
    println(describe(ok)); // prints "ok"
}

When a case binds no name, the switched variable narrows to the matched case's payload inside that arm (a payloadless case narrows to ()), so its fields are available directly. The same narrowing applies to == and != comparisons against a case in if and while conditions:

enum Event {
    Click(int x, int y),
    Quit,
}

void handle(Event event) {
    switch event {
        case Click:
            println("fire at ", event.x, ", ", event.y);
        case Quit:
            println("bye");
    }

    if event == Click {
        println("fire at ", event.x, ", ", event.y);
    }
}

void main() {
    handle(Event.Click(x = 3, y = 4));
}

defer

defer runs a statement when the current scope exits, for cleanup before returning. See Defer and destructors for details.

Planned features