Skip to main content

Combining Conditions

Building complex specifications from simple predicates using logical operators.

Operator Summary​

Sector9 provides these logical operators for specifications:

OperatorNameMeaning
notNegationTrue if operand is false
andConjunctionTrue if both operands are true
orDisjunctionTrue if at least one operand is true
==>ImplicationTrue if antecedent is false or consequent is true
<==>BiconditionalTrue if both operands have the same truth value

not, and, and or are ordinary boolean operators. ==> and <==> are ghost/specification operators: use them in contracts, static assertions, loop invariants, pure functions, and lemmas, but use if, and, or, not, or boolean equality in executable runtime code.

Precedence Rules​

Operators bind in this order (tightest to loosest):

  1. not (prefix, tightest)
  2. and
  3. or
  4. ==> and <==> (same level, loosest)

This means:

not a and b or c ==> d
// Parses as: (((not a) and b) or c) ==> d

a and b ==> c or d
// Parses as: (a and b) ==> (c or d)

Use parentheses liberally for clarity, especially in complex expressions.

Building Specifications​

Case Analysis​

Use implication to specify different cases:

public func absolute(x : Int) : async Int
ensures x >= 0 ==> result == x;
ensures x < 0 ==> result == -x;
{
if (x >= 0) { x } else { -x }
};

Each ensures clause handles one case. The verifier proves both.

Conjunctive Postconditions​

Multiple ensures clauses are implicitly conjoined:

public func clamp(x : Int, lo : Int, hi : Int) : async Int
entry_requires lo <= hi;
requires lo <= hi;
ensures result >= lo;
ensures result <= hi;
ensures x < lo ==> result == lo;
ensures x > hi ==> result == hi;
ensures lo <= x and x <= hi ==> result == x;
{
if (x < lo) { lo }
else if (x > hi) { hi }
else { x }
};

All five postconditions must hold simultaneously.

Disjunctive Conditions​

Express "one or the other" with or:

public func classify(x : Int) : async Text
ensures result == "negative" or result == "zero" or result == "positive";
{
if (x < 0) { "negative" }
else if (x == 0) { "zero" }
else { "positive" }
};

Negation for Exclusion​

Use not to exclude impossible cases:

public func findPositive(arr : [Int]) : async ?Nat
ensures result == null ==> not exists<Nat>(pure func(i : Nat) : Bool = i < arr.size() and arr[i] > 0);
{
// ... search implementation
};

Complex Patterns​

Bounded Implications​

Combine range checks with implications:

public func lookup(arr : [Nat], idx : Nat) : async Nat
entry_requires idx < arr.size();
requires idx < arr.size();
ensures result == arr[idx];
ensures idx == 0 ==> result == arr[0]; // First element case
{
arr[idx]
};

Nested Conditionals​

Express hierarchical logic with nested implications:

public func score(level : Nat, bonus : Bool) : async Nat
ensures level < 5 ==> (bonus ==> result == 2);
ensures level < 5 ==> (not bonus ==> result == 1);
ensures level >= 5 ==> result == 3;
{
if (level < 5) {
if (bonus) { 2 } else { 1 }
} else { 3 }
};

State Machine Transitions​

Verify valid state transitions:

type State = { #idle; #running; #stopped };

public func transition(current : State, event : Text) : async State
ensures current == #idle and event == "start" ==> result == #running;
ensures current == #running and event == "stop" ==> result == #stopped;
ensures current == #stopped ==> result == #stopped; // Terminal state
{
switch (current, event) {
case (#idle, "start") { #running };
case (#running, "stop") { #stopped };
case _ { current };
}
};

Logical Laws in Specifications​

Verify well-known logical identities:

public func verifyDeMorgan(a : Bool, b : Bool, c : Bool) : async ()
{
// De Morgan's laws
assert not (a and b) <==> (not a or not b);
assert not (a or b) <==> (not a and not b);

// Contrapositive
assert (a ==> b) <==> (not b ==> not a);

// Distribution
assert a and (b or c) <==> (a and b) or (a and c);
assert a or (b and c) <==> (a or b) and (a or c);
};

Tips​

Start Simple​

Build complex conditions incrementally:

// Instead of one complex ensures:
// ensures (x > 0 and y > 0 ==> result > 0) and (x <= 0 or y <= 0 ==> result <= 0)

// Split into clear cases:
ensures x > 0 and y > 0 ==> result > 0;
ensures x <= 0 ==> result <= 0;
ensures y <= 0 ==> result <= 0;

Use Helper Predicates​

For repeated conditions, define pure helper functions:

pure func inRange(x : Int, lo : Int, hi : Int) : Bool {
lo <= x and x <= hi
};

public func process(x : Int) : async Int
entry_requires inRange(x, 0, 100);
requires inRange(x, 0, 100);
ensures inRange(result, 0, 100);
{
// ...
};

Watch for Vacuous Truth​

Remember that false ==> anything is always true:

// This always passes, even if the function misbehaves when x < 0
ensures x >= 0 ==> result >= 0;

// To catch bugs for negative inputs, add:
ensures x < 0 ==> result < 0;

Summary​

  • Precedence: not > and > or > ==> / <==>
  • Multiple ensures clauses are conjoined (AND-ed together)
  • Use implication for case analysis in postconditions
  • Keep ==> and <==> in ghost/specification contexts
  • Split complex conditions into smaller, clearer parts
  • Define pure helper predicates for reusable conditions