Control Flow
Zena is an expression-oriented language. Key control flow
constructs—including if conditionals, match blocks, and try forms—can be
evaluated as expressions that produce values directly.
Expression orientation ​
In Zena, conditionals and pattern matching can produce values directly for variable initializers, function arguments, or returns:
// 'if' evaluated as an expression
let max = if (a > b) a else b;
// 'match' evaluated as an expression
let label = match (status) {
case 200: 'OK'
case 404: 'Not Found'
case _: 'Error'
};
Within the branches of if expressions and match arms, a block { ... }
evaluates to the value of its trailing expression without requiring an explicit
return:
let price = if (isMember) {
let discount = 10;
basePrice - discount // Evaluates to the result of this expression
} else {
basePrice
};
Conditionals with if and else ​
Conditionals in Zena can be used both as control flow statements and as value-producing expressions.
if as a statement ​
When used as a statement, if executes its body when the condition evaluates to
true. The else branch is optional:
if (score > 100) {
print('High score!');
} else {
print('Keep trying!');
}
if as an expression ​
In languages like JavaScript, TypeScript, and Java, choosing between values
requires the ternary operator (cond ? a : b). In Zena, if expressions
replace the ternary operator:
let max = if (a > b) a else b;
When if is used in an expression context, the else branch is mandatory:
// Chained else-if expression
let description = if (score >= 90) {
'Excellent'
} else if (score >= 70) {
'Good'
} else {
'Needs improvement'
};
Both branches of an if expression must produce compatible types. The resulting
type is the union of the branch types.
Early exits and the never type ​
An arm of an if expression can contain early control-flow jumps—such as
return, throw, break, or continue.
Because an arm that returns or throws never produces a normal value, its type is
never (the bottom type). Because the type system simplifies never | T
directly to T, you can safely perform early returns or throw errors inline:
// If opt is null, early return 0; otherwise bind opt's value
let value: i32 = if (opt != null) opt else return 0;
// If port is invalid, throw immediately; otherwise bind port
let port: i32 = if (rawPort > 0) rawPort else throw new Error('Invalid port');
Pattern conditions with if let ​
An if let condition tests whether an expression matches a pattern. When the
pattern matches, the bound variables are in scope only within the if block.
A primary example is looking up values in a Map. The Map.get(key) method
returns an inline discriminated union of tuples:
get(key: K): inline (true, V) | inline (false, _);
Using if let tests the first boolean element and extracts the second value in
a single step:
let scores = new HashMap<String, i32>();
scores['Alice'] = 95;
if (let (true, score) = scores.get('Alice')) {
// In this branch, matching 'true' narrows the tuple union to (true, V).
// 'score' is statically typed and narrowed to non-null i32.
print('Alice scored: ' + score.toString());
} else {
print('Alice was not found in the map.');
}
Because Map.get returns an inline tuple (compiled to WebAssembly multi-value
returns), this combined lookup, presence check, and narrowing involves zero heap
allocations.
if let is also commonly used with sealed class hierarchies (such as
Option<T>):
let maybeUser: Option<User> = findUser();
if (let Some { value: user } = maybeUser) {
print('Found user: ' + user.name);
}
Nullable Values vs. Patterns
Variable binding patterns like let x match any value unconditionally
(including null). To check and narrow a nullable reference type (T?), use a
standard null check like if (val != null) or type check if (val is T).
Loops and iteration ​
Zena provides three loop constructs: for-in loops for collections, C-style
for loops for indexed iteration, and while loops.
for-in loops ​
The for-in loop iterates over any object implementing the Iterable<T>
interface (such as Array, Map, Set, or custom collections):
let numbers = [10, 20, 30];
var sum = 0;
for (let n in numbers) {
sum += n;
}
The loop variable is declared with let and is locally scoped to each
iteration. Destructuring patterns can be used directly in the loop header:
let entries = [('apple', 1), ('banana', 2)];
for (let (fruit, count) in entries) {
print(fruit + ': ' + count.toString());
}
C-style for loops ​
For numeric iteration or manual loop stepping, Zena supports standard
three-clause for loops. The loop index must be declared with var because it
is mutated:
for (var i = 0; i < 10; i += 1) {
print('Index: ' + i.toString());
}
All three clauses (init, test, update) are optional:
// Infinite loop
for (;;) {
if (shouldStop()) break;
}
while loops ​
while loops repeat execution as long as the condition evaluates to true:
var remaining = 5;
while (remaining > 0) {
print('Countdown: ' + remaining.toString());
remaining -= 1;
}
while let loops ​
A while let loop executes repeatedly as long as the expression continues to
match the pattern. This is particularly useful for iterating over low-level
iterators:
let iterator = collection.iterator();
// Low-level iterator returns inline (true, element) | inline (false, _)
while (let (true, item) = iterator.next()) {
processItem(item);
}
break and continue ​
Loops support break to exit the loop immediately and continue to advance to
the next iteration:
for (var i = 0; i < 10; i += 1) {
if (i % 2 == 0) {
continue; // Skip even numbers
}
if (i > 7) {
break; // Stop loop when index exceeds 7
}
print(i.toString()); // Prints 1, 3, 5, 7
}
Multi-branch selection with match ​
The match expression replaces traditional switch statements found in C,
Java, and JavaScript with a safer, expression-oriented construct:
let label = match (statusCode) {
case 200: 'OK'
case 400: 'Bad Request'
case 404: 'Not Found'
case 500: 'Internal Server Error'
case _: 'Unknown Status'
};
match improves upon switch in several key ways:
- Expression-oriented:
matchreturns a value directly, eliminating the need for mutable variables assigned within branches. - No fallthrough: Each
caseis self-contained; nobreakstatements are required, preventing accidental fallthrough bugs. - Structural matching: Matches not just primitive values, but records, tuples, class instances, and sealed hierarchies.
Exhaustiveness checking ​
match expressions are strictly checked for exhaustiveness at compile time.
The compiler verifies that every possible value of the discriminant type is
handled by at least one arm:
- When matching an enum or a literal union type (
200 | 404 | 500), all variants must be covered. - When matching a sealed class hierarchy, every subclass must be handled.
- If any possibility is omitted, the compiler reports an error.
The wildcard pattern (_) ​
When you only need to handle specific values and want a catch-all fallback for
the remainder of a type, use the wildcard pattern _:
let category = match (httpStatus) {
case 200 | 201 | 204: 'Success'
case 400 | 401 | 403 | 404: 'Client Error'
case 500 | 502 | 503: 'Server Error'
case _: 'Other' // Catches all other integers
};
For a comprehensive guide to pattern types, destructuring, and pattern guards, see Pattern Matching.
Jump statements and unwinding ​
Zena provides several jump statements to control execution flow:
return: Exits the current function and optionally produces a return value (return result;).throw: Throws an exception or error, initiating stack unwinding (see Errors).using: Declares a scope-bound resource that is automatically disposed on every exit path, including returns, breaks, throws, and async cancellation (see Resources and Ownership).
Next ​
- Pattern Matching — irrefutable patterns, match arms, and exhaustiveness
- Collections — arrays, maps, sets, and iterators
- Errors — exception handling and error types