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:

zena
// '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:

zena
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:

zena
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:

zena
let max = if (a > b) a else b;

When if is used in an expression context, the else branch is mandatory:

zena
// 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:

zena
// 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:

zena
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:

zena
let scores = new Map<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>):

zena
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):

zena
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:

zena
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:

zena
for (var i = 0; i < 10; i += 1) {
  print('Index: ' + i.toString());
}

All three clauses (init, test, update) are optional:

zena
// Infinite loop
for (;;) {
  if (shouldStop()) break;
}

while loops ​

while loops repeat execution as long as the condition evaluates to true:

zena
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:

zena
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:

zena
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:

zena
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: match returns a value directly, eliminating the need for mutable variables assigned within branches.
  • No fallthrough: Each case is self-contained; no break statements 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 _:

zena
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