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
| Construct | Purpose | Oxide Syntax |
|---|---|---|
if | Conditional branching | if cond { } else { } |
if let | Pattern match + conditional | if let x = nullable { } |
guard | Early return on failure | guard cond else { return } |
match | Multi-way pattern matching | match x { P -> e, else -> d } |
loop | Infinite loop | loop { } |
while | Conditional loop | while cond { } |
while let | Pattern match loop | while let x = iter.next() { } |
for | Iteration | for 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, butelseis idiomatic) guardprovides clean early-return syntaxif let x = nullableauto-unwraps withoutSome()- 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.