Counterexample Analysis
Using --counterexample to find the failing case.
When verification fails with an abstract "might not hold" message, counterexamples can provide concrete values that trigger the failure. This page explains how to analyze backend models when the Viper server can produce them.
Getting Counterexamples
Add --counterexample (or --models) to your verification command:
sector9 --verify --counterexample file.sr9
Counterexample extraction requires the Viper server backend. If this environment
cannot locate viperserver.jar, the command exits before producing models. You
can still use JSON Output or TOON Format without models to inspect the failing obligation.
This produces output like:
------------------------------
[1] Postcondition might not hold
Assertion result == a + b + 1 might not hold.
method: add
Counterexample summary
return: a=0, b=0, result=0
The counterexample shows that a=0, b=0 produces result=0, but the postcondition expects result=1.
The Analysis Workflow
Counterexample analysis follows a three-step process. Use the obligation from Reading Error Messages first, then use the model to decide whether the specification or implementation is wrong.
Step 1: Identify the Failing Inputs
Look at the counterexample bindings and identify which values are relevant to the failure:
Counterexample summary
return: amount=100, self.balance=50
Here amount=100 and self.balance=50 are the inputs that cause the failure.
Step 2: Trace the Execution
Mentally execute the function with those specific values:
public func withdraw(amount : Nat) : async Nat
reads balance
modifies balance
ensures result == amount; // What happens when amount=100, balance=50?
{
balance := balance - amount; // 50 - 100 = underflow
amount
}
Step 3: Determine the Fix
Ask: Are these inputs valid or invalid?
- Valid inputs exposing a bug: Fix the implementation
- Invalid inputs: Add a precondition to rule them out
For the withdrawal example, amount=100 when balance=50 is an invalid
scenario. Add an exported runtime guard plus the matching logical precondition:
public func withdraw(amount : Nat) : async Nat
entry_requires amount <= balance;
requires amount <= balance; // Rule out invalid inputs
reads balance
modifies balance
ensures result == amount;
Analyzing Postcondition Failures
Postcondition failures are the most common case for counterexample analysis.
Example: Off-by-One Error
persistent actor Counter {
var count : Nat = 0;
public func increment() : async Nat
modifies count
ensures count == old(count) + 2; // Bug: claiming +2 when doing +1
{
count += 1;
count
};
}
Running with --counterexample:
[1] Postcondition might not hold
Assertion count == old(count) + 2 might not hold.
Counterexample summary
return: self.count=1
Analysis: When old(count)=0, incrementing by 1 gives count=1, not count=2. The postcondition is wrong.
Fix: Change + 2 to + 1 in the postcondition.
Example: Missing Case
public func abs(x : Int) : async Int
ensures result >= 0;
ensures x >= 0 ==> result == x;
{
x // Bug: doesn't handle negative case
};
Counterexample:
Counterexample summary
return: x=-1, result=-1
Analysis: When x=-1, the function returns -1, violating result >= 0.
Fix: Add the negative case:
public func abs(x : Int) : async Int
ensures result >= 0;
{
if (x >= 0) { x } else { -x }
};
Analyzing Precondition Failures
When a precondition fails at a call site, the counterexample shows the caller's state.
public func safeDivide(a : Nat, b : Nat) : async Nat
entry_requires b != 0;
requires b != 0;
{
a / b
};
public func compute(x : Nat) : async Nat {
try { await safeDivide(10, x) } catch (_) { 0 } // Error: x might be 0
};
Counterexample:
[1] Precondition might not hold
Precondition b != 0 might not hold at this call site.
Counterexample summary
entry: x=0
Analysis: The caller can receive x=0, which violates the callee's requirement.
Fix: Add a guard or precondition to the caller:
public func compute(x : Nat) : async Nat
entry_requires x != 0;
requires x != 0;
{
try { await safeDivide(10, x) } catch (_) { 0 }
};
Analyzing Invariant Failures
Actor invariants must hold at public boundaries. Counterexamples show the state when the invariant breaks.
persistent actor Bank {
var deposits : Nat = 0;
var withdrawals : Nat = 0;
invariant deposits >= withdrawals;
public func withdraw(amount : Nat) : async ()
modifies withdrawals
{
withdrawals += amount; // Can break invariant
};
}
Counterexample:
[1] Invariant might not hold
Assertion deposits >= withdrawals might not hold.
Counterexample summary
return: amount=100, self.deposits=50, self.withdrawals=150
Analysis: Starting with deposits=50, withdrawals=50, withdrawing 100 makes withdrawals=150, breaking the invariant.
Fix: Add a precondition:
public func withdraw(amount : Nat) : async ()
entry_requires withdrawals + amount <= deposits;
requires withdrawals + amount <= deposits;
modifies withdrawals
Analyzing Loop Invariant Failures
Loop invariant failures show the state at a specific iteration.
public func sum(arr : [Nat]) : async Nat {
var total : Nat = 0;
var i : Nat = 0;
while (i < arr.size())
loop:invariant total >= 0; // Too weak
loop:invariant i <= arr.size();
{
total += arr[i];
i += 1;
};
total
};
If verification fails because the invariant is too weak to prove the postcondition, you need to strengthen it:
loop:invariant forall<Nat>(pure func (j : Nat) : Bool =
j < i ==> arr[j] <= total);
Interpreting Counterexample Values
Numeric Values
Integers appear as decimal strings:
a=0, b=-5, result=100
Reference Values
Heap objects appear as solver-internal representations:
self=Null($Ref!val!0) # Null reference
self=Ref ($Ref!val!0) {...} # Non-null reference
These indicate whether references are equal or distinct. The specific syntax is not meant for direct interpretation.
Field Values
Actor fields appear with the self. prefix:
self.balance=100, self.count=5
Internal Variables
By default, compiler-generated variables like $Res (result) and $Self are hidden. Use --show-internal to see them:
sector9 --verify --counterexample --show-internal file.sr9
When Counterexamples Are Not Available
Counterexamples may be missing or incomplete in some cases:
SMT Solver Limitations
Complex constraints or timeouts can prevent the solver from producing a model:
[1] Postcondition might not hold
...
(no counterexample available)
Workaround: Simplify the specification, add intermediate assertions, or increase the verification timeout.
Symbolic Values
Some values may appear as symbolic expressions rather than concrete numbers:
x=f(...)
Workaround: Add intermediate assertions to constrain the search space.
Permission Failures
Permission errors (missing reads/modifies) don't produce counterexamples because the contract is malformed before the solver runs.
Debugging Workflow Example
Consider this complete debugging session:
persistent actor TokenVault {
var supply : Nat = 1000;
var locked : Nat = 0;
public func lock(amount : Nat) : async ()
modifies supply
modifies locked
ensures supply + locked == 1000; // Conservation law
{
supply -= amount;
locked += amount;
};
}
Step 1: Run with counterexample
$ sector9 --verify --counterexample vault.sr9
Output:
[1] Postcondition might not hold
Assertion supply + locked == 1000 might not hold.
Counterexample summary
return: amount=1001, self.supply=..., self.locked=...
Step 2: Analyze
When amount=1001 and supply=1000, the subtraction supply -= amount would underflow (or in Nat semantics, trap). The postcondition cannot hold because the operation itself is invalid.
Step 3: Fix
Add entry_requires
and requires to rule
out invalid exported calls:
public func lock(amount : Nat) : async ()
entry_requires amount <= supply;
requires amount <= supply; // Cannot lock more than available
modifies supply
modifies locked
ensures supply + locked == 1000;
Using JSON for Programmatic Analysis
For CI/CD or scripted analysis, use --json with --counterexample:
sector9 --verify --json --counterexample file.sr9 | jq '.failures[0].counterexample'
Extract specific bindings:
# Get all variable values
jq '.failures[0].counterexample.models[].bindings[] | "\(.name)=\(.value)"'
# Get only local variables
jq '.failures[0].counterexample.models[].bindings[] | select(.kind == "local")'
See JSON Output and Counterexamples (--counterexample) for the complete schema.
Summary
- Run
sector9 --verify --counterexample file.sr9to get concrete failing values - Follow the three-step workflow: identify inputs, trace execution, determine fix
- Valid failing inputs indicate implementation bugs; invalid inputs need preconditions
- Counterexamples may be available for postconditions, preconditions, invariants, and assertions
- Use
--show-internalto see compiler-generated variables - Counterexamples may be unavailable for complex constraints or permission errors
- Use
--jsonfor programmatic analysis in CI/CD pipelines