Guides · 05

Control flow

Vyi's control flow is expression-oriented: if, switch, and even bare { … } blocks can produce values, so most "compute one of several things" code needs no temporary mut. There is a single loop keyword, for, that covers while-loops, for-each, and infinite loops. Statements are separated by newlines; a semicolon works as an explicit separator when you want two statements on one line.

if as a statement and an expression

Statement-form if looks like it does everywhere, minus the parentheses. Braces are always required:

vyi
fn classify(n: i32) i32 {
    if n < 0 {
        return -1
    } else if n == 0 {
        return 0
    } else {
        return 1
    }
}

In value position, if is an expression — each branch's block yields its final expression, and the whole if is the value of the branch taken:

vyi
import { stdout } from "@io"

fn main() !u8 {
    con n = 2
    con a = if n == 1 { 100 } else if n == 2 { 200 } else { 30 }
    con sign = if n >= 0 { "+" } else { "-" }
    stdout.writeInt(i64(a))   // prints 200
    stdout.write(" ")
    stdout.write(sign)        // prints +
    stdout.write("\n")
    return Ok(0)
}

An if expression must cover both outcomes — a value has to exist either way — so in value position else is required; con v = if n > 0 { 1 } is rejected with "if-expression requires an else branch". The exception is a known condition: con v = if true { 1 } folds to its live branch at compile time, so no else is needed (a known-false condition without an else still has no value and is rejected). Known-condition folding is covered in generics and known values.

switch

switch matches one subject against a series of arms. Each arm is Pattern: { body }, and arms over an enum bind payloads directly:

vyi
enum Shape {
    Dot,
    Circle(i32),
    Rect(i32, i32),
}

fn (s: Shape) area() i32 {
    switch s {
        Dot:        { return 0 },
        Circle(r):  { return 3 * r * r },
        Rect(w, h): { return w * h },
    }
}

A switch over an enum is exhaustive — miss a variant and the program does not compile:

vyi-error
enum Color { Red, Green, Blue }

fn hue(c: Color) i32 {
    switch c {
        Red:   { return 1 },
        Green: { return 2 },
    }   // error: non-exhaustive match; missing Blue
}

fn main() !u8 { return Ok(0) }

The catch-all arm is spelled any (not __ is the inference/discard marker, and not else). With any, a switch can match on plain values — integers, strings — by equality:

vyi
fn code(word: [*]u8) i32 {
    switch word {
        "stop": { return 0 },
        "go":   { return 1 },
        any:    { return 99 },
    }
}

The comma rule

Arms are comma-delimited like struct fields. A multi-line switch requires a comma after every arm — including the last:

vyi-error
fn label(n: i32) i32 {
    switch n {
        1: { return 10 }
        2: { return 20 },
        any: { return 99 },
    }   // error: missing "," after the first arm
}

fn main() !u8 { return Ok(0) }

A single-line switch separates arms with commas but must not have a trailing one: switch n { 1: 10, 2: 20, any: 99 }.

switch as an expression

In value position each arm yields a value, and single-expression arms don't need braces:

vyi
import { stdout } from "@io"

fn main() !u8 {
    con n = 2
    con label = switch n { 1: 10, 2: 20, any: 99 }
    stdout.writeInt(i64(label))   // prints 20
    stdout.write("\n")
    return Ok(0)
}

An arm that diverges (return, break, panic) simply doesn't produce a value, and that's fine — the switch takes its value from whichever arm actually yields one.

Multi-pattern arms

Comma-separated patterns before the : are alternatives sharing one body:

vyi
import { stdout } from "@io"

enum Color { Red, Green, Blue }

fn warm(c: Color) i32 {
    switch c {
        Red, Green: { return 10 },
        Blue:       { return 1 },
    }
}

fn main() !u8 {
    con a = switch 2 { 1, 2: 11, 3: 3, any: 0 }   // value alternatives work too
    stdout.writeInt(i64(warm(Red)))   // prints 10
    stdout.write(" ")
    stdout.writeInt(i64(a))           // prints 11
    stdout.write("\n")
    return Ok(0)
}

Alternatives cannot bind payloads — a binder would be undefined whenever one of the other alternatives matched, so Ok(n), Err(_): is rejected.

Guard chains

There is no per-arm guard clause; instead, switch over true and let each arm be a condition, matched in order:

vyi
fn classify(n: i32) i32 {
    switch true {
        n > 10: { return 100 },
        n > 5:  { return 50 },
        any:    { return 0 },
    }
}

Switching over types

A union value carries which member type it holds; switch typeof u dispatches on it:

vyi
fn width(u: i32 | bool) i32 {
    switch typeof u {
        i32:  { return 4 },
        bool: { return 1 },
    }
}

switch patterns go deeper than shown here — nested destructuring, if-let binding forms, narrowing — see guide 06 — Pattern matching.

Loops

for is the only loop keyword, in three forms.

Bare condition — a while-loop:

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut n = 5
    mut sum = 0
    for n > 0 {
        sum = sum + n
        n = n - 1
    }
    stdout.writeInt(i64(sum))   // prints 15
    stdout.write("\n")
    return Ok(0)
}

Omit the condition entirely for an infinite loop (exit with break or return):

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut i = 0
    for {
        i = i + 1
        if i == 5 { break }
    }
    stdout.writeInt(i64(i))   // prints 5
    stdout.write("\n")
    return Ok(0)
}

for x in iterable — iterates ranges, fixed arrays [N]T, and array pointers [*]T (a string literal is a [*]u8, so its bytes iterate directly). The loop variable is a fresh immutable binding each iteration — no con keyword needed:

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut s = 0
    for x in 1..5 { s = s + x }          // 1+2+3+4 = 10

    con xs = [3]i32[10, 20, 12]
    for x in xs { s = s + x }            // + 42

    for b in "ab" { s = s + i32(b) }     // + 97 + 98 (bytes)
    stdout.writeInt(i64(s))   // prints 247
    stdout.write("\n")
    return Ok(0)
}

Your own types are iterable too, by implementing the iteration protocol — an @index(i32) method plus a len() method, which is exactly what the built-in collections provide (guide 10 — Collections and strings).

break and continue work as you'd expect: break exits the nearest enclosing loop, continue skips to its next iteration:

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut s = 0
    for x in 0..100 {
        if x >= 9 { break }
        if x % 2 == 0 { continue }
        s = s + x                   // odd values below 9: 1+3+5+7
    }
    stdout.writeInt(i64(s))   // prints 16
    stdout.write("\n")
    return Ok(0)
}

Ranges and slicing

a..b is a half-open range (includes a, excludes b); a...b is inclusive:

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut s = 0
    for x in 1..5  { s = s + x }   // 1+2+3+4  = 10
    for y in 1...5 { s = s + y }   // 1+…+5    = 15
    stdout.writeInt(i64(s))   // prints 25
    stdout.write("\n")
    return Ok(0)
}

A range is an ordinary value with start and end fields, and indexing an array or [*]T with one slices it — you get a sub-view over the same storage, no copy. Either end may be omitted to mean "from the start" / "to the end":

vyi
import { stdout } from "@io"

fn main() !u8 {
    con s = "hello"
    con mid  = s[1..3]   // "el"
    con head = s[..2]    // "he"
    con tail = s[3..]    // "lo"
    stdout.write(mid)
    stdout.write(" ")
    stdout.write(head)
    stdout.write(" ")
    stdout.write(tail)
    stdout.write("\n")   // prints el he lo
    return Ok(0)
}

One asymmetry: for x in 0..10 iterates the range syntactically — a range stored in a binding is a plain two-field value and is not itself iterable.

Labels and jumps

Prefix a loop with #name to label it; break #name and continue #name then target that loop from anywhere inside it — the way out of nested loops:

vyi
import { stdout } from "@io"

fn main() !u8 {
    mut cells = 0
    #rows for row in 0..5 {
        for col in 0..5 {
            if row == 3 { break #rows }        // stop everything
            if col == row { continue #rows }   // next row, skip rest of this one
            cells = cells + 1
        }
    }
    stdout.writeInt(i64(cells))   // rows 0,1,2 count col < row → prints 3
    stdout.write("\n")
    return Ok(0)
}

A label may only name an ancestor — you can't jump sideways, and there is no goto. Standalone blocks can be labeled too, and break #name exits them; but continue is loop-only — a block is never a continue target.

Block expressions and break with a value

A bare { … } block in value position evaluates to its final expression:

vyi
import { stdout } from "@io"

fn main() !u8 {
    con x = {
        con a = 2
        a + 3
    }
    stdout.writeInt(i64(x))   // prints 5
    stdout.write("\n")
    return Ok(0)
}

Inside a value-position block, break value yields early, and break #name value yields from a named ancestor block — even from inside a loop nested within it. This is how you write "search and stop at the first hit" as a single expression:

vyi
import { stdout } from "@io"

fn firstSquareAtLeast(n: i32) i32 {
    con found = #search {
        mut i = 0
        for i * i < 1000 {
            if i * i >= n { break #search i * i }
            i = i + 1
        }
        -1
    }
    return found
}

fn main() !u8 {
    stdout.writeInt(i64(firstSquareAtLeast(8)))   // prints 9
    stdout.write("\n")
    return Ok(0)
}

Only a value-position block can yield a value — a loop never produces one, so break with a value inside a plain loop is rejected:

vyi-error
fn f() i32 {
    for {
        break 5   // error: a loop is not a value-position block
    }
    return 0
}

fn main() !u8 { return Ok(0) }

defer, briefly

defer { … } schedules a block to run when the enclosing function exits — on every exit path, in last-in-first-out order. It keeps cleanup next to the thing it cleans up:

vyi
import { stdout } from "@io"

fn main() !u8 {
    defer { stdout.write("world\n") }
    stdout.write("hello ")
    return Ok(0)   // prints "hello world"
}

That one idea — registered cleanup that survives early returns, errors, and panics — gets its own guide: 08 — defer, recover, and panic.

See also: reference/expressions, reference/statements.