Hand-proving a Hoare triple is illuminating once and tedious forever. The dream is to hand a program — annotated with its specification — to a tool, and get back either a green tick ("proved correct for all inputs") or a concrete counterexample. That dream is real, and it runs on two engines bolted together: a verification-condition generator that turns "is this program correct?" into a pile of pure logic formulas, and an SMT solver that decides whether those formulas are true.
The bridge between them is the
Recall the weakest precondition
To verify a routine
Take a routine that computes an absolute value, annotated with its contract and — because there is a branch, not a loop — needing no invariant:
Applying wp to the conditional against the postcondition
which after substitution is
A verification condition is a logic formula; deciding validity is the same as deciding whether its
negation is unsatisfiable. The workhorse for satisfiability is the
SAT solver — decide whether a purely Boolean formula can be made true — the canonical
But VCs are not purely Boolean. They talk about integers, reals, arrays and functions:
Z3 (Microsoft Research) is the best-known SMT solver; CVC5 and Yices are peers. Combining theories (the Nelson–Oppen framework) lets a single query mix arithmetic, arrays and functions at once — which is exactly what a VC over real code needs.
Two answers. First, worst case is not typical case: modern SAT/SMT solvers (CDCL — conflict-driven clause learning) exploit the structure of real formulas so effectively that instances with millions of variables routinely fall in seconds, even though a crafted adversarial instance could stall them. Verification conditions are highly structured, not random. Second, expressiveness is traded against decidability: pure linear integer arithmetic is decidable; add multiplication of variables and you reach the undecidable, so tools restrict what you may write, or return "unknown". A solver's honest possible answers are sat (here's a counterexample), unsat (proved), and unknown — the price of touching an undecidable fragment.
You rarely write VCs by hand; a verification tool sits between your annotated source and the solver:
| Tool | Role |
|---|---|
| Boogie | An intermediate verification language: front-ends compile to it, it generates VCs and calls Z3. The plumbing many others reuse. |
| Dafny | A verification-aware programming language (compiles to Boogie→Z3). You write code with requires/ensures/invariant/decreases and it proves them as you type. |
| Why3 | A platform with its own language (WhyML) that dispatches VCs to many back-end provers (SMT solvers and interactive ones). |
| Frama-C | Analyses real C annotated in ACSL; its WP plug-in generates weakest-precondition VCs for industrial C code. |
The consistent theme: annotate, generate VCs by wp, discharge with SMT. What the human still owes the tool is the creative part — the loop invariants and termination measures — because those cannot in general be inferred.
Automated verification is not magic correctness. Two gaps bite constantly. First, the loop
invariant is your job: the tool needs it to cut each loop into loop-free VCs, and if you supply a
weak or wrong invariant the proof simply fails (or, if it is too weak to imply the postcondition, gets
stuck) — the solver won't invent the right one for you. Second, and more dangerous, the
specification itself is trusted, not checked. If your ensures clause is wrong,
incomplete, or vacuously satisfiable, a proof against it certifies nothing useful — the code is proved to
meet a spec that doesn't say what you meant. "Verified" always means "verified against these
annotations", never "verified to do the right thing". Garbage spec in, green tick out.