spec_to_rest
Spec verification

Techniques

Edit on GitHub

SMT and bounded model checking, and which backend handles what

Last updated:

The engine runs on two solvers, and the split between them is the whole technique. Z3 decides first-order formulas over integers, strings, and arrays; Alloy searches a finite universe for the sets, relations, and reachability that first-order logic cannot pin down decidably. Each check is routed to whichever backend can actually decide it, with no overlap.

The proof obligation

Almost everything reduces to one question, asked per operation: given that the invariants hold before, does the operation's contract guarantee they hold after? Written out, that is the implication

invariants(s) and requires(in, s) and ensures(in, s, s', out) => invariants(s')

and the trick is to check it by trying to break it. The engine asserts the negation, that the pre-state is valid, the operation runs, and an invariant still fails afterward, then hands it to Z3:

entity Account {
  balance: Money
  invariant: balance >= 0
}

operation Withdraw {
  input: account_id: AccountId, amount: Money
  requires: account_id in accounts
  ensures: accounts'[account_id].balance = accounts[account_id].balance - amount
}
; assert the negation of "Withdraw preserves balance >= 0" and look for a model
(assert (>= (balance acc) 0))                          ; invariant holds before
(assert (member acc accounts))                         ; requires
(assert (= (balance_post acc) (- (balance acc) amt)))  ; ensures
(assert (not (>= (balance_post acc) 0)))               ; invariant broken after
(check-sat)

If Z3 finds a model, that model is a counterexample: a concrete balance and amount that drive the account negative, reported as invariant_violation_by_operation. If it cannot, no such state exists and the operation is safe. The same negate-and-search shape produces the other three findings, an unsatisfiable invariant set (contradictory_invariants), a self-contradictory precondition (unsatisfiable_precondition), and a precondition no invariant-respecting state can satisfy (unreachable_operation).

Z3, for first-order facts

Z3's value is that on its decidable fragments, linear arithmetic, strings, arrays, uninterpreted functions, an unsat result is a proof, not a sample. That covers the bulk of a spec: scalar and refinement invariants, arithmetic in postconditions, equality and membership. The translation models each entity field as an accessor function and the state as a domain predicate plus a mapping function, with pre-state and post-state as separate symbols, then states the invariants as definitions and the verification condition as an assertion. The exact SMT-LIB the engine emits, and how to dump and read it, are on the live verification pipeline page.

Alloy, for sets and time

Some properties sit outside what Z3 can decide: quantifying over subsets, transitive closure, whether a state is reachable through some sequence of operations. Those go to Alloy 6, which does bounded model checking, it searches a finite universe of a bounded number of entities and steps for a counterexample. The tradeoff is worth stating plainly: a clean Alloy result means no counterexample exists up to that scope, strong evidence rather than a universal proof, whereas Z3's unsat on a decidable fragment holds for all sizes. State-machine and temporal questions run through Alloy too in this pragmatic first version, whether some sequence of operations can reach a forbidden state, or deadlock.

Picking a backend

The router sends each check to the one backend that can decide it: first-order and arithmetic obligations to Z3, powerset, transitive-closure, and temporal ones to Alloy, with the two sets disjoint so nothing is checked twice or slips between them. A fuller temporal logic like TLA+ was considered for the time-based properties and left out as too heavy for this stage, the comparison page covers why. The exact routing rules are on the verification pipeline page.

On this page