In compiler theory, there are multiple related, and often mixed, concepts that all help you compile code less often [1]:
Already in the early times of FORTRAN (as of FORTRAN II [1, p. 384]), independent compilation of modules was used to speed up recompilation. This allowed a change in one module to require only the recompilation of that module and linking against the previously compiled other modules of the program. Skipping the compilation of all modules except the changed one was a significant improvement over compiling everything again. This did come at the cost of possible link-time issues. To be able to link the program together, the modules needed to be up-to-date with the data layout defined in COMMON. This was done manually and only checked by the programmer.
Mesa introduced separate compilation, which solved this issue by moving the static checks of cross-module dependencies to compile-time [7]. When a Mesa module is compiled, the result includes a symbol file that can be used during the compilation of other modules that depend on that module. Other languages such as ML and C use interface files that are written by the programmer. Separate compilation brought back type correctness for statically typed programs, while preserving fast recompilation from independent compilation.
To further speed up recompilation, we can save intermediate results during compilation. If parts of the program do not change, then the intermediate results of those parts can be reused. The term separate compilation applies to compilation where intermediate results are saved per file [7]. For sub-file level tracking of changes and intermediate results, the term incremental compilation is used [17, 18]
Even when not generating executable code, these concepts still apply to proof assistants. When making a change in a formal development, we would like to spend as little time as possible on re-checking other parts of the project, especially when processing time is measured in hours, not minutes.
Unsound independent compilation is obviously undesirable for proof assistants, and sound incremental compilation is also surprisingly tricky to pull off because it requires the compiler to faithfully keep track of internal dependencies, which may require far-reaching restructuring of the implementation.
A basic amount of separate compilation on the other hand can probably be found in most formal systems: when mutual dependencies are not allowed between different files, the sensible unit of compilation is a single file, and changing one unit should not lead to units that do not depend on it being re-checked. This can be implemented by a build system as basic as make
. It gets more interesting when we want some changes not to affect (the entire closure of) dependents, roughly sorted from "simple" to "hard":
- non-semantic changes such as whitespace, comments
- changing a proof without changing the statement (only simple when proof terms are not exported)
- file-internal changes, such as local notations
- changing a definition that should be regarded as opaque in other files, e.g. the definition of real numbers may not be material once the relevant statements have been proved about it (problematic if you want definitions to compute)
- changing or introducing some declaration that is not used in other files (very hard to do soundly in the presence of proof search or complex name resolution)
Are there proof assistants that tackle some of these, or other, challenges beyond the basic make
approach? Are there plans to do so? Are there other issues that make this topic inherently more complex for proof assistants?
[1] Smits, Jeff, Gabriël DP Konat, and Eelco Visser. "Constructing Hybrid Incremental Compilers for Cross-Module Extensibility with an Internal Build System." The Art, Science, and Engineering of Programming 4.3 (2020): 16-1.