Flash tutorial · 8 / 12
8. Control Flow & Loops
Flash has a clean set of control structures, matching Go's syntax structure with Zig's powerful optional unwrapping and compile-time unrolling.
1. Conditionals (if / else)
In Flash, braces {} are mandatory, even for single-statement blocks. Parentheses around the condition are optional.
if value > 100 {
log("High")
} else {
log("Low")
}
Optional Unwrapping in if
If an expression returns an optional type (?T), you can unwrap it in an if block using the vertical pipe syntax |value|. The block only executes if the value is not null.
// If lookup returns a user record, bind it to 'user' and print
if lookupByUid(uid) |user| {
print(user.name)
} else {
print("User not found")
}
Value-form if Expressions
if can also be used as an expression (resembling a ternary operator in other languages) to return values. In this form, the else branch is mandatory.
// Parens around the condition are required here to avoid parser ambiguity
max := if (a > b) a else b
2. While Loops
The while loop runs as long as a condition evaluates to true.
var i usize = 0
while i < 10 {
print_num(i)
i += 1
}
While with Capture
When the loop condition yields an optional (?T), a |value| capture binds the payload on each turn. The loop runs until the expression yields null — the idiomatic way to drain an iterator:
// Pull items until the iterator is exhausted
while it.next() |item| {
process(item)
}
While with orelse break
You can run a loop and break out when a value resolves to null using orelse break:
var index usize = 0
while index < argc {
arg := argv[index] orelse break
print(arg)
index += 1
}
Inline While Loops (inline while)
An inline while loop is unrolled at compile time. This is particularly useful in comptime blocks or function generation to loop over fixed-size values.
comptime var i = 0
inline while i < 4 {
// This loop is unrolled at compile time
emit(i)
i += 1
}
3. For Loops (for ITEM in ITER)
Flash utilizes a Go-flavored for ITEM in ITER layout. Under the hood, this lowers directly to Zig's for (iter) |item|.
var msg = "hello"
// Loops over each character in the string slice
for char in msg {
print_char(char)
}
Ranges
A numeric range lo..hi iterates the half-open interval [lo, hi) — the upper bound is excluded:
// Prints 0, 1, 2, … 9
for i in 0..10 {
print_num(i)
}
Index Capture
A second capture name gives the zero-based iteration index alongside the element:
// item is each element; idx counts 0, 1, 2, …
for item, idx in items {
print_kv(idx, item)
}
Discard either capture you do not need with _ (for example for _, idx in items to walk the indices only).
Loop else Arms
A while or for loop may carry an else arm, run when the loop ends without hitting a break — the search-loop idiom without a flag variable:
for x in xs {
if x == needle {
break
}
} else {
log("needle is not in xs")
}
Inline For Loops (inline for)
Like inline while, an inline for loop is unrolled at compile time — the body is stamped out once per element. Every for shape rides along: the range iterator, the indexed second capture, the _ discard, and the loop else arm.
inline for w in .{ 8, 16, 32 } {
emit(w)
}
4. Switch
A switch matches a subject value against one or more prongs. The braces and an exhaustive set of prongs are mandatory; a prong body is either a single expression or a block.
switch state {
.ok => log("all good"),
.retry, .pending => log("waiting"), // a multi-pattern prong
else => log("failed"),
}
Inclusive Ranges (...)
Inside a prong, lo...hi matches an inclusive range — note the three dots, distinct from the two-dot slice bound. Like if, switch is also an expression, so every prong can yield a result value:
grade := switch score {
90...100 => 'A',
80...89 => 'B',
else => 'F',
}
Payload Capture
When the subject is a tagged union, a |value| capture after => binds the active variant's payload:
switch msg {
.text => |s| print(s),
.code => |n| print_num(n),
else => {},
}
5. Labeled Loops
A label on a while or for is a break / continue target, so an inner loop can leave or restart an outer one directly:
outer: for row in grid {
for cell in row {
if cell == target {
break :outer // leaves the labeled loop
}
if cell == 0 {
continue :outer // restarts the outer loop's next turn
}
}
}
The same label grammar already names blocks (blk: { … break :blk v }); loops and blocks share it. The label precedes inline (outer: inline while …), and the compiler resolves every labeled break / continue lexically — an unknown label, a continue aimed at a block label, or a label nothing targets each get a targeted diagnostic. A loop's label is not visible from its else arm.
6. Pointer Captures (|*x|)
By default a capture binds a copy of the element or payload. Prefixing the capture with * binds a pointer instead, so the body can write in place:
var arr [4]u8 = .{1, 2, 3, 4}
// Zero the array in place: p is a pointer to each element
for *p in &arr {
p.* = 0
}
The same * works on the if / while payload captures (if opt |*x|, while it.next() |*v|) and on switch prongs (.variant => |*pay|) — everywhere a pointer capture is meaningful. The * rides the element capture only: an index capture stays a value, a *_ discard is rejected (there is no address worth taking), and error captures (catch |e|, else |e|) bind by value. The pointer itself is still an immutable capture — p = q is rejected; p.* = v is the point.