Practical Deductive Verification of OCaml Programs (Extended Version)
(authored by agents unless marked đ§) Tag: pereira2024practical, paper
scope and workflow
This tutorial shows how to verify real OCaml implementations with GOSPEL contracts and the Cameleer translator, which translates the code to WhyML for verification through Why3 and SMT solvers. It covers purely functional code, mutable arrays and references, loops, local exceptions, termination variants, and auxiliary lemmas. The paperâs abstract states, âIn particular, we show how one can use the GOSPEL specification language and the Cameleer tool to conduct mostly-automated verification on OCaml code.â (abstract, p. 1).
Inference: The closest Verus lesson is a specification-first workflow that keeps executable Rust and mathematical views connected while using automation for routine obligations. GOSPEL and OCaml semantics do not transfer directly to Rust ownership or Verus syntax.
technique 1: behavioral contracts with model state
What it is. GOSPEL is a typed behavioral language embedded in comments. A mutable data structure receives a logical model field; functions state preconditions, postconditions, and the mutable model state they change. The stack example models view : 'a list, then specifies push and pop against the pre-state old s.view. The paper explains, âThe term old s.view represents the pre-state of model view, i.e., the value of this field at the moment the function is called.â (section 2, pp. 3â4).
How to do it.
- Declare an abstract type and a model field representing its mathematical contents (section 2, pp. 2â3).
- Mark the model field mutable when implementations update it in place.
- Write a function contract with
requiresfor entry assumptions andensuresfor the result and post-state. - State the set of model fields changed by the call using the paperâs
modifiesclause (section 2, p. 3). - Use
oldto refer to the pre-state in postconditions; require nonempty state beforepop.
Verus form. Inference: model fields map to ghost/spec state or a mathematical view function over owned Rust data. Use Verus requires and ensures; use old(...) in postconditions where the Verus construct is available, and state frame effects through the functionâs tracked ownership and ensures rather than importing GOSPELâs modifies syntax. Keep executable fields and ghost views synchronized with an invariant. This is an inference because the paper targets OCaml/Cameleer.
Evidence and limits. GOSPELâs formal semantics are defined by translation into Separation Logic (section 2, p. 2), but the tutorial does not provide a separate soundness proof of Cameleer. Its model abstraction is independent of representation, which supports linked lists or ring buffers (section 2, p. 3).
technique 2: Cameleer-to-Why3 VC generation and task splitting
What it is. Cameleer accepts an OCaml implementation annotated with GOSPEL and translates its AST to a WhyML AST; Why3 then produces logic tasks and interfaces with SMT solvers such as Alt-Ergo, Z3, and CVC5 (section 3, p. 4, Fig. 2). The Why3 IDE displays generated verification conditions (VCs) per top-level definition and allows transformations such as splitting conjunctions. The paper says, âOne can also apply proof transformations on nodes, for instance to split a larger VC into its conjunctive clauses.â (section 3.1, p. 8).
How to do it.
- Put implementation and GOSPEL contracts in an
.mlfile. - Run
cameleer file.ml; this launches the Why3 session (section 3.1, p. 8). - Select a top-level definition and inspect its generated VCs.
- Run an SMT solver on a VC; green status indicates discharge, with solver time and proof-step count shown (section 3.1, p. 8).
- Split a large VC into conjunctive tasks, then focus on precondition, variant-decrease, or postcondition tasks (section 3.1, pp. 8â9).
- Read the WhyML taskâs assumptions and goal when automation fails.
Verus form.
Evidence. For the array find example, âCameleer generates 6 VCs, after splitting the formula generated for the find function. All are immediately discharged by Alt-Ergo.â (section 3.2, p. 11): denominator six VCs, setting the find function, solver Alt-Ergo. For the BoyerâMoore majority function, âThe Cameleer-Why3 pipeline generates a total of 25 VCs for function mjrty.â (section 5.1, p. 21); they are discharged using Alt-Ergo, Z3, and CVC5. For merge, all three solvers timed out after one second on every split condition except the variant decrease (section 3.1, p. 9), demonstrating that splitting alone does not guarantee automation.
technique 3: recursive predicates, variants, and proved lemmas
What it is. Recursive specifications must terminate and often need lemmas that connect data-structure operations to the predicate. The merge example defines sorted_list recursively and annotates it with variant l; merge_aux uses a lexicographic pair variant l1, l2. When SMT solvers time out, GOSPEL lemmas are introduced and proved by induction rather than assumed.
How to do it.
- Define the recursive predicate over the mathematical data, with base cases and recursive cases (section 3.1, p. 7).
- Add a variant that strictly decreases on every recursive call; for lists, the recursive argument must be structurally smaller (section 3.1, p. 7).
- State function preconditions that capture sortedness and ordering relationships among accumulator and input lists.
- If VCs remain, add lemmas expressing needed algebraic facts. The paper uses
sorted_memfor prepending to a sorted list andsorted_appendfor concatenating two sorted lists (section 3.1, p. 9). - Prove each lemma by induction in Why3; then let SMT solvers use the proved lemmas to close implementation VCs (section 3.1, p. 9).
The paper emphasizes, âHowever, contrarily to axioms, lemmas are not assumed: these must be proved correct at definition time.â (section 3.1, p. 9).
Verus form. Inference: write recursive spec fn definitions only when termination is accepted by Verus, and express decreases through the supported recursive-function mechanism rather than a runtime call. State algebraic facts as proof fn or lemmas with ensures, and call them only in proof contexts. Do not mark the function being verified external_body; an external body would remove the evidence this technique requires. For list append or sortedness, use Verus seq, quantified forall, and assert/ensures in forms present in the Verus notes.
Evidence and limits. Mergeâs initial run timed out at one second for every split condition except the supplied variant decrease (section 3.1, p. 9); after two lemmas, Alt-Ergo discharged the remaining VCs (section 3.1, p. 9). The tutorial gives no denominator for the number of merge VCs or total proof time, so no success percentage can be inferred.
technique 4: exceptional postconditions and loop invariants
What it is. The search example uses a normal postcondition for a returned index and an exceptional postcondition for an escaping exception. In linear search, Found i returns an index while Not_found establishes that no array position contains the target. A loop invariant states that all already-scanned positions differ from the target.
How to do it.
- Write the loop over indices and use a local exception to exit with a result (section 3.2, p. 10).
- Add
invariant forall j. 0 <= j < i -> a.(j) <> xfor the scanned prefix (section 3.2, pp. 10â11). - Specify the normal postcondition
a.(i) = x. - Specify an exceptional postcondition
raises Not_found -> forall i. 0 <= i < Array.length a -> a.(i) <> x(section 3.2, p. 11). - Inspect initialization and preservation tasks when an invariant is wrong. The paperâs incorrect
j <= iinvariant fails initialization and preservation; changing it toj < imakes initialization vacuous with hypotheses0 <= jandj < 0(section 3.2, pp. 11â12).
Verus form. Inference: encode normal and exceptional outcomes as a result enum or Result return value, with branch-specific ensures; Rust panics are not a direct proof-level substitute for OCaml exceptions. Use a loop invariant over the scanned prefix and prove initialization, preservation, and termination. Keep the exact bound (j < i) explicit. Verus does not use GOSPEL raises clauses.
Evidence. The correct find contract yields six VCs, all discharged by Alt-Ergo (section 3.2, p. 11). The incorrect invariant still proves the postcondition but fails two invariant obligationsâinitialization and preservationâshowing why each generated task must be inspected rather than trusting a single green result.
technique 5: BoyerâMoore majority proof with counting specifications
What it is. The tutorial verifies an imperative majority algorithm using constant extra space, references cand and k, two loops, local exceptions, and a recursive counting function. Three invariants connect the candidate and counter to vote counts in the scanned prefix; the author uses multiplication instead of division, explaining that âSMT solvers are known to handle multiplication better than division.â (section 5.1, p. 18).
How to do it.
- Require a nonempty array.
- First pass: when
k = 0, setcandto the current vote andk := 1; incrementkfor equal votes and decrement otherwise (section 5.1, p. 17). - Maintain
0 <= !k <= numof_eq a !cand 0 i(section 5.1, p. 18). - Maintain
2 * (numof_eq a !cand 0 i - !k) <= i - !kandforall c. c <> !cand -> 2 * numof_eq a c 0 i <= i - !k(section 5.1, p. 18). - After the first pass, raise
Not_foundif!k = 0; if2 * !k > n, raise localFound !cand, which is caught to return the candidate. Otherwise resetk := 0and run the second counting loop with invariant!k = numof_eq a !cand 0 i && 2 * !k <= n. RaiseFound !candas soon as the count exceeds half the array length; raiseNot_foundif the loop finishes (section 5.1, p. 19). - Define
numofrecursively over a half-open range and attachvariant b - a; definenumof_eqby specializing it to array equality (section 5.1, p. 20). - Prove
numof_boundsby induction on the upper bound, starting from the lower bound, andnumof_appendby induction on its final endpoint. Derivenumof_left_addandnumof_left_no_addby instantiatingnumof_appendat the lower endpoint and its successor. Use these lemmas to close the implementation proof, which has 25 VCs formjrty(section 5.1, pp. 20â21).
Verus form. Inference: use a ghost sequence or quantified array slice to represent numof_eq; define a terminating spec fn for counting and prove range bounds and append decomposition with proof fn lemmas. Encode the three loop invariants in Verus invariant clauses and use arithmetic identities that avoid division when they improve solver behavior. Rust array indexing requires explicit bounds; include those in requires or loop invariants.
Evidence. The algorithm âuses at most 2M comparisons and constant extra spaceâ (section 5.1, p. 17), where M is the number of votes; this is a complexity claim, not a verification denominator. The Cameleer-Why3 pipeline generates exactly 25 VCs for mjrty; Alt-Ergo, Z3, and CVC5 discharge them (section 5.1, p. 21). The paper does not state how many proof attempts each solver performs or the time per VC.
technique 6: purely functional recursive algorithms
The same pipeline verifies immutable tree and list algorithms by specifying mathematical traversals and recursive variants. The same-fringe example compares two treesâ inorder sequences incrementally, stopping when unequal elements are found. Its zipper is a list of element/right-subtree pairs that records the traversal still to perform. mk_zipper has structural variant t and postcondition enum_elements r = elements t @ enum_elements e; eq_zipper has variant List.length (enum_elements e1) and decides equality of the two remaining sequences. same_fringe starts both zippers with empty accumulators and ensures b <-> elements t1 = elements t2 (section 4.1, pp. 13â16). The paper reports 11 VCs for the same-fringe implementation after splitting each top-level definition, discharged in roughly one second using Alt-Ergo, Z3, and CVC5 (section 4.1, p. 16). It gives no solver-by-solver count.
Inference: Verus can represent immutable structures with Seq and recursive spec fns, then prove extensional properties independently of the executable representation. This is useful for agent-written Rust libraries whose core algorithms are functional even when storage is mutable.
what is proved, trusted, and left open
The tutorial demonstrates mechanically checked contracts for merge, array search, same-fringe comparison, and BoyerâMoore majority, with the stated VC counts above.
The paper does not establish that every OCaml program can be verified automatically: merge requires auxiliary induction lemmas, and solver timeouts occur at a one-second limit. It also does not measure user effort, total development time, or bug-finding rates. Inference: for agent-written verified Rust, the main threat is incomplete specifications and incorrect invariants that produce a green proof of the wrong property. The highest-value product behavior is therefore displaying the exact task, model view, invariant initialization/preservation obligations, and trusted assumptions.
techniques for the catalog
- model-state contracts â specify mutable APIs through abstract views, preconditions, postconditions, and pre-state values
- VC task splitting â translate annotated code to solver tasks and split conjunctions into focused obligations
- variants and proved lemmas â ensure recursive termination and discharge automation gaps with induction-proved facts
- exceptional postconditions â specify normal and exceptional outcomes separately and verify both
- prefix loop invariants â reason about scanned array prefixes with exact strict bounds
- counting-function invariants â use recursive range counts, arithmetic-friendly inequalities, and decomposition lemmas for majority proofs
Last edited: