Browse documentation
Proving with agents
On this page
Give your AI agent this brief and the path of a proof repository as qp assemble writes it (From a repository to a proof),
or follow it yourself; QuickProof does not include the agents we use. qp assemble --templates DIR can put a
copy, with the project's names filled in, into every proof repository (The qp command).
Below, a spec is the C program that states a property (qp calls it a law), LAW is the name of one (its file
is harness/LAW.c), and LOGICAL is the Coq name of the repository's src/ directory (see _CoqProject).
Nothing the agent writes is trusted; the work counts only when qp verify accepts it ("Done"). The spec is not the
agent's to change: people review it, because it is exactly what the theorem states.
The theorems
For each spec LAW the file src/contracts/LAW.v ends with
Theorem LAW : harness_safe whole_prog.
Proof.
Admitted.whole_prog is the spec linked with the code it calls, and harness_safe whole_prog is VST's whole-program
judgement for it. Read through VST's adequacy theorem, it says that no run of the program has undefined behaviour in
CompCert's C semantics (x86-64), for every value the nondet_* calls may return (What a proof means has
the exact meaning). A failing gp_assert is undefined behaviour, so every assertion holds, and out-of-bounds
accesses, uses after free, signed overflow and the like are ruled out. gp_assume(c) cuts off the runs where c is
false. quickproof/coq/QuickProof/Contract.v defines the theorem, and Env.v there the specifications of
nondet_*, gp_assert, gp_assume, malloc and free. Specs write these two as qp_assert and qp_assume,
macros for gp_assert and gp_assume; the Clight, Env.v and older specs use the gp_ names.
Where things are
harness/LAW.c: the spec. Read it first.- The code under test: the C files named in
quickproof.conf. src/contracts/LAW.v: the contract. Everything above the bannerTHE THEOREMis generated (the program,whole_prog, and a check of the globals it leaves undefined). Then comes the theorem.src/<dir>/code.v: the Clight (CompCert's C dialect) of each C file, generated by clightgen and never edited. The functionfisf_fthere, its name is_f, its localiis_i, and clightgen's temporaries are_t'1,_t'2, and so on. Each C file is a unit, with a module insrc/units/that defines itsCS : compspecsand its programqp. Only the functions of the units named in a contract file's top comment are inwhole_prog._CoqProject:src/is the Coq libraryLOGICAL, sosrc/proof/Foo.visLOGICAL.proof.Foo.quickproof/: the framework at the commit the proof is checked with.quickproof/examples/(see itsREADME.md): worked examples, each with the code, the spec inharness/, a VST specification and a body proof per C function inproof/src/proof/, and the proof ofmainand of the theorem inproof/src/contracts/.rleis a loop over a byte buffer, withmalloc,nondet_bytes,qp_assume,qp_assertandfreein its spec.adler32has a functional model in Coq, a buffer split in two and three calls in one spec.find_byte, a search loop that exits early, is the smallest complete example.
What you may change
- Add Coq files under
src/, with specifications and lemmas insrc/proof/. - In
src/contracts/LAW.v, add your proof between the generated part and the bannerTHE THEOREM, and replace theAdmitted.of the theorem by a proof (usuallyProof. exact LAW_proved. Qed.).
Nothing else. In particular:
- Never change the statement of a theorem, anything above
THE THEOREM, the specs, the C code,quickproof/,quickproof.conf,_CoqProject,.gitmodules, or any generatedcode.vorsrc/units/*.v. - Never use
Admitted,admit,Axiom,Parameter,Conjecture, or anything else that assumes what it should prove. Only the standard axioms and VST's malloc library are accepted. - Never rewrite git history, move or delete the tag
contract, or commit changes outsidesrc/other than notes such asCOUNTEREXAMPLE.md. - Do not weaken a specification until it says nothing useful about the code. It only has to be strong enough to prove the fixed theorem, but it must be true of the code.
How to prove a property
-
Read the spec and the code. Find which C functions the spec calls, directly and indirectly.
-
For each of them, write a VST specification in
src/proof/<Name>.v, then prove its body. The file starts like the examples:Require Import VST.floyd.proofauto. Require Import VST.floyd.VSU. Require LOGICAL.units.<Unit>. Require Import LOGICAL.<dir>.code. #[export] Instance CS : compspecs := LOGICAL.units.<Unit>.CS. Definition Vprog : varspecs. mk_varspecs LOGICAL.<dir>.code.prog. Defined.A specification:
Definition f_spec := DECLARE _f WITH p: val, sh: share, bs: list Z PRE [ tptr tuchar, tulong ] PROP (readable_share sh; Zlength bs <= Int64.max_unsigned; Forall (fun b => 0 <= b <= Byte.max_unsigned) bs) PARAMS (p; Vlong (Int64.repr (Zlength bs))) SEP (data_at sh (tarray tuchar (Zlength bs)) (map (fun b => Vint (Int.repr b)) bs) p) POST [ tulong ] EX r: Z, PROP (0 <= r <= Zlength bs) RETURN (Vlong (Int64.repr r)) SEP (data_at sh (tarray tuchar (Zlength bs)) (map (fun b => Vint (Int.repr b)) bs) p). Definition Gprog : funspecs := [f_spec]. Lemma body_f: semax_body Vprog Gprog f_f f_spec. Proof. start_function. ... Qed.C types in VST:
size_tistulongwith valuesVlong (Int64.repr n);uint8_tistucharandintistint, both with valuesVint (Int.repr n);uint32_tistuint; a pointer touint8_tistptr tuchar. Memory:data_at sh t v p(initialised),data_at_ sh t p(any contents). -
Write every loop invariant before you prove the loop; most failed attempts come from a weak invariant. It must give the bounds every array access needs and keep what the postcondition needs. Use
forward_loop Inv break: Post, orforward_for_simple_bound n Invfor a countingforloop.forward_ifneeds a join postcondition when both branches continue. -
Prove
mainin the contract file againstmain_spec_std p(withDefinition p := whole_prog.). Start withstart_function. sep_apply (create_mem_mgr gv)., then use oneforward_callper call. The environment functions have the specificationsnondet_size_spec,nondet_u8_spec,nondet_u32_spec,nondet_bytes_spec,gp_assume_spec,gp_assert_spec, andmalloc_spec'/free_spec'under the names_quickproof_mallocand_quickproof_free. Agp_assertcall leaves the obligation that its condition is not zero, and that is where the property is proved. The examples show the whole pattern, including the compspecs ofwhole_progandchange_composite_envfrom the unit'sCS. -
Assemble the theorem as the examples do:
Lemma LAW_proved : harness_safe whole_prog. Proof. harness_safe_by_specs CS G main_post_TT. - reflexivity. - entry_shared (semax_body_main_std _ _ p _ _ body_main eq_refl). entry_shared F.body_f. (one per C function of whole_prog) entry_external (@external_ok_size CS). (one per environment function it calls) ... apply Forall_nil. Qed.Glistsmain's specification (QPmain_spec_ext' p tt main_post_TT), your function specifications and those of the environment functions the program calls.
Useful tactics: forward, forward_call, forward_if, forward_loop, Intros, Exists, entailer!, cancel,
sep_apply, list_solve, rep_lia, lia, Znth_map, Int64.unsigned_repr. Unsigned comparisons of size_t
appear as Int64.ltu; the examples show how to turn them into < on Z. Several properties, one library explains how one
body proof serves several specs.
Checking your work
Build everything at the root of the repository:
coq_makefile -f _CoqProject -o CoqMakefile
make -f CoqMakefile -j4The first build also builds the framework's library in quickproof/coq/QuickProof. grep -rn Admitted src
shows what is left to prove. After you change a file that others import, build again before you look at the files
that import it.
Interactive work is much faster than recompiling. Use any Coq environment that reads _CoqProject: coq-lsp or
VsCoq in an editor, or an MCP server for Coq such as rocq-mcp. Without one, put Show. in a proof and compile to
print the goals. Only qp verify is the acceptance check.
Done
The work is done when
quickproof/tools/qp verify .prints PASS after your changes are committed. It checks everything again on a fresh clone (The qp command). Only
proof sources (src/**.v) and notes may have changed since the tag contract. The regenerated Clight and every
theorem's statement must be unchanged. Print Assumptions may name only the standard axioms and VST's malloc
library. The check fails if the proof's own sources (src/ and the other files of _CoqProject, comments
excluded) contain any Admitted, admit, Axiom, Parameter or Conjecture, even in a lemma that no theorem
uses; only in a run/prebuild project, a vendored library, are they just listed.
Any other claim of success does not count. Run it at the end, and again after any change.
If the property is false
A property can be false. If a proof gets stuck because the code really can break the property, look for a concrete
input that does it, write it with the reason into COUNTEREXAMPLE.md, and stop. That is a result. Never weaken the
property, the spec or the code to get around it. If you can run qp on the project's workspace, qp check NAME LAW
runs the spec natively and under CompCert's interpreter, and qp fuzz NAME LAW --libfuzzer searches for a failing
input (The qp command).
Running your agent
The agent needs to read the repository, write files under src/, commit them with git, and run make, coqc
and qp verify with the toolchain (Coq 8.20.1, CompCert 3.15, VST 2.15) in its PATH. Allow nothing else. When
the toolchain runs in a container, run these commands through it, with the repository mounted at the same path
on both sides.
An agent can get stuck on a loop invariant or run out of budget.