A compiler for the monadic translation of Haskell programs to Coq that uses the Free
monad as presented by Dylus et al. to model partiality and other ambient effects.
This compiler was originally developed as part of a bachelor's thesis. The compiler has been extended with additional features and its architecture changed compared to the version presented in the thesis but the explanation of the monadic translation in the thesis is still up to date.
The compiler's source code is documented using Haddock. The documentation is automatically build by our CI pipeline and published here.
Additional documentation can be found in the doc/
directory.
The Free Compiler is written in Haskell and uses Cabal to manage its dependencies. To build the compiler, the GHC and Cabal are required. To use the Coq code generated by our compiler, the Coq proof assistant must be installed. The compiler has been tested with the following software versions on a Debian based operating system.
In order to use the base library, the Coq files in the base library need to be compiled first. Make sure to compile the base library before installing the compiler. We provide a shell script for the compilation of Coq files. To compile the base library with that shell script, run the following command in the root directory of the compiler.
./tool/compile-coq.sh base
Note: If you add or remove files from the
base
library (or any other directory that contains Coq code that you wish to compile using the script above), the automatically generated Makefile needs to be updated. For this purpose the script provides the command line option--recompile
../tool/compile-coq.sh --recompile base
First, make sure that the Cabal package lists are up to date by running the following command.
cabal new-update
To build and install the compiler and its dependencies, change into the compiler’s root directory and run the following command.
cabal new-install freec
The command above copies the base library and the compiler’s executable to Cabal’s installation directory and creates a symbolic link to the executable in
~/.cabal/bin
.
To test whether the installation was successful, make sure that ~/.cabal/bin
is in your PATH
environment variable and run the following command.
freec --help
If you want to run the compiler without installing it on your machine (i.e., for debugging purposes), execute the following command in the root directory of the compiler instead of the freec
command.
cabal new-run freec -- [options...] <input-files...>
The two dashes are needed to separate the arguments to pass to the compiler from Cabal’s arguments.
Alternatively, you can use the ./tool/run.sh
bash script.
./tool/run.sh [options...] <input-files...>
Besides invoking Cabal and forwarding its command line arguments to the compiler, the script also sets the -Wwarn
flag of the GHC automatically.
This overwrites the -Werror
flag of the GHC that is set in addition to the -Wall
flag by default.
The default configuration causes compilation to fail when there are unhandled warnings in the source code.
As a result, the CI pipeline rejects commits that introduce such warnings.
While it is important that no warnings are reported in a production setting, fatal warnings can be disrupting when debugging.
Thus, we recommend using the ./tool/run.sh
script during development and running ./tool/full-test.sh
(which uses the default settings) once before pushing your local changes.
To compile a Haskell module, pass the file name of the module to freec
.
For example, to compile the examples from the Data.List
module run the the following command.
freec ./example/Data/List.hs
In order to compile multiple modules which import
on each other, multiple file names can be passed as an argument.
freec ./example/Data/List.hs ./example/Data/Function.hs
Both commands above print the generated Coq code directly to the console.
See the --output
option below for how to write the generated Coq code into files instead.
By default generated Coq code is printed to the console.
To write to a file instead, specify an output directory using the --output
option.
A file X/Y/Z.v
is placed into the output directory for every module X.Y.Z
that is compiled.
For example, the following command creates the files example/generated/Data/List.v
and example/generated/Data/Function.v
freec -o ./example/generated ./example/Data/*.hs
In addition to the .v
files .json
files are generated as well.
The JSON files contain the module interfaces for the translated modules.
The module interface files can be used to import modules that have been translated already without specifying them on the command line.
For example, if Data.List
and Data.Functor
have been translated already, the ListFunctor
example can be compiled on its own since its imports can be served from the module interface files.
freec -o ./example/generated ./example/ListFunctor.hs
The compiler searches for module interfaces in the output directory (see --output
option) and the current working directory by default.
If you want to compile modules that import files that have been written to a different output directory, you can use the --import
command line option to specify additional paths to search for modules interfaces in.
For example, if Data.List
and Data.Functor
have been translated already and their output has been written to ./examples/generated
, you can translate ListFunctor
and print its output to the console as follows.
freec -i ./example/generated ./example/ListFunctor.hs
To add multiple import paths just add one --import
option for each path.
There can be arbitrarily many import paths and the --import
and --output
options can be mixed.
./tool/run.sh -o <dir0> -i <dir1> -i <dir2> … <input-files...>
In the example above, the compiler would search for a module interface file first in the current working directory and then in the directories in the order they have been specified on the command line (i.e., first .
, then <dir0>
, then <dir1>
, then <dir2>
and so on).
Predefined data types and operations are not build directly into the compiler but part of the base library that accompanies the compiler.
The compiler uses the same mechanism that is used to load module dependencies to load modules from the base library, i.e., module interface files.
In contrast to automatically generated module interface files, the base library does not use the JSON file format but TOML since the module interfaces of the base library are maintained manually and TOML is a more user friendly format.
The module interface file format is documented in doc/ModuleInterfaceFileFormat.md
.
In order for the compiler to locate the Prelude.toml
module interface file, the location of the base library must be known.
If the compiler is installed as described above or executed using cabal
, it will usually be able to locate the base library automatically.
Otherwise, it may be necessary to tell the compiler where the base library can be found using the --base-library
option.
freec -b ./base ./example/Data/List.hs
When an output directory has been specified using the --output
option and the output directory does not contain a _CoqProject
file, it is created automatically.
The _CoqProject
file tells Coq where to find the compiler’s base library and assigns the logical name Generated
to the directory that contains the generated Coq code.
The file has the following format where <base-library>
is the path to the base library (see --base-library
command line option).
-R <base-library> Base
-R . Generated
The logical names Base
and Generated
are used in the generated import sentences.
For example, a generated file could contain the following lines.
From Base Require Import Prelude.
From Generated Require Export Data.List.
You can safely edit the _CoqProject
file or supply your own configuration as long as Coq is still able to locate the .v
files under their logical prefixes.
The compiler will not overwrite your changes since the _CoqProject
file is only written to if it does not exist.
If you don't want the _CoqProject
file to be generated at all, you can add the --no-coq-project
flag.
However, note that you may not be able to compile the generated Coq code if the _CoqProject
file is missing.
The main goal for the translation of Haskell code to Coq is to prove properties of the Haskell program in Coq. In order to do so, we have to formulate the properties to prove first. Due to the overhead involved with our translation, stating propositions in Coq directly is very tedious. Therefore, we provide a mechanism for deriving Coq properties from QuickCheck properties. This allows us to state the proposition in Haskell instead of Coq.
Consult doc/ProvingQuickCheckProperties.md
for more details and examples.
By default the compiler does support a limited subset of the Haskell programming language only.
There is experimental support to relax some of those restrictions.
Add the --transform-pattern-matching
command line option to automatically transform the input modules using a pattern matching compiler library for haskell-src-exts
before they are translated by our compiler.
For example, the PatternMatching
example can be translated as follows.
freec --transform-pattern-matching ./example/PatternMatching.hs
Consult doc/ExperimentalFeatures/PatternMatchingCompilation.md
for more details and examples.
We recommend using the following additional software during development. Both of these tools are used to make sure that we are using a consistent code style throughout the project and are described in more detail in the next section below.
The versions mentioned above are the versions used by our CI pipeline.
Brittany is a code formatter for Haskell. It can be installed via Cabal as follows.
cabal new-install brittant
The CI pipeline runs brittany
on all Haskell source files in the src
and example
directories and compares its output with the committed files.
If there are Haskell source files that have not been formatted using brittany
, the CI pipeline fails.
The same check is performed by the ./tool/check-formatting.sh
and ./tool/full-test.sh
scripts.
There is Brittany support for various editors (see also Editor Integration). If your editor is not supported, we are also use the following shell script that we provide.
./tool/format-code.sh [files...]
If no files are specified all Haskell source files in the src
and example
directory are formatted by default.
Note that the script overwrites the formatted files.
Thus, you should create a backup beforehand by git add
ing your changes, for example.
Of course Brittany is not perfect. Among others, data type declarations are not formatted at the moment. So don't rely entirely on the output of our automatic tests and manually check your code nonetheless.
HLint is a tool that gives suggestions on how to improve Haskell source code. It can be installed via Cabal as follows.
cabal new-install hlint
The CI pipeline runs hlint
on all Haskell source files in the src
directory.
If HLint has suggestions for how the code can be improved, the CI pipeline fails.
The same check is performed by the ./tool/full-test.sh
script.
There are HLint plugnis for many editors. If there is no such plugin for your editor, you can run the following command instead.
hlint src
Remember, that HLint only makes suggestions and you don't have to follow these suggestions.
However, since the CI pipeline fails if HLint finds possible improvements, suggestions have to be ignored explicitly.
Edit the .hlint.yaml
file for this purpose and leave a comment why you had to ignore that suggestion.
To test whether the compiler is working correctly, you can run the included unit tests via one of the following Cabal commands.
cabal new-test
cabal new-run freec-unit-tests -- [options...]
Similar to how we discourage using cabal new-run
during development to run the compiler, we recommend using our ./tool/test.sh
script to execute unit tests instead.
./tool/test.sh [options...]
-
Firstly it allows command line options such as
--match
to be passed to the test suite more easily. The--match
command line option can be used to run specific unit tests instead of the full test suite. For more details on supported command line options use the following command./tool/test.sh --help
-
Secondly the script configures HSpec (the library that we are using for testing) to create a failure report. The failure report allows you to add the
--rerun
command line option to run test that failed in the previous run only../tool/test.sh --rerun
This allows you to focus on the failed test cases during debugging. Once all tests are fixed, all tests are executed again.
-
Finally the script overwrites GHC's
-Werror
flag that is set by default for our compiler with-Wwarn
. Doing so improves the development workflow. However, in production, the compiler must compile without any warnings. Thus, the CI pipeline will reject any commits that contain unhandled warnings. To make sure that your local changes do not cause warnings to be reported, run thefull-test.sh
script before pushing../tool/full-test.sh
The script simulates the CI pipeline locally but runs much faster since there is no overhead for creating test environments, uploading and downloading artifacts, initializing caches etc. If the script succeeds, it is not guaranteed that the CI pipeline will definitely pass, but it should catch the most common mistakes.
The Free Compiler is licensed under The 3-Clause BSD License.
See the LICENSE file for details.