Nitpick Formal Verification & Design by Contract

Nitpick’s fundamental philosophy is the rejection of unsafe behavior. To achieve this, it deeply integrates with the Z3 SMT solver to mathematically prove the correctness of the code before it is allowed to execute.

1. Static Assertions

The assert_static builtin allows developers to encode compile-time logic checks. If the expression evaluates to false, compilation immediately halts.

assert_static(1i32 == 1i32);

2. Formal Proofs (prove)

The prove keyword interacts directly with the Z3 verification backend (when the --verify compiler flag is set). It forces the SMT solver to construct a mathematical proof that the subsequent expression holds true across all possible control flows and variable states.

If the solver finds a path where the expression is false, compilation fails. The compiler extracts a counterexample showing the failing variable assignments (visible via --prove-report).

int32:x = get_val();
if (x > 0) {
    prove(x != 0); // Mathematically verified at compile time.
}

2.1 Path Condition Accumulation (v0.66.1)

The prove keyword is path-condition-aware: branch guards from enclosing if, while, and other control flow are accumulated and asserted as Z3 axioms before checking the proof obligation. This means prove(x != 0) inside if (x > 0) automatically benefits from the guard x > 0.

3. Loop Invariants (invariant)

Loop constructs (loop, while, till, when) accept an optional invariant clause that specifies conditions the verifier must prove hold at every iteration.

Rules<int32>:r_pos = { $ > 0i32 };
limit<r_pos> int32:sum = 1i32;

while (sum < 100i32) invariant sum > 0i32 {
    sum = sum + 1i32;
};

When compiled with --verify-contracts, the compiler proves the inductive step: given that the invariant holds at the start of an iteration and the loop condition is true, the invariant still holds at the end of the iteration.

4. Limit Rulesets

The limit<Rules> keyword allows developers to bind strict structural constraints to types. These constraints are heavily analyzed by the verifier to ensure physics boundaries or contract bounds are not breached during runtime execution.

(See contracts_limits_specs.txt for extensive examples on Rules limits and Design by Contract features like requires and ensures.)

5. Z3 Borrow Checker Integration (v0.66.6)

The borrow checker leverages Z3 to prove index disjointness for array borrows. When two mutable borrows ($$m) target the same array but use index variables constrained by different limit<Rules>, Z3 can prove the indices are always unequal, suppressing false-positive aliasing errors.

Rules<int32>:EvenIdx = { $ % 2i32 == 0i32 };
Rules<int32>:OddIdx  = { $ % 2i32 == 1i32 };

func:update_interleaved = int32(
    limit<EvenIdx> int32:i, limit<OddIdx> int32:j,
    int32[100]:arr
) {
    $$m int32:a = arr[i];   // mutable borrow at even index
    $$m int32:b = arr[j];   // Z3 proves i != j → no aliasing error
    pass(a + b);
};

6. Verification Compiler Flags

Flag Purpose
--verify Enable Z3 Rules/limit verification
--verify-contracts Verify requires/ensures/invariant contracts
--verify-overflow Verify integer arithmetic overflow
--verify-concurrency Verify data race & deadlock freedom
--verify-memory Verify use-after-free & recursion bounds
--verify-level=N Verification depth (0=rules, 1=+assertions, 2=+contracts, 3=all)
--smt-opt Enable SMT-guided optimizations
--smt-timeout=N Per-query Z3 solver timeout in ms (default: 5000)
--prove-report Emit prove/assert_static outcomes with counterexamples
--debug-z3 Dump SMT-LIB2 for proof obligations

7. Verification Backends

Nitpick has two complementary verification systems:

(See VERIFICATION_BACKENDS.md in the Nitpick repository root for the full architectural documentation.)