Control Flow
Branching and looping. Most control-flow constructs are expressions in Kestrel — they produce a value — which means you can use them on the right side of let.
If / Else
let isAdmin = false;
let isMember = true;
if isAdmin {
println("full access");
} else if isMember {
println("member access");
} else {
println("no access");
}
Because if is an expression, you can assign its result:
let score = 85;
let label = if score > 90 {
"excellent"
} else if score > 70 {
"good"
} else {
"needs work"
};
Each branch must produce a value of the same type. Unless one of them diverges — calls a !-returning function, breaks, or returns — in which case it's allowed to be a different shape, since execution will never get past it. See Functions → Return Types.
Loops
while runs as long as a condition holds:
var i = 0;
while i < 10 {
println("\(i)");
i = i + 1;
}
loop is unconditional — exit it with break:
var countdown = 3;
loop {
if countdown == 0 { break; }
println("\(countdown)");
countdown = countdown - 1;
}
for walks an iterable:
let prices = [12, 30, 7];
var total = 0;
for price in prices {
total = total + price;
}
continue skips to the next iteration. break exits the loop. Both can target an outer loop with a label:
let rows = [[1, 2], [3, 4]];
let target = 3;
search: for row in rows {
for cell in row {
if cell == target {
break search;
}
}
}
Guard
guard is the early-exit pattern. It checks a condition and, if it fails, runs an else block that must leave the surrounding scope (return, throw, break, or call a !-returning function).
enum ParseError {
case EmptyInput
case ParseFailed
}
func parse(input: String) -> Optional[Int] {
if input == "42" { .Some(42) } else { .None }
}
func transform(n: Int) -> Int { n * 2 }
func process(input: String) -> Result[Int, ParseError] {
guard not input.isEmpty else {
return .Err(.EmptyInput);
}
guard let .Some(parsed) = parse(input) else {
return .Err(.ParseFailed);
}
// parsed is in scope here, unwrapped
.Ok(transform(parsed))
}
Compared to if, guard keeps the happy path at the outer indentation level. Use it when "the rest of this function depends on this being true."
Match
match checks a value against patterns. The first matching arm runs.
enum Status {
case Active
case Paused(String)
case Stopped
}
let status = Status.Paused("rebalancing");
match status {
.Active => println("running"),
.Paused(reason) => println("paused: \(reason)"),
.Stopped => println("done"),
}
Like if, match is an expression — every arm produces a value:
let label: String = match status {
.Active => "live",
.Paused(_) => "halted",
.Stopped => "off"
};
The compiler checks that every case is handled. Add a new variant to the enum and every existing match lights up red until you cover it. Use _ as a catch-all when you genuinely want to default; don't use it to silence the exhaustiveness checker. For the deeper pattern-matching story — destructuring, guards, bindings — see Enums → Pattern Matching.