Skip to main content

Conditionals

Conditionals let runtime code and specifications split facts into explicit cases. In verified code, each branch is a proof path: postconditions, actor invariants, and declared effects must hold no matter which branch runs.

Basic Syntax

The if/else construct is an expression in Sector9, meaning it returns a value. The basic form is:

if (condition) { thenExpr } else { elseExpr }

The condition must be of type Bool. Both branches return values, and the overall type is the common supertype of both branch types.

cond-basic.sr9
// Basic if/else expressions
module {
// If/else returns a value
pure func max(a : Int, b : Int) : Int {
if (a >= b) { a } else { b }
};

// If without else returns unit
public func maybeLog(shouldLog : Bool) : async () {
if (shouldLog) {
// side effect would go here
};
};
}

If Without Else

When you omit the else branch, the expression returns unit ():

if (condition) {
// side effect
};

This is useful for conditional side effects where you don't need a return value.

Chained Conditionals

Use else if to chain multiple conditions:

cond-chain.sr9
// Chained if/else if/else
module {
pure func sign(x : Int) : Int {
if (x > 0) {
1
} else if (x < 0) {
-1
} else {
0
}
};

pure func grade(score : Nat) : Text {
if (score >= 90) { "A" }
else if (score >= 80) { "B" }
else if (score >= 70) { "C" }
else if (score >= 60) { "D" }
else { "F" }
};
}

Chained conditionals are evaluated top-to-bottom. The first branch whose condition is true executes, and remaining branches are skipped.

Type Requirements

Condition Type

The condition expression must be exactly Bool. There are no implicit conversions:

// Correct
if (x > 0) { ... }
if (flag) { ... }
if (a == b) { ... }

// Wrong - these are not Bool
// if (x) { ... } // x is Int, not Bool
// if (count) { ... } // count is Nat, not Bool

Branch Types

Branch types don't need to match exactly. The compiler computes the least upper bound (LUB) of both types:

let x : Int = if (flag) { 5 } else { -3 };  // Both Int
let y = if (flag) { 1 } else { 2 }; // Inferred as Int

If branch types are incompatible, the compiler issues warning M0081 and infers an opaque supertype. Prefer explicit type annotations or matching branch types.

Nested Conditionals

Conditionals can be nested arbitrarily deep:

cond-nested.sr9
// Nested conditionals
module {
pure func quadrant(x : Int, y : Int) : Nat {
if (x >= 0) {
if (y >= 0) { 1 } else { 4 }
} else {
if (y >= 0) { 2 } else { 3 }
}
};

// Combining with boolean operators is often cleaner
pure func quadrantAlt(x : Int, y : Int) : Nat {
if (x >= 0 and y >= 0) { 1 }
else if (x < 0 and y >= 0) { 2 }
else if (x < 0 and y < 0) { 3 }
else { 4 }
};
}

For complex conditions, consider boolean operators (and, or) or switch expressions so the value shape is visible in the code.

Conditionals in Pure Functions

Pure functions can use conditionals for side-effect-free case analysis:

cond-pure.sr9
// Conditionals in pure functions
module {
// Pure functions with conditionals for case analysis
pure func abs(x : Int) : Int {
if (x < 0) { -x } else { x }
};

pure func clamp(x : Int, lo : Int, hi : Int) : Int {
if (x < lo) { lo }
else if (x > hi) { hi }
else { x }
};

// Using pure conditionals in contracts
pure func isValid(x : Nat) : Bool {
x > 0 and x < 1000
};
}

Pure conditional functions are callable from requires and ensures clauses, making them useful for reusable predicates.

Verification with Conditionals

Postconditions with Implication

When a function's behavior depends on a condition, implication (==>) is often clearer than a nested conditional:

cond-contract.sr9
// Conditionals with verification contracts
persistent actor {
var balance : Nat = 100;

// Postconditions use implication for branch-dependent behavior
public func withdraw(amount : Nat) : async Bool
modifies balance
ensures amount <= old(balance) ==> result == true;
ensures amount <= old(balance) ==> balance == old(balance) - amount;
ensures amount > old(balance) ==> result == false;
ensures amount > old(balance) ==> balance == old(balance);
{
if (amount <= balance) {
balance -= amount;
true
} else {
false
}
};
}

The verifier proves each implication under the branch fact on the left side. Use this when different input regions imply different result facts.

Invariant Preservation

When conditionals appear in methods, every branch that can reach the end of the method must re-establish actor invariants:

persistent actor {
var balance : Nat = 0;
invariant balance <= 1000;

public func adjust(delta : Int) : async ()
modifies balance
entry_requires balance + delta >= 0;
entry_requires balance + delta <= 1000;
requires balance + delta >= 0;
requires balance + delta <= 1000;
{
if (delta > 0) {
balance += delta; // Must preserve: balance <= 1000
} else {
balance -= (-delta); // Must preserve: balance <= 1000
};
};
}

The verifier checks that the invariant holds after both branches.

Conditional Effects

When a field is written in only one branch, it's still included in the effects summary:

private func maybeIncrement(flag : Bool) : () modifies counter {
if (flag) {
counter += 1;
};
// counter is in effects even though it's conditionally written
};

Fields not written in any branch can be framed as unchanged. Use modifies to make conditional writes explicit in the method contract.

Common Patterns

Guard Pattern

Check a condition before proceeding:

public func process(x : Nat) : async Nat
entry_requires x > 0;
requires x > 0;
{
if (x == 0) {
return 0; // Early return (though precondition already guards this)
};
// Main logic
x * 2
};

Conditional Assignment

Select between values:

let status = if (count > 0) { #active } else { #inactive };

Null Checking

For option types, prefer switch over if:

// Prefer switch for options
switch (maybeValue) {
case (?v) { /* use v */ };
case null { /* handle null */ };
};

// Rather than
if (maybeValue != null) { ... }

Restrictions

Contract Style

Prefer implications or switch expressions in contracts because they keep each proof case explicit:

// Correct - use implication
ensures x > 0 ==> result > 0;

// Also valid when a true value expression is clearer
ensures if (x > 0) { result > 0 } else { true };

No Allocation in Loop Conditions

Array or object literals cannot appear in while conditions:

// Rejected
while (([] : [Nat]).size() == 0) { ... }

// Use a variable instead
let arr : [Nat] = [];
while (arr.size() == 0) { ... }

Summary

  • if/else is an expression that returns a value
  • The condition must be Bool; no implicit conversions
  • Branch types are unified via least upper bound
  • if without else returns unit ()
  • Use ==> or switch in contracts when a proof splits into cases
  • Both branches must preserve invariants
  • Prefer switch for option types and pattern matching