Control Flow

<!-- verified-against: seed-llvm; examples: docs-examples-test -->

Conditions and loops

fn classify(value: i64, values: [3]i64) -> i64 {
    let label = if value > 0 { 1 } else if value < 0 { -1 } else { 0 }

    let mut index: usize = 0
    let mut total = label
    while index < len(values) {
        total += values[index]
        index = index + 1
    }

    for item in values {
        total += item
    }

    total
}

if is an expression. while and for support break and continue. A loop headed by name: can be targeted from nested loops with break :name / continue :name; plain break/continue keep meaning the innermost loop, and sequential reuse of a label is allowed. Exiting runs edge-specific cleanup for every scope in between, and never crosses a spawn boundary. Labeled break/continue take no value. for over ranges, arrays, slices, and text/buffer values (str, string, bytes, shared_bytes) lowers to a direct allocation-free index/length loop. Text and buffers yield u8 elements (byte units, matching indexing); move-only elements require explicit indexing and ownership handling. enumerate follows the same types with a usize index.

Match and ADTs

enum Response {
    Ok(i64)
    Err(str)
}

fn status(value: Response) -> i64 {
    match value {
        Ok(code) => code
        Err(message) => -1
    }
}

Match arms need no commas unless the boundary is ambiguous: an arm whose body ends in an expression followed by a (/[ pattern requires the comma (B044). Match must be exhaustive unless remaining flow is unreachable. Patterns support literals, tuples, structs, variants, bindings, discard, and guards. Move-only payloads transfer only when an arm is selected; a failed pattern or guard preserves the owner for later arms.

if let and while let

fn label(found: ?string) -> i64 {
    if let Some(text) = found { len(text) as i64 } else { 0 }
}

fn count_down(from: ?i64) -> i64 {
    let mut total: i64 = 0
    let mut current = from
    while let Some(value) = current {
        total += value
        if value > 0 { current = Some(value - 1) } else { current = None }
    }
    total
}

Both are surface forms over match and desugar to a two-arm match whose second arm is the complement, so binding, move/borrow behavior, exhaustiveness, and cleanup are exactly the match rules. if let Some(...) names its complement None; another variant pattern keeps a wildcard arm, which H032 rejects when the scrutinee carries a move-only payload (write the explicit match arms there), and an irrefutable pattern (while let name = value) is rejected as H029.

Options, results, and ?

fn find(enabled: bool) -> ?i64 {
    if enabled { Some(42) } else { None }
}

fn forward(enabled: bool) -> ?i64 {
    let value = find(enabled)?
    Some(value)
}

? yields the present/success payload or returns the corresponding failure. Result propagation may change success type but preserves error type. No implicit allocation or clone occurs. ? cannot cross a spawn boundary.

Cleanup on flow edges

Fallthrough, return, break, and continue execute the verified cleanup plan for live owners. Panic/trap/abort edges never unwind and run no cleanup.