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);
}