Skip to main content

Choosing the Right Assertion

Sector9 provides three assertion forms with distinct verification and runtime behavior. This guide helps you choose the right one.

Quick Decision

QuestionAnswer
Need runtime protection?Use runtime:assert or trap
Only documenting a proof step?Use assert
Marking an error state as impossible?Use trap
Need an error message?Use trap
Guarding a positive condition?Use runtime:assert

Comparison Table

Aspectassertruntime:asserttrap
VerificationProves condition trueProves condition trueProves condition false
RuntimeErased (no effect)Traps if falseTraps if true
Performance costZeroNon-zeroNon-zero
Error messageNoNoYes (optional)
Pure/lemma allowedNoNoNo

Decision Flowchart

Is this for runtime protection or proof documentation?

├─► Proof documentation only
│ └─► Use assert

└─► Runtime protection

├─► Guarding something that MUST be true?
│ └─► Use runtime:assert

└─► Marking an error condition as impossible?
└─► Use trap

When to Use assert

Use assert when you need zero-cost documentation or proof guidance. The verifier proves the condition, but no runtime code is generated.

Breaking Down Complex Proofs

public func spread() : async Nat
ensures result == 1;
{
let a = { x = 1; y = 2 };
let b = { z = 3 };
let c = { ...a; ...b };

// Document field values after spreading
assert c.x == 1;
assert c.y == 2;
assert c.z == 3;

1
}

Documenting Type Properties

public func checkBounds(n : Nat) : async ()
{
// Nat is always non-negative - verifier proves this
assert n >= 0;
}

Intermediate Proof Steps

public func bump(n : Nat) : async Nat
ensures result == n + 1;
{
var acc : Nat = 0;
acc += n;
assert acc == n; // Document intermediate state
acc += 1;
assert acc == n + 1; // Matches postcondition
acc
}

When to Use runtime:assert

Use runtime:assert for defense-in-depth: the verifier proves the condition, and runtime traps if it fails. This catches unexpected violations. On exported public methods, use entry_requires for the entry guard when outside callers must be rejected before method execution.

Guarding Division

public func div(a : Int, b : Int) : async Int
entry_requires b != 0;
requires b != 0;
ensures result == a / b;
{
runtime:assert b != 0; // Proved + runtime guard
a / b
}

Guarding Array Access

public func get(i : Nat) : async Int
reads data
entry_requires i < data.size();
requires i < data.size();
ensures result == data[i];
{
runtime:assert i < data.size(); // Bounds check
data[i]
}

Echoing Preconditions

public func process(opt : ?Nat) : async Nat
entry_requires opt != null;
requires opt != null;
{
runtime:assert opt != null; // Echo the precondition
switch opt {
case (?v) v;
case null 0;
}
}

When to Use trap

Use trap to mark error conditions as unreachable. The verifier proves the condition is false, and runtime traps if it becomes true.

Marking Impossible Branches

public func unwrap(opt : ?Nat) : async Nat
entry_requires opt != null;
requires opt != null;
{
switch opt {
case (?v) v;
case null {
trap true("Reached null case with non-null precondition");
};
}
}

Expressing Negative Invariants

public func withdraw(amount : Nat) : async ()
modifies balance
entry_requires amount <= balance;
requires amount <= balance;
{
trap amount > balance; // Underflow is impossible
balance := balance - amount;
}

Error Conditions with Messages

public func transfer(from : Nat, to : Nat, amount : Nat) : async ()
entry_requires from != to;
requires from != to;
{
trap from == to("Cannot transfer to self");
}

runtime:assert vs trap

These are proof inverses:

  • runtime:assert P - asserts P is true (traps if false)
  • trap P - asserts P is false (traps if true)

Choose based on how you think about the condition:

ConditionUse
"This must be true"runtime:assert
"This must never happen"trap
// These create equivalent proof obligations:
runtime:assert b != 0;
trap b == 0;

// Choose based on intent:
runtime:assert balance >= amount; // "Balance must cover amount"
trap amount > balance; // "Underflow must not happen"

Common Patterns

Precondition Echo Pattern

Echo preconditions with runtime:assert for defense-in-depth:

public func safeDiv(a : Int, b : Int) : async Int
entry_requires b != 0;
requires b != 0; // Contract for callers
{
runtime:assert b != 0; // Runtime guard
a / b
}

Proof Checkpoint Pattern

Use assert to document what the verifier knows at each point:

public func compute(x : Nat, y : Nat) : async Nat
entry_requires x > 0;
entry_requires y > 0;
requires x > 0;
requires y > 0;
ensures result > 0;
{
assert x > 0; // From precondition
assert y > 0; // From precondition
let sum = x + y;
assert sum >= x; // Nat addition property
assert sum > 0; // Follows from above
sum
}

Unreachable Branch Pattern

Use trap for branches that preconditions make impossible:

public func getFirst(arr : [Nat]) : async Nat
entry_requires arr.size() > 0;
requires arr.size() > 0;
{
if (arr.size() == 0) {
trap true("Empty array with non-empty precondition");
};
arr[0]
}

Restrictions

All three assertion forms are rejected in pure functions and lemmas:

// REJECTED - Error M0240
public pure func bad(x : Nat) : Nat {
assert x > 0; // Not allowed in pure functions
x
}

Use requires/ensures for pure function contracts instead.

The old() expression cannot appear in runtime:assert conditions:

// REJECTED - old() not allowed
public func bad() : async () modifies x {
x := x + 1;
runtime:assert x == old(x) + 1; // Error
}

// CORRECT - use local variable
public func good() : async () modifies x {
let prev = x;
x := x + 1;
runtime:assert x == prev + 1; // OK
}

Summary

  • Use assert for zero-cost proof documentation and intermediate steps
  • Use runtime:assert for defense-in-depth when guarding conditions that must be true
  • Use trap for marking error conditions as impossible, especially with error messages
  • All three are rejected in pure functions and lemmas
  • runtime:assert cannot use old() expressions