Formal Verification: The Gap Between Perfect Code and Reality
Having been down this road [1], a big problem is that the people doing this tend to be too much into the theory and not enough into wanting programs not to crash. That&#x27;s some of the Coq world.<p>It doesn&#x27;t have to be this way. Microsoft&#x27;s work in this area led to the Static Driver Verifier [2], which was a huge success. Windows crashes due to kernel drivers breaking the rest of the kernel have pretty much disappeared.  They were once (Windows XP and earlier) responsible for over half of Windows crashes.<p>Most of the things you want to prove about programs to make sure they don&#x27;t crash are mathematically trivial. Detecting buffer overflows and off-by-one errors seldom requires elaborate proofs. If they do, your program design probably has a problem. Simple proofs can be handled by a prover which uses a Nelson-Oppen decision procedure.  This is a complete decision procedure for formulas which contain only addition, subtraction, multiplication by constants, Boolean operators, structures, and arrays.  This covers most data access operations.  It&#x27;s fully automatic and complete - the algorithm always succeeds within its problem space. You need one of those for the easy parts.<p>For the hard parts, we used the Boyer-Moore prover. (Sources are available and I brought it back to life on Github.[3] It runs 3000x faster than it did in 1982.)<p>The interface with the language is crucial to usability. Most efforts get this wrong. They put the verification stuff in separate files, or in comments.  It should be part of the normal code.  We built the verification language into the compiler, so you used the same source and the same syntax for compiling and verification. There were additional statements - ASSERT, ENTER, EXIT, etc.  The same compiler front end was used for both compiling and verification. The verifier itself ran off the byte code from the compiler front end.  All type and size issues had been decided at that point, so the verifier was looking at unambiguous operations such as &quot;pop two operands from stack, add in 16-bit twos complement arithmetic, push on stack&quot;. This captured the semantics of the program the same way the compiler did.<p>The user never sees the easy stuff. Every subscript use generates a bounds check assertion which has to be proved.  If the prover is able to prove the assertion true, the user doesn&#x27;t see it.  Only the errors are visible.  Another trick is that you don&#x27;t write &quot;assert(a and b and and d)&quot;, you write &quot;assert(a); assert(b);&quot;. Those generate separate proof goals, and if only one fails,the user gets error messages about only that one.  Also, when you write &quot;assert(a); assert(b);&quot;, the second one is proved under the assumption the first one is true.  This prevents cascading errors.<p>A key point here is that verification is meaningless unless you prove <i>all</i> the theorems. If you write &quot;assert(false)&quot; other errors go away, and you&#x27;re left with one simple error, which you cannot eliminate.
You don&#x27;t get to dismiss it. It&#x27;s possible to write an assertion that&#x27;s equivalent to &quot;assert(false);&quot;, such as &quot;assert(0 &gt; 1)&quot;; This is why I&#x27;m so negative on the arguments from some of the Rust crowd that a little undefined behavior is OK. It can blow at any seam.<p>When you needed to prove something hard, you wrote<p><pre><code>    assert(a);
    assert(b);
</code></pre>
where a could be proved by the Nelson-Oppen prover from the code before it.  B was chosen such that if you assumed b, the Nelson-Oppen prover could prove anything required past that point.  Now you had one error message, that the system couldn&#x27;t prove &quot;a implies b&quot;. So then someone had to try to prove that with the Boyer-Moore prover. The Boyer-Moore prover is mostly automatic, but often someone had to provide intermediate theorems to get to a hard one.  Those were all machine-checked. If this succeeded, the Boyer-Moore prover generated a theorem that a implies b for use by the verifier. Rerunning the program verifier would then succeed.  Such theorems could be reused with different variable bindings, so if the same problem came up again, it didn&#x27;t require more work with the Boyer-Moore system.<p>If you&#x27;re generating vast amounts of user-visible proof work, something is wrong.<p>The whole system I&#x27;m describing is now on Github.[4] It&#x27;s decades-old code, but I&#x27;ve ported about 80% of it to modern compilers. The Franz LISP to Common LISP port of the prover isn&#x27;t working well, and the author of that code died years ago. If anyone wants to work on that, go ahead.<p>[1] <a href="http:&#x2F;&#x2F;www.animats.com&#x2F;papers&#x2F;verifier&#x2F;verifiermanual.pdf" rel="nofollow">http:&#x2F;&#x2F;www.animats.com&#x2F;papers&#x2F;verifier&#x2F;verifiermanual.pdf</a>
[2] <a href="https:&#x2F;&#x2F;docs.microsoft.com&#x2F;en-us&#x2F;windows-hardware&#x2F;drivers&#x2F;devtest&#x2F;static-driver-verifier" rel="nofollow">https:&#x2F;&#x2F;docs.microsoft.com&#x2F;en-us&#x2F;windows-hardware&#x2F;drivers&#x2F;de...</a>
[3] <a href="https:&#x2F;&#x2F;github.com&#x2F;John-Nagle&#x2F;nqthm" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;John-Nagle&#x2F;nqthm</a>
[4] <a href="https:&#x2F;&#x2F;github.com&#x2F;John-Nagle&#x2F;pasv" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;John-Nagle&#x2F;pasv</a>