ajhahn.de
← all chapters

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.