← Back to AILP Home

Invariants

invariant clauses attach facts to loops. A loop invariant must hold before the first iteration and after each loop back-edge.

Syntax

func:count_down = int32(int32:x)
    requires x >= 0i32
{
    while (x > 0i32) invariant x >= 0i32 {
        x -= 1i32;
    }
    pass(x);
};

Multiple invariant expressions are comma-separated:

while (i < n) invariant i >= 0i32, sum >= 0i32 {
    sum += i;
    i += 1i32;
}

Entry and back-edge obligations

The compiler checks two points:

  1. Entry: the invariant must hold before the first possible iteration.
  2. Back-edge: after the loop body executes and control jumps back to the condition, the invariant must still hold.

v0.34.4 added Z3 entry obligations and --debug-contracts runtime entry/back-edge checks.

Runtime checking under debug contracts

When a fact is runtime-dependent and --debug-contracts is enabled, the compiler keeps a runtime guard. A failing runtime invariant follows the normal Result/failsafe path used by contract checks in that build mode.

Example from the v0.34.4 regression shape:

func:bad_entry = int32() {
    int32:i = 0i32;
    while (i < 1i32) invariant i == 1i32 {
        i += 1i32;
    }
    pass(i);
};

The invariant is false at entry, so the debug runtime check catches it before the body can repair it.

Purity rules

Invariant clauses must be boolean and solver-friendly. v0.34.4 locks two clear front-end diagnostics:

Practical pattern

Start with facts that are already visible at loop entry:

func:sum_to_n = int32(int32:n)
    requires n >= 0i32
{
    int32:i = 0i32;
    int32:sum = 0i32;

    while (i < n) invariant i >= 0i32, sum >= 0i32 {
        sum += i;
        i += 1i32;
    }

    pass(sum);
};

Then add stronger facts only when the verifier has enough local evidence.

See also