Statements
Expression Statements
Section titled “Expression Statements”Any expression can be used as a statement:
do do_something() { println("done") }
do main() { mut counter i64 = 0 println("Hello") counter++ do_something()}Conditional Statements
Section titled “Conditional Statements”If Statements
Section titled “If Statements”do main() { mut x i64 = 0 if x < 0 { println("negative") } or x == 0 { println("zero") } otherwise { println("positive") }}The or keyword introduces additional conditions.
The otherwise keyword introduces the default case.
elif is an alias for or, and else is an alias for otherwise. Both dialects are valid, user’s
choice — but the two branch keywords move together, so an elif chain must close with else and an
or chain must close with otherwise. See Keywords.
do main() { mut x i64 = 0 if x < 0 { println("negative") } elif x == 0 { println("zero") } else { println("positive") }}Loop Statements
Section titled “Loop Statements”For Loops
Section titled “For Loops”do main() {for i in range(0, 10) { println("${i}")}
for (i in range(0, 10)) { // Parentheses optional}}Use the blank identifier _ to iterate by count without needing the loop variable:
do main() {for _ in range(0, 5) { // body runs 5 times; loop counter is discarded}}_ cannot be read inside the loop body. If you need the counter value, use a named variable instead.
For-Each Loops
Section titled “For-Each Loops”do main() {mut items [string] = {"a", "b", "c"}for_each item in items { println(item)}}An optional index variable can precede the value variable, separated by a comma:
do main() { mut items [string] = {"a", "b", "c"} for_each i, item in items { println("${i}: ${item}") } // Output: 0: a, 1: b, 2: c}The index variable is always of type i64 and is zero-based. It works with both arrays and strings:
do main() {for_each i, ch in "hello" { println("${i}: ${ch}")}}The blank identifier _ can be used in either position:
do main() { mut items [string] = {"a", "b", "c"} for_each _, item in items { println(item) } // discard index (same as no index) for_each i, _ in items { println(i) } // index only, discard value}Map iteration is also supported. With two variables, the first is the key and the second is the value:
// Single variable iterates keys only
do main() {mut ages map[string:i64] = {"alice": 30, "bob": 25}for_each k, v in ages { println("${k}: ${v}")}for_each key in ages { println(key)}}Map iteration order is undefined (maps are unordered).
Mutation during iteration:
- Arrays: The loop length is captured when
for_eachbegins. Appending to the array during iteration is safe; new elements are added to the array but are not visited by the current loop. The full array (including appended elements) is available after the loop ends. Operations that shift or drop existing elements —remove_at,insert_at,clear,sort, and element assignment — are not allowed during iteration and will panic at runtime. - Maps: Modifying a map during
for_each(inserting or deleting keys) is not allowed and will panic at runtime. Read the map freely, but do not mutate it until the loop completes.
A for-each loop can iterate over an inline array literal directly, with no variable needed:
do main() { for_each item in {"a", "b", "c"} { println(item) }}This works with any element type and supports the index form (for_each i, item in {...}) as well.
While Loops
Section titled “While Loops”while is an alias for as_long_as. Both are valid, user’s choice.
do main() { mut count i64 = 0 as_long_as count < 10 { count++ }
// Equivalent, using while (don't mix with as_long_as in one file): // while count < 10 { // count++ // } println(count)}Infinite Loops
Section titled “Infinite Loops”The loop statement creates an infinite loop that runs until explicitly terminated with break or return:
do main() { loop { mut line string = input() if line == "quit" { break } println("You said: ${line}") }}Control Flow Statements
Section titled “Control Flow Statements”The break statement terminates the innermost enclosing loop:
do main() {for i in range(0, 100) { if i == 5 { break }}}Continue
Section titled “Continue”The continue statement skips to the next iteration of the innermost enclosing loop:
do main() {for i in range(0, 10) { if i % 2 == 0 { continue } println("${i}") // Prints odd numbers only}}Return
Section titled “Return”The return statement exits the current function, optionally returning a value:
do add(a i64, b i64) -> i64 { return a + b}
do greet() { println("Hello") return // Void return}
do main() {
}When Statements (Pattern Matching)
Section titled “When Statements (Pattern Matching)”do main() { mut x i64 = 5 when x { is 1 { println("one") } is 2, 3 { println("two or three") } is range(4, 10) { println("four to nine") } default { println("other") } }}switch is an alias for when and case is an alias for is. The two words move together: a
switch must use case branches and a when must use is branches. default is spelled the same
either way. See Keywords.
do main() { mut x i64 = 2 switch x { case 1 { println("one") } case 2, 3 { println("two or three") } default { println("other") } }}Allowed condition types: integer and float types, string, char, bool, and enum types. Float conditions emit a warning about imprecision. Collection types (arrays, maps) are not allowed.
Strict mode requires all possible values to be handled:
const Direction enum { NORTH EAST SOUTH WEST}
do main() { mut direction Direction = .NORTH #strict when direction { is Direction.NORTH { println("north") } is Direction.EAST { println("east") } is Direction.SOUTH { println("south") } is Direction.WEST { println("west") } }}When a when statement matches on enum values (i.e. one or more is branches use EnumName.VARIANT patterns) and has no default branch, the compiler warns if #strict is not present. This warns that exhaustiveness is not being checked. The fix is to either add #strict to enforce exhaustive coverage or add a default branch. This applies at any nesting depth.
An empty default {} branch emits a warning. Unmatched values are silently ignored, which is almost never intentional. Handle the case or drop the default branch and use #strict.
Ensure Statement
Section titled “Ensure Statement”The ensure statement specifies a function to call when the enclosing function exits (whether normally or via early return):
do cleanup() { println("cleanup") }
do process_file() { ensure cleanup() // ... do work ... if true { return // cleanup() will be called } // cleanup() will be called when function ends}
do main() { process_file()}defer is an alias for ensure. Both are valid, user’s choice.
Or-Return Statement
Section titled “Or-Return Statement”The or_return keyword provides error propagation shorthand for a call whose return tuple ends in Error — (T, Error) or (T, U, ..., Error):
import @ioimport @json
do two() -> (i64, i64, Error) { return 1, 2, nil }do do_work() -> (i64, Error) { return 1, nil }
do load() -> (string, Error) { // Bare or_return: propagates the error with zero values mut content = io.read_file("data.txt") or_return return content, nil}
// With custom fallback values:do load_with_fallback() -> (string, Error) { mut content = io.read_file("data.txt") or_return "", error(.Unknown, "failed to load") return content, nil}
// Destructuring a call that returns more than one non-error value:do consume() -> (i64, Error) { mut a, b = two() or_return // two() -> (i64, i64, Error); a, b bound, error propagated return a + b, nil}
// No binding — run the call only for its error:do run() -> Error { do_work() or_return // propagate on error, otherwise discard the values return nil}
do main() {}When the call returns a non-nil error, or_return immediately returns from the enclosing function. Without explicit fallback values it returns zero values for the enclosing function’s non-error slots plus the original error. A destructuring or_return binds every non-error slot the call returns; a statement with no binding discards them. Both the call and the enclosing function must have Error as their last return type.