Skip to main content

Statements

Every statement form and its rules. Statements appear inside function, tree, method and branch { } bodies.

object Enemy { float x; float vx; int health; }

fn spawn_wave(array<Enemy> enemies) {
repeat (3) { enemies.push(Enemy { x: 0.0, vx: 40.0, health: 10 }); }
}

fn update_enemies(array<Enemy> enemies, float dt) {
int alive = 0;
for (Enemy e in enemies) {
if (e.health <= 0) { continue; }
e.x = e.x + e.vx * dt;
alive++;
}
if (alive == 0) {
spawn_wave(enemies)?;
}
}

Every statement ends with ; or is a { } block. A lone ; (empty statement) is an error; write { }.

Blocks​

{ ... } groups statements and starts a new scope. At the start of a statement, { always begins a block, never a map literal.

Variable declarations​

int count = 0;
Enemy? target;

A statement that starts with a type and a name declares a local; see Local variables.

Expression statements​

Any expression followed by ; is a statement, and its value is thrown away. A call that can fail still needs handling: file_remove(path)?; or file_remove(path) !! { ... };.

Compound assignment​

score += 10;
health -= damage;
speed *= 0.5;
names[i] += " the Brave";
hits[slot] += 1;
  • x op= v stores x op v in x, for +=, -=, *=, /= and %=. The operator works as it does on its own: int32 wraps, float32 stays single precision, and string += string or string += char appends text.
  • The result must fit the target without a conversion, as in a plain assignment. byte b; b += 1; is an error because b + 1 is an int; write b = byte(b + 1);. The same holds for char.
  • The target can be a variable, a writable field, or an element of an array, slice or map. A range such as a[0:2] is not a target.
  • The target is evaluated once. In boxes[next()].count += 1, next() runs once, and that one box is both read and written. The target's parts run before the value on the right.
  • Reading a map element can fail when the key is missing, so m[k] += 1 must be handled, as in (m[k] += 1)?;, unless it is inside if (m ?? k).
  • Like =, it is an expression whose value is the stored value, and it groups right to left: a = b += 1 adds to b, then assigns b to a.

Increment and decrement​

i++;
grid[x][y]--;
  • x++ means x += 1; x-- means x -= 1. The target can be a variable, field or element, and is evaluated once: a[f()]++ calls f once.
  • They are statements only. int j = i++;, f(i++) and prefix ++i are syntax errors. They are also allowed in a for header.

Copy statement​

source => destination; copies elements from one array, slice or buffer into another.

source => destination;
packet[0:8] => header;
input => output[4:];
dst[0:4] = src[4:8]; // same copy, destination first
  • Both sides are an array<T>, slice<T> or buffer with the same element type (a buffer holds bytes).
  • The lengths must match, or the task traps.
  • Overlapping source and destination copy correctly.

if​

if (hp <= 0) { println("defeated"); } else if (hp < 20) { println("low health"); } else { println("ok"); }
  • The condition must be bool. Numbers and references are not true or false on their own: write count != 0, p != null.
  • else belongs to the nearest if without one.
  • Map check: inside if (m ?? k) { ... } (or if (m ?? k && ...)), reading m[k] cannot fail, so it needs no ?. This works when m is a local or parameter and k is a local, parameter or literal, until either is reassigned inside the block. The else branch doesn't get this.

switch​

switch (key) {
'w', 'k' => { move(0, -1)?; }
's', 'j' => { move(0, 1)?; }
'q' => return;
_ => { beep(); }
}

switch (shape) {
Circle c => { draw_circle(c.r)?; }
Square s => { draw_square(s.side)?; }
_ => { }
}

switch compares one value against each arm's patterns in order and runs the first arm that matches. The value is worked out once. At most one arm runs; there is no fallthrough and no break out of a switch.

  • Value switch. The value is an int, int32, byte, char, bool, string or enum. Each pattern is a constant: a number, char, string or bool literal, an enum member (Direction.North), or a const. An arm may list several patterns, separated by commas. A pattern's type must convert to the value's type without an explicit conversion, so an enum value only matches its own members and an int never matches an enum member (switch pattern does not match the switch value type 'E'). Other expressions are rejected (a switch pattern must be a literal, an enum member or a constant).
  • Type switch. The value is an object, interface or engine object (nullable or not). Each pattern is Type name: the arm runs when the value's actual type is Type, with name holding it as a Type inside the arm. The rules of is apply, so a Type the value can never be is an error. Write Type _ to test without a name. A type pattern is its arm's only pattern. A null value matches no type pattern.
  • _ matches anything. It is the arm's only pattern and must be the last arm ('_' must be the last switch arm).
  • A pattern may appear only once (duplicate switch pattern).
  • A switch over an enum without _ must list every member; the error names the missing ones (switch over 'E' does not cover Q, R; add them or a '_' arm). Other switches without _ do nothing when no arm matches.
  • An arm's body is a block or a single statement. Variables declared in an arm, including a type pattern's name, exist only in that arm.
  • break and continue in an arm apply to the enclosing loop. return and throw leave the function as usual.
  • A value that can fail must be handled with ? or !!.

For the expression form, see Expressions.

while​

while (fuel > 0.0) { fuel = fuel - burn_rate; }

The condition must be bool and is checked before each pass.

repeat​

repeat (3) { println("fire!"); }

Runs the body a fixed number of times. The count is an int, worked out once before the first pass; zero or negative runs it zero times.

Counted for​

for (int i = 0; i < n; i++) { ... }
for (i = 0; i < n; i++) { ... }
for (; jobs.length() > 0; ) { jobs.pop()?; }
for (;;) { ... }
for { ... } // loops forever
  • Every part is optional, and so are the parentheses.
  • A variable declared in the header exists only inside the loop.
  • A missing condition means "forever". A condition must be bool.
  • continue still runs the step (i++).

for ... in​

for (int score in scores) { total = total + score; }
for (int i, int score in scores) { ranked[i] = score; }
for (view<Motion, Integrate> entity in rows) { ... }
for (string name in ammo) { ... }
for (string name, int count in ammo) { ... }
for (char c in title) { ... }
  • Loops over an array<T>, slice<T> or rows<Q, A>, in index order, over a map<K, V>, or over a string's characters. Channels and buffers cannot be looped over (for requires an array, slice, map, or string value).
  • With two variables, a sequence gives the index (an int) and the element; a map gives the key and the value. With one variable, a map gives its keys.
  • A string gives each Unicode code point as a char, and with two variables its index counted in characters. These are the same characters as s.chars(): bytes that are not valid UTF-8 read as U+FFFD, one per byte.
  • Each variable's type must accept what it receives (for (float f in ints) works).
  • For a sequence, the length is read once before the loop. Elements you push during the loop are not visited; removing elements during the loop can make it index past the end and trap. Assigning the index variable does not change which element comes next.
  • A map is visited in an unspecified order. Assigning to a key the loop has reached is fine, but adding a new key to the map during the loop traps (trap code -11). The loop keeps iterating the map it started with even if the body assigns the variable it came from. A null map is visited zero times, like a null array.
  • For numbers, the loop variable is a copy. For objects, it is the same object as the element, so changing its fields changes the element.

select​

select waits for the first of several channels to have a value.

select {
int button = clicks.receive() { println("clicked button " + int_to_string(button)); }
string key = keys.receive() { println("pressed " + key); }
else { println("no input this frame"); }
}
  • Each arm is Type name = channel.receive() { ... }. Anything other than a receive() call is a select arm must receive from a channel. The variable exists only in that arm's block.
  • At least one arm is required. else, if present, comes last.
  • Without else, the select waits, and it can fail if a channel is closed, so it must be in a function that can fail (or inside a !! guard). With else, it never waits or fails and can be used anywhere.
  • select { ... } unless ch; also gives up when the channel ch fires first, and then fails with -305. It needs the same failing context. unless after an else block is a select with an else arm never waits; it cannot take 'unless'.

How arms are chosen: select in Concurrency.

Guard blocks​

Guards attach error handlers (!!) and cleanup (&&) to a statement or a block.

file_remove("cache.bin") !! { println("no cache to remove"); };

{
string text = file_read_text("settings.txt")?;
println("volume: " + int_to_string(int_from_string(text)?));
} !! {
println("load failed: " + err.message);
};

Mutex lock = mutex_create()?;
lock.lock()? && { lock.unlock(); };
FormMeaning
call() !! { ... };If the call fails, run the handler; err holds the error.
{ ... } !! { ... };If anything in the block fails, run the handler.
expr && { ... };Run the block when the enclosing block exits.
{ ... } && { ... };Run the second block when the first one exits.
  • && is a guard only when { follows directly; otherwise it is logical AND.
  • Guards chain left to right, each wrapping everything to its left: {A} && {D} !! {H}; runs D before H; {A} !! {H} && {D}; runs H before D.
  • A guarded statement ends with ;.

Full rules: Errors.

break and continue​

  • Only inside while, repeat, for and for ... in (break is only valid inside a loop). They apply to the innermost loop; there are no labels. A switch is not a loop: break in a switch arm leaves the loop around it.
  • Pending && cleanup blocks of the scopes they leave run first.
  • They cannot be used inside an && cleanup block.
  • They cannot jump out of a branch { } block, because the block runs as a separate task (break cannot leave a branch block). Loops inside the block can use them normally.

return​

return;
return value;
  • The value must match the function's result type; return; is only for void functions.
  • In a branch { } block, return finishes the task. The block's task type comes from its return statements; a block that returns no value is task<void>.
  • Pending && cleanup runs before returning. return cannot be used inside a cleanup block.

Reaching the end of a function​

A function, method or tree with a result, and a branch { } block that returns a value, must not be able to reach its closing brace. The compiler checks this with a few simple rules. A statement always leaves when it is:

  • return or throw;
  • a block containing a statement that always leaves;
  • an if with an else where both branches always leave (an if without else never does);
  • a while (true) or a for with no condition (or a literal true one), as long as no break in it targets that loop;
  • a switch statement whose arms cover every value (it has a _ arm, or lists every enum member or both true and false) and where every arm always leaves;
  • a select where every arm and the else block always leave. Without else, a select that runs no arm fails, which also leaves; the same holds for select { ... } unless ch;;
  • a guarded block { ... } && { ... }; whose guarded block always leaves, or { ... } !! { ... }; where both the guarded block and the handler always leave.

Nothing else counts: other loops, ?, and conditions that merely happen to be constant all may complete. If the body of a function with a result can complete, the check fails:

fn sign(int x) -> int {
if (x < 0) { return -1; }
if (x > 0) { return 1; }
} // function 'sign' can reach its end without returning a value

Add the missing return, or turn the last if into an if/else. A branch { } block reports branch block can reach its end without returning a value. Code after a statement that always leaves is allowed. void functions, methods and trees may reach their end.

throw​

throw new error(3, "bad count");
throw err; // rethrow inside a handler

The value must be an error. throw is allowed in functions that can fail, in branch { } blocks, and inside !! guards. See Errors.

Single-statement bodies​

The body of if, else, while, repeat and for, and a switch arm, may be one statement without braces: if (done) return;.

See also​