Skip to main content

Step Lemmas

Step lemmas break complex loop invariant proofs into manageable inductive steps that the SMT solver can verify.

The Inductive Gap Problem

When verifying loops, the verifier must prove that the loop invariant holds after each iteration. For simple arithmetic, this works automatically. For non-trivial mathematical relationships, the SMT solver often cannot bridge the gap between the current state and the next state.

Consider computing the triangular number (sum 1 + 2 + ... + n). The loop invariant states acc == triangular(i). After adding i + 1 to acc, we need to show acc == triangular(i + 1). This requires proving:

triangular(i) + (i + 1) == triangular(i + 1)

The SMT solver knows the definition triangular(n) == n * (n + 1) / 2 through the helper contract, but cannot always use that algebraic identity at the exact point where the loop needs it. A step lemma makes the relationship explicit at the call site.

The Step Lemma Pattern

Step lemmas follow a three-part structure:

  1. Proof helper: Define the closed-form mathematical formula
  2. Step lemma: Prove how the formula evolves from iteration i to i + 1
  3. Loop: Call the lemma before updating the accumulator
lemma-step.sr9
// Step lemma for loop invariant maintenance
module {
// Proof-only abstract helper defining the closed-form formula
public ghost abstract func triangular(n : Nat) : Nat
requires true;
ensures result == n * (n + 1) / 2;

// Step lemma: proves how triangular evolves at each iteration
public lemma triangularStep(i : Nat) : ()
ensures triangular(i) + (i + 1) == triangular(i + 1);
{
};

// Sum 1 + 2 + ... + n using the step lemma
public func sumToN(n : Nat) : Nat
ensures result == triangular(n);
{
var i : Nat = 0;
var acc : Nat = 0;

while (i < n) {
loop:invariant i <= n;
loop:invariant acc == triangular(i);

triangularStep(i); // Establishes: triangular(i) + (i+1) == triangular(i+1)
acc += i + 1;
i += 1;
};

acc
};
}

The key is line 24: triangularStep(i) establishes the fact that triangular(i) + (i + 1) == triangular(i + 1). Combined with the loop invariant acc == triangular(i), the verifier can prove the invariant holds after the update.

Anatomy of a Step Lemma

A step lemma has these characteristics:

PropertyDescription
ParametersCurrent loop variable(s)
PreconditionsUsually none (or bounds from the loop)
PostconditionRelates state at i to state at i + 1
BodyOften empty, or a short chain of lemma/axiom calls
Call siteInside the loop, before the state update

The postcondition captures the inductive step: given what holds at iteration i, what holds at iteration i + 1.

Parameterized Step Lemmas

Step lemmas can take additional parameters beyond the loop counter. This makes them reusable across different configurations:

step-linear.sr9
// Step lemma for linear progression with arbitrary step size
module {
pure func lin(start : Int, step : Int, n : Int) : Int { start + n * step };

public lemma linStep(start : Int, step : Int, i : Int) : ()
ensures lin(start, step, i) + step == lin(start, step, i + 1);
{
};

public func linear(start : Int, step : Int, n : Int) : Int
requires n >= 0;
ensures result == lin(start, step, n);
{
var i : Int = 0;
var acc : Int = start;

while (i < n) {
loop:invariant i >= 0;
loop:invariant i <= n;
loop:invariant acc == lin(start, step, i);
linStep(start, step, i); // Parameterized step lemma
acc += step;
i += 1;
};

acc
};
}

The linStep lemma takes three parameters: start, step, and i. It proves the relationship for any starting value and step size, not just specific constants.

Step Lemmas for Non-Obvious Identities

Some algorithms rely on mathematical identities that are not obvious to the SMT solver. Computing n^2 by summing odd numbers demonstrates this:

step-square.sr9
// Step lemma proving n^2 = 1 + 3 + 5 + ... + (2n-1)
module {
pure func square(n : Nat) : Nat { n * n };
pure func odd(i : Nat) : Nat { 2 * i + 1 };

public lemma squareStep(i : Nat) : ()
ensures square(i + 1) == square(i) + odd(i);
{
};

public func squareByOdds(n : Nat) : Nat
ensures result == square(n);
{
var i : Nat = 0;
var acc : Nat = 0;

while (i < n) {
loop:invariant i <= n;
loop:invariant acc == square(i);
squareStep(i); // square(i+1) == square(i) + odd(i)
acc += odd(i);
i += 1;
};

acc
};
}

The identity (i+1)^2 = i^2 + (2i+1) is the key insight. The step lemma squareStep encodes this, allowing the verifier to connect the loop invariant to the postcondition.

Call Placement

Usually call the step lemma before updating the accumulator:

while (i < n) {
loop:invariant acc == formula(i);

stepLemma(i); // First: establish the relationship
acc += update; // Then: update matches the lemma's postcondition
i += 1;
}

Calling after the update usually does not work because the lemma establishes facts about the pre-update state. The verifier needs those facts before reasoning about the assignment. If the lemma is about a post-update value, name that value first and call the lemma before the invariant check point.

When Step Lemmas Are Needed

Use step lemmas when:

  • The loop invariant involves multiplication or division
  • The formula has a closed form different from the iterative computation
  • The relationship between iterations is algebraic, not structural
  • The verifier fails with "postcondition might not hold" for a loop
  • Quantified loop invariants need a reusable one-step preservation fact

You do not need step lemmas when:

  • The invariant is purely additive (e.g., acc == i * constant)
  • The update matches the invariant structure directly
  • Simple bounds checking suffices
  • A local assertion already gives the solver enough information

Missing Step Lemma Example

Omitting the step lemma causes verification to fail:

step-missing_unsat.sr9
// Missing step lemma causes verification failure
module {
public pure trusted func triangular(n : Nat) : Nat
requires true;
ensures result == n * (n + 1) / 2;

public lemma triangularStep(i : Nat) : ()
ensures triangular(i) + (i + 1) == triangular(i + 1);
{
};

public func sumToN(n : Nat) : Nat
ensures result == triangular(n);
{
var i : Nat = 0;
var acc : Nat = 0;

while (i < n) {
loop:invariant i <= n;
loop:invariant acc == triangular(i);
// WRONG: Missing triangularStep(i);
// The verifier cannot prove: acc + (i+1) == triangular(i+1)
acc += i + 1;
i += 1;
};

acc
};
}

The verifier reports that the loop invariant might not be preserved. It knows acc == triangular(i) and acc' == acc + (i + 1), but cannot prove acc' == triangular(i + 1) without the explicit step lemma.

Multiple Step Lemmas

Complex loops may require multiple step lemmas to maintain different invariants:

while (i < n) {
loop:invariant sum == formula1(i);
loop:invariant product == formula2(i);

stepSum(i); // Maintains sum invariant
stepProduct(i); // Maintains product invariant

sum += next1;
product *= next2;
i += 1;
}

Each lemma focuses on one relationship, keeping the proofs modular.

Step Lemmas with Preconditions

Some step lemmas require preconditions from the loop context:

public lemma divStep(num : Nat, denom : Nat, i : Nat) : ()
requires denom > 0; // Required for division
requires i < num / denom;
ensures formula(i) + denom == formula(i + 1);
{
};

The preconditions must be provable at the call site. Loop invariants often establish the necessary facts. If the lemma uses division, include guards such as denom > 0; see division verification.

Deriving Step Lemmas

To derive a step lemma for a formula f(i):

  1. Write out f(i + 1) expanded
  2. Write out f(i) expanded
  3. Express the difference or relationship between them
  4. That relationship becomes the postcondition

For triangular numbers:

  • f(i + 1) = (i + 1) * (i + 2) / 2
  • f(i) = i * (i + 1) / 2
  • f(i + 1) - f(i) = i + 1
  • Postcondition: f(i) + (i + 1) == f(i + 1)

Common Mistakes

Wrong Variable in Lemma Call

// Wrong: using j instead of i
stepLemma(j); // Establishes facts about j, not the loop variable i
acc += update;
i += 1;

Step Lemma After Update

// Wrong: lemma called after the update
acc += update;
stepLemma(i); // Too late - verifier already failed to prove invariant
i += 1;

Missing Parameters

// Wrong: not passing all loop-relevant values
stepLemma(i); // If step depends on start/step, those must be passed

Summary

  • Step lemmas prove the inductive step that the SMT solver cannot derive automatically
  • Follow the pattern: pure function, step lemma, loop with lemma call before update
  • The postcondition relates state at iteration i to iteration i + 1
  • Call the lemma inside the loop, before updating the accumulator
  • Parameterize lemmas for reusability across different configurations
  • Derive lemmas by comparing f(i + 1) to f(i) algebraically
  • Add preconditions for division, bounds, or collection membership facts the lemma needs