Laurel Language Designer Guide

2.2. Verification🔗

To achieve goal 1.2, enable proving properties through verification, Laurel has the following features.

2.2.1. Assertions🔗

An assert states a property that must hold at the point where it appears. An assertion is the basic unit of proof: everything else in this section is a way of making the facts an assertion needs available, or of stating such properties more conveniently.

procedure abs(x: int) returns (r: int)
{
    if x < 0 then { r := -x } else { r := x };
    assert r >= 0
};

Here the assertion holds on both branches, so it is discharged; had a branch left r negative, the assertion would report a failure.

2.2.2. Quantifiers🔗

Laurel supports universal (forall) and existential (exists) quantifiers in properties, written forall(x: T) => P(x) and exists(x: T) => P(x). They let a single property range over unboundedly many values.

procedure allNonNegativeSquares()
  opaque
{
    assert forall(x: int) => x * x >= 0
};

procedure someMultipleOf42()
  opaque
{
    assert exists(x: int) => x == 42
};

The two quantifiers correspond to the two analysis modes. A universal quantifier is what soundness (correctness) checking needs: to prove a procedure correct, its properties must hold for all inputs and all reachable states, so proving a forall establishes the property for every case. An existential quantifier fits bug finding (incorrectness) mode: exhibiting some state that satisfies a property is enough to witness that a situation is reachable, for example a state that violates an intended invariant. Because unrestricted quantifier instantiation is a common cause of slow verification, a forall may carry an explicit trigger, written forall(i: int) { P(i) } => …, that tells the solver which terms may instantiate it.

2.2.3. Old🔗

In a postcondition, old(e) denotes the value of the expression e in the procedure's pre-state, before the body ran. This lets a contract relate the final state to the initial one, which is how mutation is specified. The specification is written in a mutation-free style: old and the current value are both just expressions, and comparing them describes the effect of the mutation without the contract itself performing any mutation.

procedure increment(counter: Counter)
  opaque
  ensures counter#value == old(counter#value) + 1
  modifies counter
{
  counter#value := counter#value + 1
};

The postcondition counter#value == old(counter#value) + 1 captures the mutation performed by the body, yet it is a pure comparison between two values. A caller learns exactly how the field changed relative to its prior value without the contract mutating anything, keeping verification code erasable (see goal 7).