Control Flow
if, while, loop, for, match, defer, with, select, return, assert, and tests.
Zynx control flow is block-oriented. The everyday forms are if, while,
loop, for, and match. Cleanup uses defer and with. Async channel
coordination uses select.
import std.os;
fn main() {
let x = 10;
if x > 5 {
_ = os.write("if: pass\n");
} else {
_ = os.write("if: fail\n");
}
}
Use else if for chained branches. For a compact value choice, use the ternary
operator. See Operators for ternary
precedence and associativity.
let label = count == 0 ? "empty" : "ready";
if and while conditions may also bind one value with name := expression.
The binding is the whole condition expression, the assigned value is tested by
the normal condition rules, and the name lives in the surrounding block scope.
fn next_value() -> i32? {
return 7;
}
fn main() {
if value := next_value() {
assert value == 7;
}
}
while condition bindings are currently limited to copyable values, so each
iteration can overwrite the previous condition binding without moving an owner.
while repeats while its condition is true.
import std.os;
fn main() {
let i = 0;
_ = os.write("while: ");
while i < 3 {
_ = os.write("{i} ");
i += 1;
}
_ = os.write("\n");
}
Use loop for an intentional infinite loop. Exit with break; move to the
next iteration with continue.
import std.os;
fn main() {
let i = 0;
loop {
i += 1;
if i == 1 {
continue;
}
_ = os.write("loop checked\n");
break;
}
}
The compiler warns for while true and constant-true while conditions; prefer
loop { ... } for that shape.
for iterates ranges, arrays, slices, strings, and types that implement the
current iterable protocol.
import std.os;
fn main() {
_ = os.write("exclusive: ");
for j in 0..<3 {
_ = os.write("{j} ");
}
_ = os.write("\n");
_ = os.write("inclusive: ");
for j in 1...3 {
_ = os.write("{j} ");
}
_ = os.write("\n");
}
..< is exclusive at the end. ... is inclusive at the end.
For ranges and iterable types, for index, value in collection binds the
zero-based iteration index as usize and the current value:
for index, value in 0..<3 {
_ = os.write("{index}: {value}\n");
}
match can be used as an expression or as a standalone statement. Expression
matches are useful when all arms produce one value.
fn score(name: str) -> i32 {
return match name {
"fast" => 100,
"steady" => { return 80; },
_ => 0,
};
}
fn main() {
assert score("fast") == 100;
assert score("steady") == 80;
assert score("other") == 0;
}
Expression arms can be simple expressions. Block arms that produce a match
value use return value; inside the arm block. In a match arm block, that
return yields the arm value instead of returning from the enclosing function.
Statement-form match may be written with or without a trailing semicolon
after the closing brace:
import std.os;
fn main() {
let x = 2;
match x {
1 => _ = os.write("one\n"),
2 => _ = os.write("two\n"),
_ => _ = os.write("other\n"),
}
}
Non-enum matches need a _ wildcard today. Enum matches can use contextual
variant patterns such as .Some(value).
defer registers cleanup code for the current scope. Deferred actions run in
last-in, first-out order when the scope exits, including return, break, and
continue.
import std.os;
fn main() {
defer _ = os.write("last\n");
defer _ = os.write("first\n");
_ = os.write("body\n");
}
Use defer expr; for one expression or defer { ... } for a block.
return, break, and continue are not allowed inside defer. In async fn,
await is not allowed inside defer, and a live defer may not cross an
await or select suspension point. For a scope that also has live owning
values, registered defers run before automatic owner cleanup for that scope.
with creates a scoped resource binding. When the block exits, the resource is
dropped.
import std.os;
struct Resource {
id: i32,
drop(self) {
_ = os.write("drop {self.id}\n");
}
}
fn make_resource(id: i32) -> Resource {
return Resource { id };
}
fn main() {
with make_resource(1) as resource {
_ = os.write("inside {resource.id}\n");
}
}
with is used by scoped APIs such as future.Group<T, E>.
See RAII and Drop for the ownership
rules behind drop(self), defer, with, and manual std.drop.
select is valid only inside async fn. It waits on std.channel send and
receive operations.
import std.channel;
import std.os;
async fn main() {
let (tx, rx) = channel.bounded<i32>(1);
await tx.send(7);
select {
case msg = rx.recv() {
match msg {
.Value(value) => _ = os.write("recv {value}\n"),
.Closed => _ = os.write("closed\n"),
}
}
else {
_ = os.write("not ready\n");
}
}
}
Receive cases use case name = receiver.recv(). Send cases use
case sender.send(value). else must be the last case.
return exits the current function with an optional value in ordinary blocks.
assert is a keyword statement that checks a boolean condition. It is not a
function call: write assert value > 0;, not assert(value > 0);.
fn require_positive(value: i32) -> i32 {
assert value > 0;
return value;
}
Parentheses are still valid when they are part of a larger condition:
error MaybeError {
Missing
}
fn maybe_value() throws(MaybeError) -> i32? {
return null;
}
fn main() {
assert (try maybe_value()) == null;
}
Assert messages are allowed inside test blocks.
They are also valid in ordinary code when the extra diagnostic text is useful.
test "numeric default" {
assert 1 + 1 == 2, "math should work";
}
Tests are run through zynx test <source.zx>.