Control Flow
Conditions require parentheses. There are no truthy values — conditions must be explicitly bool. Use != 0, != null, etc.
If / else
if (x > 0) {
io.println("positive");
} else if (x == 0) {
io.println("zero");
} else {
io.println("negative");
}if is a statement: it gives no value, so let x = if (c) { 1 } else { 2 };
is a compile error. Assign a var in both branches, or use @select, which
evaluates both values and never branches:
var x: i64;
if (c) { x = 1; } else { x = 2; }
let y: i64 = @select(c, 1, 2);(@tape code is the exception: the compile-time evaluator reads
if (c) { a } else { b } as a value.)
While loops
var i: i64 = 0;
while (i < 10) {
io.println("{i}");
i = i + 1;
}For loops
Iterate over ranges:
for (i in 0..10) {
io.println("{i}");
}
for (i in 0..=9) {
io.println("{i}"); // inclusive: 0 through 9
}Iterate over slices or arrays:
for (item in items) {
process(item);
}A pointer to an array (*[N]T) iterates the array it points at.
An array literal can be iterated in place. The loop owns the array: it lives until the loop ends and is then freed, strings included. The element type is the first element’s:
for (name in ["ada", "grace", "linus"]) {
io.println("{name} has {name.len} letters");
}for (x in []) is a compile error, because an empty literal has no
element type.
Iterate over a string’s bytes (each a u8):
fn count_spaces(s: string) -> i64 {
var n = 0;
for (b in s) {
if (b == 32) { n = n + 1; }
}
return n;
}A string or slice is read again each time round: if the body assigns the one being iterated, the loop goes on over the new value.
Iterate over an integer count:
for (i in count) {
io.println("{i}"); // 0 to count-1
}Iterator protocol
Any struct with a fn Type.next(*self) -> ?T method can be used with for:
struct Counter { current: i64; end: i64; }
fn Counter.next(*self) -> ?i64 {
if (self.current >= self.end) { return null; }
let val = self.current;
self.current = self.current + 1;
return val;
}
var c = Counter { current: 0, end: 5 };
for (val in c) {
io.println("{val}");
}for calls next on the iterator itself, so the loop advances c: after
a break, a second loop over c continues where the first stopped. An
iterator held in a let can’t be advanced, so for over one is a compile
error; an iterator a call returns (for (x in v.iter())) is the loop’s
own. When the collection is itself a temporary (for (x in load().iter())),
or a part of one (for (x in v.get(0).items.slice()), a field of the copy
get returns), or a temporary is passed to the call that makes the iterator
(for (w in str.split(names(), " ")), whose words are views into the
string names() returns), it lives until the loop ends, as if a let
before the loop held it.
The loop variable is read-only: over an array or slice it is the element
itself, so change an element through its index (a[i].x = 0).
Match
Tagged unions
Pattern matching on tagged unions uses Type.Variant syntax with field destructuring:
match (shape) {
Shape.Circle { radius } => { return 3.14 * radius * radius; }
Shape.Rect { w; h } => { return w * h; }
Shape.Point => { return 0.0; }
}Bindings destructure the variant’s fields by name. Use .. to ignore remaining fields.
Enums
Match also works with enums — the scrutinee is compared directly against each variant:
enum Direction : u8 { North = 0; East = 1; South = 2; West = 3; }
match (dir) {
Direction.North => { move_forward(); }
Direction.East => { turn_right(); }
Direction.South => { turn_around(); }
Direction.West => { turn_left(); }
}Wildcard
Use _ as a catch-all arm for unmatched values:
match (dir) {
Direction.North => { move_forward(); }
_ => { io.println("not going north"); }
}Match must be exhaustive — either all variants are covered, or a _ wildcard is present.
_ catches what no other arm lists, wherever it is written.
Several values in one arm
An arm can list several patterns, separated by commas; it runs when the value equals any of them:
match (dir) {
Direction.North, Direction.South => { io.println("vertical"); }
Direction.East, Direction.West => { io.println("horizontal"); }
}On a tagged union, an arm that lists several variants binds nothing, so it lists
only variants without fields (Shape.Point, Shape.Empty =>); a variant with
fields gets an arm of its own. Each value or variant may be listed once in a
match: listing one twice is an error that names both lines.
Integers
Match on any integer type with constant arms: literals (negative ones too),
@byte("x") and @codepoint("x") for characters, and consts. An integer has
too many values to list, so a _ arm is required:
const TAB: u8 = 9;
fn kind(c: u8) -> string {
match (c) {
@byte("0"), @byte("1") => { return "bit"; }
@byte(" "), TAB => { return "space"; }
0 => { return "nul"; }
_ => { return "other"; }
}
}An arm must fit the value’s type (300 on a u8 is an error: it could never
match), and a variable is not an arm — compare with if for that.
Strings and byte views
Match a string or a []const u8 against string literals. Arms compare exact
bytes — never case-folded, never a prefix — and _ is required:
fn apply(name: []const u8) -> i64 {
match (name) {
"fill" => { return 1; }
"stroke", "stroke-width" => { return 2; }
"" => { return 0; }
_ => { return -1; }
}
}The value is looked at, not copied; a string made for the match
(match (read_name())) is freed when the match ends, however it is left.
Enum or match?
A set of names your program declares (states, node kinds, format fields) is an
enum: it gets .name(), descriptions, parsing from text with
words.gen_lookup, and a match without _. Input — a byte of a string, a word
read from a file or the network — is matched directly on the integer or the
text. Case-insensitive words (HTTP header names) are folded and looked up, not
matched: arms never ignore case.
Break and continue
while (true) {
let val = read_next();
if (val == 0) { break; }
if (val < 0) { continue; }
process(val);
}Return
fn find(items: []i64, target: i64) -> ?i64 {
for (i in 0..items.len) {
if (items[i] == target) { return i; }
}
return null;
}Last modified: