Control Flow

Conditionals

Conditions must be bool.

if (score >= 90) {
    io.println("excellent");
} elseif (score >= 60) {
    io.println("pass");
} else {
    io.println("retry");
}

There is no implicit truthiness. Check length, nullability, or equality directly:

if (items.length() > 0 && currentUser != null) {
    io.println("ready");
}

Loops

while (i < 10) {
    i++;
}

for (let int i = 0; i < 3; i++) {
    io.println(i);
}

for (n in [1, 2, 3]) {
    io.println(n);
}

for (key in data.keys()) {
    io.println(key);
}

break exits a loop. continue skips to the next iteration.

Use for-in for collections and generators. Use C-style for loops when the index itself matters:

list<string> names = ["Ada", "Grace", "Linus"];

for (let int i = 0; i < names.length(); i++) {
    io.println("${i}: ${names[i]}");
}

This is the direct equivalent of a basic counter loop in C, JavaScript, PHP, or Go. The loop variable is available in the loop body and can be used as an index. Use this pattern when you need both the current value and its position.

Range literals support an optional by step:

for (i in 0..10 by 2) {    # 0 2 4 6 8 10
    io.print("${i} ");
}

for (i in 10..0 by -2) {   # 10 8 6 4 2 0
    io.print("${i} ");
}

for (i in 0..<10 by 3) {   # 0 3 6 9 (exclusive upper bound)
    io.print("${i} ");
}

The step defaults to 1 when omitted. It can be any integer expression.

For lazy range iteration over large sequences, import collections and use collections.range(start, end, step):

import collections;

for (i in collections.range(0, 5, 1)) {
    io.println(i);
}

for (i in collections.range(10, 0, -2)) {
    io.println(i);
}

collections.range is lazy and suitable for large ranges. Use the by clause on range literals for the common case; use collections.range when you need lazy evaluation or runtime-constructed sequences.

Range methods and properties

Range literals produce first-class values with methods and read-only properties.

let r = 0..10 by 2;

r.length()       # 6
r.isEmpty()      # false
r.contains(4)    # true
r.contains(3)    # false
r.first()        # 0
r.last()         # 10
r.toList()       # [0, 2, 4, 6, 8, 10]

r.start          # 0
r.end            # 10
r.step           # 2

toList() materialises the range into a list. Avoid it for large ranges - iterate with for-in instead.

first() and last() return null for empty ranges (e.g. 5..3).

A range converts to a string via as string or interpolation:

io.println(r as string);         # 0..10 by 2
io.println("range: ${r}");       # range: 0..10 by 2

Destructuring

let [first, second] = pair;
let {name, age} = person;

for ([key, value] in data.items()) {
    io.println(key);
}

Destructuring is best for small, well-known shapes. For request bodies, decoded JSON, or optional fields, check membership first so errors are clearer:

if (person.hasKey("name")) {
    let name = person["name"];
    io.println(name);
}

Match

match dispatches on a value, comparing it against a sequence of case patterns. It works as either a statement or an expression depending on context.

Match expression

When assigned to a variable or used inside a larger expression, match produces a value. Every branch must end with a semicolon and produce a value. A trailing ; after the closing } marks it as an expression statement:

let label = match (status) {
    case 200 => "ok";
    case 404 => "missing";
    default  => "error";
};

The entire match (status) { ... } evaluates to the value of the matched branch. Use default (or case _) to ensure every possible input is covered; a MatchError is thrown if no case matches and there is no default.

Match expressions can appear anywhere a value is expected:

io.println(match (x % 2) {
    case 0 => "even";
    default => "odd";
});

Match statement

When match appears as a top-level statement - not assigned or used as a value - each branch executes an action and there is no trailing ; after }:

match (command) {
    case "serve"   => startServer();
    case "migrate" => runMigrations();
    default        => showHelp();
}

The distinction is syntactic: expression match is terminated by ; after } and the whole expression has a type; statement match is not and produces no value.

Multi-statement branches

Use a block body { ... } when a branch needs more than one statement:

match (status) {
    case "ok" => {
        io.println("success");
        return true;
    }
    default => {
        io.println("failed");
        return false;
    }
}

Guard clauses

A when guard filters a case with an additional boolean condition:

match (score) {
    case int n when n >= 90 => io.println("A");
    case int n when n >= 70 => io.println("B");
    default                 => io.println("C");
}

Pattern matching with types

case can match by type, binding the value to a name if it matches:

match (value) {
    case int n    => io.println("int: " + (n as string));
    case string s => io.println("string: " + s);
    case null     => io.println("null");
    default       => io.println("other");
}

Enum payload destructuring

Enum variants with associated values can be destructured in case:

enum Result { Ok(string), Err(string) }

let text = match (result) {
    case Result.Ok(string value)   => value;
    case Result.Err(string message) => "error: " + message;
};

Defer

defer registers a call to run when the surrounding function or top-level script exits. Deferred calls run in last-in, first-out order.

func run(): void {
    defer io.println("done");
    io.println("working");
}

Arguments to deferred calls are evaluated when the defer statement is executed.

defer is especially useful with files, sockets, database transactions, and locks:

func writeLocked(any file, string text): void {
    io.lock(file);
    defer io.unlock(file);
    io.writeln(file, text);
}