Skip to main content

Exhaustiveness Checking

Sector9 ensures that switch expressions cover all possible values of a type, then gives the verifier a complete case split to reason about.

Two Proof Stages

Pattern matching participates in two proof stages:

  1. Compile time: The type checker ensures all cases are covered and warns about unreachable branches.
  2. Verification time: Supported switches are lowered to discriminator-based branch expressions, so postconditions can be proven case by case.

The compiler rejects non-exhaustive patterns before verification begins. This means you cannot accidentally miss a case. The Viper lane then applies an additional verified-subset check: some patterns accepted by the language frontend, such as or-patterns, are still rejected during verification lowering.

Basic Variant Matching

When you match on a variant type, each constructor must have a case:

exhaustive-basic.sr9
// Basic exhaustive pattern matching on variants
persistent actor {
type Status = { #pending; #approved; #rejected };

public func statusCode(s : Status) : async Nat
ensures switch (s) {
case (#pending) result == 0;
case (#approved) result == 1;
case (#rejected) result == 2;
};
{
switch (s) {
case (#pending) 0;
case (#approved) 1;
case (#rejected) 2;
}
};
}

The Status type has three variants, and the switch covers all three. The postcondition uses the same switch syntax to specify what each case should return.

Variants with Payloads

Variant constructors can carry data. Pattern matching extracts payloads into bound variables:

exhaustive-payload.sr9
// Exhaustive matching with variant payloads
persistent actor {
type Color = { #red; #blue : Nat; #green : (Nat, Nat) };

public func colorValue(c : Color) : async Nat
ensures switch (c) {
case (#red) result == 4;
case (#blue x) result == x;
case (#green (a, b)) result == a + b;
};
{
switch (c) {
case (#red) 4;
case (#blue x) x;
case (#green (a, b)) a + b;
}
};
}

Each case binds variables that can be used in both the implementation and the postcondition:

  • #red carries no payload
  • #blue x binds x : Nat
  • #green (a, b) binds a tuple payload

Option Types

The built-in option type ?T has exactly two cases: null and ?value:

exhaustive-option.sr9
// Exhaustive matching on option types
persistent actor {
public func increment(opt : ?Nat) : async ?Nat
ensures switch (opt, result) {
case (null, null) true;
case (?n, ?m) m == n + 1;
case (_, _) false;
};
{
switch (opt) {
case (null) null;
case (?n) ?(n + 1);
}
};
}

The postcondition uses a tuple pattern (opt, result) to express the relationship between input and output for each case. The wildcard case (_, _) false keeps the postcondition exhaustive while making impossible combinations fail if the implementation ever produces them.

Nested Pattern Matching

You can nest switches to handle combinations of variant types:

exhaustive-nested.sr9
// Nested exhaustive pattern matching
persistent actor {
type Shape = { #circle : Nat; #square : Nat };
type Color = { #red; #blue : Nat };

public func describe(s : Shape, c : Color) : async Nat
ensures switch (s, c) {
case ((#circle r), (#red)) result == r;
case ((#circle r), (#blue k)) result == r + k;
case ((#square a), (#red)) result == a * 4;
case ((#square a), (#blue k)) result == a * 4 + k;
};
{
switch (s) {
case (#circle r) {
switch (c) {
case (#red) r;
case (#blue k) r + k;
}
};
case (#square a) {
switch (c) {
case (#red) a * 4;
case (#blue k) a * 4 + k;
}
};
}
};
}

The implementation uses nested switch statements, while the postcondition uses a single switch with tuple patterns. Both approaches are valid and must cover all combinations.

How Verification Works

When Sector9 translates supported switch expressions to the verification backend, it generates:

  1. Discriminator functions for each variant constructor (e.g., isStatus$pending, isStatus$approved)
  2. Exhaustiveness and exclusivity axioms ensuring every value has one constructor tag
  3. Nested if-then-else chains with an unreachable fallback

For a type with constructors #a, #b, #c, the verifier knows:

forall v : Type :: isType$a(v) || isType$b(v) || isType$c(v)

These axioms let the SMT solver reason about which branch is taken and verify that postconditions hold.

Detecting Wrong Postconditions

The verifier catches mismatches between implementation and postcondition:

// Exhaustive matching with variant payloads
persistent actor {
type Color = { #red; #blue : Nat; #green : (Nat, Nat) };

public func colorValue(c : Color) : async Nat
ensures switch (c) {
case (#red) result == 4;
case (#blue x) result == x;
case (#green (a, b)) result == a + b;
};
{
switch (c) {
case (#red) 4;
case (#blue x) x;
case (#green (a, b)) a + b;
}
};
}

The wrong version claims #red returns 5, but the implementation returns 4. The verifier detects this contradiction and reports a verification failure.

Compile-Time Errors

If you miss a case, the compiler rejects the program before verification:

// REJECTED: missing #rejected case
switch (status) {
case (#pending) 0;
case (#approved) 1;
}

Error M0145: this pattern of type Status does not cover value #rejected

Similarly, unreachable patterns trigger warnings:

// WARNING: second #pending is never matched
switch (status) {
case (#pending) 0;
case (#pending) 1; // unreachable
case (#approved) 2;
case (#rejected) 3;
}

Warning M0146: this pattern is never matched

Unsupported Patterns

Some pattern features are not supported in verification:

PatternStatusExample
Simple variantsSupportedcase (#red)
Variant payloadsSupportedcase (#blue x)
Tuple patternsSupportedcase (a, b)
Option patternsSupportedcase (?n)
WildcardsSupportedcase (_)
Or-patternsFrontend supported, Viper-lane unsupportedcase (0 or 1)
Refutable let patternsViper-lane unsupportedlet ?x = opt
Refutable for patternsViper-lane unsupportedfor (?x in xs.vals())
Mutable record field patternsSyntax unsupportedcase ({ var x = n })

Attempting to use unsupported verified patterns results in a translation error such as patterns in let bindings must be irrefutable for verification or for-in: pattern with boolean guards not supported.

Summary

  • Pattern exhaustiveness is checked at compile time, then used as a complete case split during verification
  • All variant constructors must have matching cases
  • Postconditions can use switch expressions to specify per-case behavior
  • The verifier generates exhaustiveness axioms for SMT reasoning
  • Missing cases are compile-time errors (M0145)
  • Unreachable patterns trigger warnings (M0146)
  • Or-patterns, refutable let patterns, and refutable for patterns are not supported in the Viper verification lane