Control Flow

Control flow constructs let you run code conditionally or repeatedly. Oxide provides several ways to control execution: if expressions, match expressions, guard statements, and various loops. While the semantics match Rust exactly, some syntax is designed to be more approachable.

if Expressions

An if expression lets you branch your code based on conditions:

fn main() {
    let number = 7

    if number < 5 {
        println!("condition was true")
    } else {
        println!("condition was false")
    }
}

The condition must be a Bool. Unlike some languages, Oxide won't automatically convert non-Boolean types:

fn main() {
    let number = 3

    // This won't compile!
    if number {  // Error: expected Bool, found Int
        println!("number was three")
    }

    // This works:
    if number != 0 {
        println!("number was not zero")
    }
}

Multiple Conditions with else if

Chain conditions with else if:

fn main() {
    let number = 6

    if number % 4 == 0 {
        println!("number is divisible by 4")
    } else if number % 3 == 0 {
        println!("number is divisible by 3")
    } else if number % 2 == 0 {
        println!("number is divisible by 2")
    } else {
        println!("number is not divisible by 4, 3, or 2")
    }
}

Using if in a let Statement

Because if is an expression, you can use it on the right side of a let:

fn main() {
    let condition = true
    let number = if condition { 5 } else { 6 }

    println!("The value of number is: \(number)")
}

Both branches must return the same type:

fn main() {
    let condition = true

    // This won't compile!
    let number = if condition { 5 } else { "six" }  // Error: incompatible types
}

if let for Pattern Matching

The if let syntax combines pattern matching with conditionals. It's especially useful for nullable types:

fn main() {
    let maybeNumber: Int? = Some(42)

    // Auto-unwrap: the Some() wrapper is implicit for T?
    if let number = maybeNumber {
        println!("Got number: \(number)")
    }

    // Explicit Some() also works
    if let Some(number) = maybeNumber {
        println!("Got number: \(number)")
    }

    // With else branch
    if let value = maybeNumber {
        println!("Value: \(value)")
    } else {
        println!("No value present")
    }
}

Rust comparison: In Oxide, if let x = nullable automatically wraps the pattern in Some() when the right-hand side is a nullable type. In Rust, you must always write if let Some(x) = nullable.

#![allow(unused)]
fn main() {
// Rust requires explicit Some()
if let Some(number) = maybe_number {
    println!("Got number: {}", number);
}
}

guard Statements

The guard statement is for early returns when conditions aren't met. The else block must diverge (return, break, continue, or panic):

fn processUser(user: User?): Result<String, Error> {
    guard let user = user else {
        return Err(anyhow!("User not found"))
    }
    // `user` is now available and non-null

    guard user.isActive else {
        return Err(anyhow!("User is not active"))
    }
    // We know user is active here

    Ok("Processing \(user.name)")
}

guard is particularly useful for validation at the start of functions:

fn divide(a: Int, b: Int): Result<Int, String> {
    guard b != 0 else {
        return Err("Cannot divide by zero".toString())
    }

    Ok(a / b)
}

fn processItems(items: Vec<Item>): Result<Summary, Error> {
    guard !items.isEmpty() else {
        return Err(anyhow!("No items to process"))
    }

    // Continue with non-empty items...
    Ok(summarize(items))
}

Rust comparison: Oxide's guard let x = expr else { } is equivalent to Rust's let Some(x) = expr else { }. The guard condition else { } form is similar to an inverted if.

#![allow(unused)]
fn main() {
// Rust
let Some(user) = user else {
    return Err(anyhow!("User not found"));
};

// Or for conditions:
if items.is_empty() {
    return Err(anyhow!("No items"));
}
}

match Expressions

The match expression compares a value against a series of patterns. Oxide uses -> for match arms (instead of Rust's =>) and else for the wildcard pattern (instead of _):

fn main() {
    let number = 3

    match number {
        1 -> println!("one"),
        2 -> println!("two"),
        3 -> println!("three"),
        else -> println!("something else"),
    }
}

Matching Multiple Patterns

Use | to match multiple values:

fn main() {
    let number = 2

    match number {
        1 | 2 -> println!("one or two"),
        3 -> println!("three"),
        else -> println!("other"),
    }
}

Matching Ranges

Use ..= for inclusive ranges:

fn main() {
    let number = 7

    match number {
        1..=5 -> println!("one through five"),
        6..=10 -> println!("six through ten"),
        else -> println!("something else"),
    }
}

Matching with Guards

Add conditions to patterns with if:

fn main() {
    let pair = (2, -2)

    match pair {
        (x, y) if x == y -> println!("twins"),
        (x, y) if x + y == 0 -> println!("opposites"),
        (x, _) if x % 2 == 0 -> println!("first is even"),
        else -> println!("no match"),
    }
}

Matching Enums

Match is essential for working with enums:

enum Status {
    Active,
    Inactive,
    Pending { reason: String },
}

fn describeStatus(status: Status): String {
    match status {
        Status.Active -> "User is active".toString(),
        Status.Inactive -> "User is inactive".toString(),
        Status.Pending { reason: r } -> "Pending: \(r)".toString(),
    }
}

Rust comparison: Oxide uses dot notation (Status.Active) for enum variants. Rust's double colon syntax (Status::Active) does not exist in Oxide and will cause a syntax error.

Matching Nullable Types

Use null in pattern position to match None:

fn describe(value: Int?): String {
    match value {
        Some(n) if n > 0 -> "positive: \(n)".toString(),
        Some(n) if n < 0 -> "negative: \(n)".toString(),
        Some(0) -> "zero".toString(),
        null -> "no value".toString(),
    }
}

Match as Expression

Like if, match is an expression and returns a value:

fn main() {
    let number = 3

    let description = match number {
        1 -> "one",
        2 -> "two",
        3 -> "three",
        else -> "many",
    }

    println!("Number is \(description)")
}

Multi-Statement Arms

Use blocks for complex match arms:

fn process(value: Int): String {
    match value {
        0 -> "zero".toString(),
        n if n > 0 -> {
            let doubled = n * 2
            let squared = n * n
            "positive: doubled=\(doubled), squared=\(squared)".toString()
        },
        else -> {
            println!("Warning: negative value")
            "negative".toString()
        },
    }
}

Loops

Oxide provides three loop constructs: loop, while, and for.

Infinite Loops with loop

The loop keyword creates an infinite loop:

fn main() {
    var counter = 0

    loop {
        counter += 1
        println!("Count: \(counter)")

        if counter >= 5 {
            break
        }
    }
}

Returning Values from Loops

You can return a value from a loop using break:

fn main() {
    var counter = 0

    let result = loop {
        counter += 1

        if counter == 10 {
            break counter * 2
        }
    }

    println!("Result: \(result)")  // Prints: Result: 20
}

Loop Labels

Use labels to break or continue outer loops:

fn main() {
    var count = 0

    'outer: loop {
        println!("count = \(count)")
        var remaining = 10

        loop {
            println!("remaining = \(remaining)")

            if remaining == 9 {
                break
            }
            if count == 2 {
                break 'outer
            }
            remaining -= 1
        }

        count += 1
    }

    println!("End count = \(count)")
}

Conditional Loops with while

Execute code while a condition is true:

fn main() {
    var number = 3

    while number != 0 {
        println!("\(number)!")
        number -= 1
    }

    println!("LIFTOFF!")
}

while let for Conditional Pattern Matching

Similar to if let, but loops while the pattern matches:

fn main() {
    var stack: Vec<Int> = vec![1, 2, 3]

    while let value = stack.pop() {
        println!("Popped: \(value)")
    }
}

Iterating with for

The for loop iterates over collections:

fn main() {
    let numbers = [10, 20, 30, 40, 50]

    for number in numbers {
        println!("Value: \(number)")
    }
}

Iterating with Ranges

fn main() {
    // 1 to 4 (exclusive end)
    for i in 1..5 {
        println!("\(i)")
    }

    // 1 to 5 (inclusive end)
    for i in 1..=5 {
        println!("\(i)")
    }

    // Reverse order
    for i in (1..=5).rev() {
        println!("\(i)")
    }
}

Iterating with Index

Use enumerate() to get both index and value:

fn main() {
    let names = vec!["Alice", "Bob", "Charlie"]

    for (index, name) in names.iter().enumerate() {
        println!("\(index): \(name)")
    }
}

Loop Control

Use break and continue to control loop execution:

fn main() {
    for i in 1..=10 {
        if i == 3 {
            continue  // Skip 3
        }
        if i == 8 {
            break  // Stop at 8
        }
        println!("\(i)")
    }
}

Combining Control Flow

Control flow constructs can be combined for complex logic:

fn processUsers(users: Vec<User>): Vec<String> {
    var results: Vec<String> = vec![]

    for user in users.iter() {
        // Skip inactive users
        guard user.isActive else {
            continue
        }

        // Handle different user types
        let message = match user.role {
            Role.Admin -> "Admin: \(user.name)".toString(),
            Role.Moderator -> "Mod: \(user.name)".toString(),
            Role.User -> {
                if let email = user.email {
                    "User: \(user.name) <\(email)>".toString()
                } else {
                    "User: \(user.name)".toString()
                }
            },
        }

        results.push(message)
    }

    results
}

Summary

ConstructPurposeOxide Syntax
ifConditional branchingif cond { } else { }
if letPattern match + conditionalif let x = nullable { }
guardEarly return on failureguard cond else { return }
matchMulti-way pattern matchingmatch x { P -> e, else -> d }
loopInfinite looploop { }
whileConditional loopwhile cond { }
while letPattern match loopwhile let x = iter.next() { }
forIterationfor item in collection { }

Key Oxide differences from Rust:

  • Match arms use -> (Rust's => is invalid in Oxide)
  • Match wildcard is else (Rust's _ can still be used, but else is idiomatic)
  • guard provides clean early-return syntax
  • if let x = nullable auto-unwraps without Some()
  • Enum variants use dot notation (Enum.Variant); Rust's :: syntax does not exist in Oxide

These constructs give you precise control over program flow while maintaining Oxide's goal of being approachable and readable.