Control Flow

Conditions require parentheses. There are no truthy values — conditions must be explicitly bool. Use != 0, != null, etc.

If / else

tape
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:

tape
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

tape
var i: i64 = 0;
while (i < 10) {
    io.println("{i}");
    i = i + 1;
}

For loops

Iterate over ranges:

tape
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:

tape
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:

tape
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):

tape
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:

tape
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:

tape
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:

tape
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:

tape
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:

tape
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:

tape
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:

tape
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:

tape
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

tape
while (true) {
    let val = read_next();
    if (val == 0) { break; }
    if (val < 0) { continue; }
    process(val);
}

Return

tape
fn find(items: []i64, target: i64) -> ?i64 {
    for (i in 0..items.len) {
        if (items[i] == target) { return i; }
    }
    return null;
}

Last modified: