In previous work, Charguéraud introduced a framework for the verification of imperative higher-order programs, based on characteristic formulae (CF). Given a source-level program, the approach allows the user to state a specification for it, in the style of Separation Logic [22], and prove the specification using the full power of a proof assistant. It has proved successful in verifying robust and modular specifications for non-trivial programs [6], and even establishing complexity results [7].
The key component of such a framework is a function that produces, from a source-level program
, its characteristic formula
. Applying the logical predicate
to an environment
, a pre-condition
and a post-condition
yields the proposition
, which implies program
admits
as a pre-condition and
as a post-condition, in environment
. The user is left with the task of proving the goal
using specialised CF tactics alongside general-purpose tactics in an interactive theorem prover.
Charguéraud’s work is realised in a tool named CFML, where (a subset of) OCaml is the language of the certified programs, and Coq is the proof assistant that hosts the characteristic formulae. Only part of the soundness theorem for CFML has been proved in Charguéraud’s Coq formalisation.
In this paper, we describe how a CF framework has been constructed and proved sound for the entire CakeML language [26], including its exception mechanism and I/O features. CakeML is a substantial subset of Standard ML, with the notable feature that its compiler has been verified (in the HOL4 proof assistant). In addition to capturing language features not modeled in CFML, we give this framework a fully verified soundness theorem. The entire development is formalised in HOL4, which also plays the role of the proof assistant hosting the characteristic formulae. Though tactic details are not the main topic of this paper, we also provide HOL4 tactic support for our CF framework, just as CFML provides Coq tactics to support the proof of
theorems.
This paper’s material goes beyond previous work on characteristic formulae and CFML in the following ways:
-
We give a mechanised proof of soundness of characteristic formulae with respect to CakeML’s formal semantics (Sect. 2). By way of contrast, CFML’s soundness proof is mostly performed outside of Coq.
-
We support additional language features, such as I/O (Sect. 3) and exceptions (Sect. 3.2). This makes our framework go beyond CFML, and thus able to handle all features of the CakeML programming language.
-
We implement technology to make proofs using characteristic formulae interoperate with the existing synthesis tool for CakeML, namely the proof-producing translator from HOL functions to CakeML (Sect. 4).
As an appetiser, in Fig. 1 we show the code for a simple implementation of the Unix cat program, that we are able to verify using our framework. The specification for cat, proven correct in our framework, and thus a HOL4 theorem, is given in Fig. 2. The main steps of the cat proof are described in Sect. 5.
1.1 Background on CF
This subsection and the next one provide background on CF and CakeML. Readers familiar with these topics can skip ahead to Sect. 1.3.
Characteristic formulae, as introduced in Charguéraud’s PhD thesis [4], are essentially total correctness Hoare triples for ML-style functional programs. The key component of any CF framework is a function
that produces, from a source-level expression
, the expression’s characteristic formula
. Applying
to an environment
, pre-condition
and post-condition
yields a proposition
, which implies program expression
can have
as a pre-condition and
as a post-condition, in environment 
While the
function is the main workhorse behind any CF framework, most user-proved specifications are stated in terms of a Hoare-triple-like judgement for functional applications,
, written with Hoare-triple notation. The intuition is that
is true if the application of function-value
to curried arguments
admits
as a pre-condition and
as a post-condition. An example of a specification stated in terms of
is shown in Fig. 2.
Charguéraud’s initial version of CF [5] only applied to pure ML programs. Charguéraud has since extended his approach to support reasoning about imperative stateful ML programs in a style inspired by separation logic and its frame rule [6]. More recently, Charguéraud and Pottier have verified amortized complexity results using CFML [7]. The version we have ported to CakeML is based on Charguéraud’s framework for imperative stateful ML programs, but without support for proofs about complexity results.
In Charguéraud’s implementation of CF, called CFML, the mechanism for generating characteristic formulae from OCaml programs, i.e., the
function, is external to the proof assistant (Coq), and the translation from OCaml to Coq is not completely transparent, e.g., it translates the OCaml’s fixed-size int type to the mathematical integers in Coq. The soundness theorem for CFML has been proved on paper using an idealised semantics for a subset of OCaml. In contrast, our CakeML formalisation of CF models all formal entities in the logic of the proof assistant (HOL4 in our case) and the key theorem, i.e., soundness, is proved as a theorem inside the proof assistant.
1.2 Background on CakeML
The original goal of the CakeML project, as outlined in the first CakeML paper [18], was to provide a fully proof-producing code generation tool (code extraction tool) that given ML-like functions in higher-order logic (HOL) automatically produces equivalent executable machine code. The CakeML translator [18] is a proof-producing tool which generates CakeML code from functions in HOL. The output of the translator can then be input into a verified compiler [15, 26] that transforms CakeML programs to observationally compatible machine code. The verified CakeML compiler function was bootstrapped in logic using the fully proof-producing work-flow mentioned above [15].
As the compiler is maturing, the focus of CakeML project is shifting to the task of developing a general ecosystem of tools around the CakeML language. This is where CF technology comes into the picture. Our CF formalisation provides a verification framework that enables users to prove correctness theorems for imperative CakeML programs that use any of CakeML’s language features, e.g., references, arrays, exceptions and I/O. One can, of course, prove correctness theorems directly over the CakeML semantics. However, such direct proofs would be incredibly tedious for anything but very simple programs.
The formal semantics of the CakeML language is central to its CF framework and the CF framework’s soundness proof. Figures 3 and 5 provide some detail of CakeML’s operational semantics, which we write in the functional big-step style [20]. Figure 5 shows the definitions of the datatype for the deeply embedded CakeML values that the semantics operates over. Figure 3 shows a few cases of the expression evaluation function
. The figure includes the case of function application
, i.e., application of expression
to expression
, and shows the semantics, using the helper function
, of applying a non-recursive
value to an argument. For this application, the environment
from the
value is extended to map the variable
to value
. Before evaluation enters the expression from the
a clock is checked and decremented, following the style of functional big-step semantics [20]. In the semantics, each function is only applied to one argument at a time.
1.3 A Tour of the Material
The remainder of this section provides a brief tour of the contributions of this paper: the soundness theorem, our extensions for I/O and exceptions, and our integration of the CakeML CF technology with our existing CakeML proof tools.
We formalise the theorem of soundness of CF with respect to the CakeML semantics. In CFML, the soundness proof is only captured on paper, using idealised semantics for a subset of ML, and the Coq library uses axioms in the places where it would relate to the language semantics. In contrast, we were able to implement an axiom-free CF library for the whole CakeML language, and perform a mechanical proof of soundness, using CakeML’s pre-existing semantics.
This not only validates the CF approach introduced by Charguéraud, but also shows that it is flexible as well as extensible. Although CakeML’s semantics were not designed with CF in mind, we could directly reuse the CakeML language without any modification, and we were able to carry out the proofs without any particular issue (although some technical details differ from the paper proof). Moreover, as detailed in Sect. 3, we could extend the approach to handle new language features that are not supported by CFML.
The soundness theorem, which justifies proving properties about a characteristic formula to give equivalent properties about the program itself, is stated as follows. If the characteristic formula for the deeply embedded expression
(and environment
) holds for some shallowly embedded pre-condition
and shallowly embedded post-condition
, i.e.,
, then, starting from a state satisfying
is guaranteed to successfully evaluate in CakeML’s functional big-step semantics [20], and reach a new state
and value
satisfying
. Here
converts a CakeML state into a representation to which one can apply separation logic connectives, and
asserts disjoint union:
.
This mechanised proof eliminates the last bits of paper proof that need to be trusted in CFML. Section 2 details the main steps leading to the proof.
We extend the CF framework introduced in CFML to handle two new language features: exceptions, and I/O through CakeML’s foreign-function interface (FFI). These extensions are proved sound with respect to the CakeML semantics, and neatly make our framework able to handle all features of the CakeML programming language.Footnote 1
The extension which adds support for I/O is implemented by carefully modifying the
function, shown in the soundness theorem above. We modified the
function so that it makes visible the state of the FFI in the pre- and post-conditions. There were numerous tricky details to get right in the definition of
because the design goal was to make I/O reasoning local in the style of separation logic. Our support for I/O is local in that the proof for a piece of code which only uses, say, the print-to-stdout FFI ports does not impose any assumptions on the behaviour, state, or even existence of other FFI ports, e.g., ports for reading-from-stdin. In the spirit of separation logic, our framework allows combining different assertions about the FFI using CF’s equivalent to the separation logic frame rule. Section 3 provides details on how we modified
to make the FFI available in CF proofs.
Support for exceptions is implemented by making the post-conditions differentiate whether the result is a normal return with a value or a value raised as an exception. The new framework is able to reason about exception handling code. Section 3.2 explains how exceptions are supported and the effect their introduction had on the proofs.
With these extensions our framework covers all of CakeML’s language features and makes it possible to develop a verified standard library for CakeML with complete specifications for library functions that perform I/O or must raise exceptions in certain circumstances. For example, our cat implementation has a routine for opening files, called openIn (whose specification is shown in Fig. 4). A call to the CakeML function for openIn raises an exception if the file could not be opened, e.g., if there is no file at the given path. More precisely,
describes whether a file exists in
with name
, and the BadFileName exception is raised when no file could be found.
In compiled CakeML code, the actual system call for opening a file is handled by a short stub of C code that is attached to the external side of CakeML’s FFI. If an error occurs, the C code signals failure via the return value for the FFI call and, on the CakeML side, the library routine raises the relevant exception on receiving the error code from the C stub. At present, the external C code is unverified and we just make assumptions about its effect on the rest of the world. In the future, we aim to provide verified external assembly stubs that can replace the current unverified C code.
We integrate the CF framework into the CakeML ecosystem by making it interoperate with an existing synthesis tool, namely the automatic translation from HOL functions into CakeML. This tool [18] essentially implements a proof-producing extraction mechanism: given a function in higher-order logic (HOL), the tool generates CakeML code along with a proof that the produced code correctly implements the HOL function with respect to CakeML’s semantics. As HOL functions are pure, the translator is essentially limited to producing purely functional CakeML code.Footnote 2
At present, the most important use of this translation tool is in bootstrapping the verified CakeML compiler, where we now benefit from CF. The translation tool is used to generate CakeML code for the CakeML compiler’s implementation. The compiler is defined as functions in HOL, so before we can run the compiler on itself, we need to transform the compiler definition into the source language of the compiler, i.e., CakeML abstract syntax. CF comes into the picture because the translation tool can only produce pure functions. Previously we had to manually verify low-level I/O code that reads the input and passes it to the compiler function, and separate code that prints the result of running the CakeML’s compile function. By making the CF and translation tools able to build on each other’s results, we have replaced the difficult manual I/O code proofs by understandable CF proofs about I/O.
The bootstrap has thus far benefited from automatic conversion of translator produced results to CF theorems. The bridge between them also works in the other direction: proved results from CF can be used in the translator. Since the translator essentially only deals in pure functions, the CF-verified programs have to implement a pure interface in order to fit the translator. Such programs are not necessarily pure themselves: they can allocate memory, and use imperative structures and algorithms. We plan to make use of the CF-to-translator direction in the future to provide more efficient drop-in replacements for parts of the bootstrapped compiler. These replacement parts would be verified using CF, and replace the code produced by the translator. The register allocator is a particular example that we believe would benefit from using an imperative-style implementation instead of the current automatically generated pure functional implementation.
Section 4 provides details on how we have connected the translation tool and the CakeML CF framework.
All our developments were carried out in the HOL4 theorem prover, and have been integrated into the main CakeML repository. They are available online at https://cakeml.org and https://code.cakeml.org under the characteristic sub-directory.