Skip to main content

Contracts

Contracts attach preconditions, postconditions, and invariants to functions and loops. They are the bridge between "I think this holds" and "the compiler proved this holds."

Clause syntax at a glance

Verum accepts contract clauses in two equivalent shapes.

Bare form — each clause keyword stands alone on its own signature line. Each precondition takes its own requires; each postcondition takes its own ensures. This is the form used throughout the stdlib and the VCS conformance specs under vcs/specs/L1-core/proof/contracts/:

fn foo(...) -> T
requires pre1
requires pre2
ensures post1
ensures post2
{
body
}

where form — accepted for single-line signatures with one ensures:

fn abs(x: Int) -> Int where ensures result >= 0 {
if x >= 0 { x } else { -x }
}

Two gotchas the compiler enforces today:

  1. where requires does not parse — use the bare requires keyword on its own signature line.
  2. Each precondition / postcondition takes its own keyword; comma-joining on one line (e.g. requires a, b, c or ensures x, y) fails to parse. Repeat the keyword per clause.

requires — preconditions

fn divide(a: Int, b: Int) -> Int
requires b != 0
{
a / b
}

Multiple preconditions: repeat the requires keyword (no comma-joining on a single line).

fn middle(xs: &List<Int>, i: Int) -> Int
requires i >= 0
requires i < xs.len()
{
xs[i]
}

Every caller must establish the conjunction of all requires clauses. Failure becomes:

error[V3401]: precondition not established
--> src/foo.vr:5:5
|
5 | divide(10, input);
| ^^^^^^^^^^^^^^^^^ requires `input != 0`
= counter-example: input = 0

Preconditions compose naturally with refinement types. The above is equivalent to:

fn divide(a: Int, b: Int { self != 0 }) -> Int { a / b }

Prefer the refinement form when the precondition concerns a single parameter; prefer the requires clause when it spans several parameters or references external state:

fn transfer(from: &mut Account, to: &mut Account, amount: Money)
requires from.balance >= amount,
from != to
{ ... }

ensures — postconditions

fn abs(x: Int) -> Int
ensures result >= 0
ensures result == x || result == -x
{
if x >= 0 { x } else { -x }
}

Each ensures keyword takes one boolean expression; multiple clauses are conjoined by repeating the keyword. result refers to the return value.

Loop invariant and decreases

while lo < hi
invariant 0 <= lo && hi <= xs.len()
invariant forall i in 0..lo. xs[i] < key
invariant forall i in hi..xs.len(). xs[i] > key
decreases hi - lo
{
...
}
  • invariant holds at entry and after every iteration.
  • decreases is a well-founded measure that strictly decreases each iteration — proves termination.

Without an explicit decreases, the compiler tries to infer one from the loop shape (while i < n with i += 1). When inference fails, you supply it.

throw and the error frame

A function with throws(E) commits to a contract on its error cases:

fn parse_u32(s: Text) -> Int throws(ParseError)
ensures result >= 0
ensures result <= U32_MAX
{
let n: Int64 = s.parse()?;
if n < 0 || n > U32_MAX {
throw ParseError.OutOfRange;
}
n
}

forall / exists in contracts

Bounded quantifiers are first class:

fn all_positive(xs: &List<Int>) -> Bool
ensures result == (forall i in 0..xs.len(). xs[i] > 0)
{
xs.iter().all(|x| *x > 0)
}

Unbounded quantifiers (forall i: Int. ...) are allowed but may degrade proof performance; the solver will often need triggers.

old() — referring to prior state

In postconditions of methods that mutate, old(self.field) refers to the field's value at call-entry:

fn push(&mut self, x: T)
ensures self.len() == old(self.len()) + 1
ensures self[old(self.len())] == x
{ ... }

Contract inheritance

A protocol can declare contracts; implementations must satisfy them:

type Stack<T> is protocol {
fn push(&mut self, x: T)
ensures self.len() == old(self.len()) + 1;

fn pop(&mut self) -> Maybe<T>
ensures match result {
Some(_) => self.len() == old(self.len()) - 1,
None => self.len() == 0 && old(self.len()) == 0,
};
};

Every implement Stack<T> for X is required to satisfy the contracts; the compiler emits one obligation per method per implementation.

Contracts as documentation

Contracts that pass verification double as documentation. The docgen tool (verum doc) extracts them into the generated reference:

/// Transfers `amount` from `from` to `to`, assuming both accounts
/// are distinct and `from` has sufficient balance.
fn transfer(from: &mut Account, to: &mut Account, amount: Money)
requires from.balance >= amount
requires from != to
ensures from.balance == old(from.balance) - amount
ensures to.balance == old(to.balance) + amount
{ ... }

Separation logic (selected cases)

When contracts need to talk about disjoint memory regions, Verum uses a lightweight separation-logic fragment. This is handled under the hood by verum_verification::separation_logic — user-facing cases are limited to array partitioning and disjointness of mutable references:

fn swap_regions<'a>(a: &'a mut [Int], b: &'a mut [Int])
requires disjoint(a, b)
{ ... }

Most code does not need this; the common case is covered by mutable- reference aliasing rules.

See also