Keywords

1 Introduction

Formal verification, including model checking [1], is an important technology to examine with mathematical rigor whether a computational model satisfies a given specification. It has attracted significant attention not only in academia but also in industry and helped to find potential errors or delivered correctness guarantees of practical computing systems [2, 3, 4, 5, 6, 7]. The challenges for formal verification in the modern era have escalated due to multiple reasons, including the ever-increasing system complexity, emerging applications with domain-specific modeling languages, interactions among heterogeneous computing components, and the usage of generative artificial intelligence to aid the implementation of systems. Therefore, formal-methods researchers and practitioners have to consider the following essential question: How can we continue to improve existing tools and develop new tools to tackle the aforementioned challenges while keeping up with the ever-increasing development pace?

To approach this question, we observe that formal-methods tools heavily rely on transformation. For example, consider the following three-step approach: a model in a frontend modeling language and a specification are transformed to an intermediate representation used by a tool, the semantics of the intermediate representation is transformed to mathematical formulas in the logic used by a prover, and the answer of a prover is transformed back to a certificate in the frontend language to help users to understand the verification results. Various transformation procedures connect different abstraction layers in formal-methods tools and play a key role in their correctness, efficiency, and usability. This ‘transformation game’ can be made explicit, and the steps could be decomposed such that the components implementing each step can be reused.

Technologies that can be used as library, such as SAT [8] and SMT [9] solvers, usually have standardized input formats and application programming interfaces to facilitate their usage by formal-methods tools in an off-the-shelf manner. On the contrary, the transformation procedures in verification tools are often integrated and hard-wired in individual tools. The lack of modularity and reusability makes the development of formal-methods tools error-prone, because developers often need to re-implement the same techniques. For example, a new verifier for the programming language C [10] often needs to implement the functionality to interpret the semantics of C [11] (e.g., as logical formulas in SMT-LIB 2 format [12]) and can hardly reuse the transformation procedures in other mature software verifiers. This slows down the progress pace in the development of verification tools and the growth of the importance of formal methods. The goal of this paper is to advocate transformation as a means to decompose verification tools.

1.1 Transformation as a Paradigm to Develop Formal-Methods Tools

We advocate the possibility to construct verification tools by means of transformation, to continue the success story of formal verification while facing the contemporary challenges. The paradigm of modular transformation calls for using or inventing exchange formats to represent verification artifacts, building modular transformers based on the exchange formats, and employing transformers as building blocks to develop new tools.

A related paradigm to modular transformation is cooperative verification [13], which advocates that several tools should work together to solve a task by exchanging information. A framework has been developed to facilitate the combination and collaboration of tools [14]. Modular transformation differs from cooperation verification in its emphasis on the transformation components of verification tools. While cooperative verification investigates how different tools can solve a verification task together, modular transformation deals with the design of transformation components such that they are maximally reusable and can be easily combined to construct new verification tools, with or without cooperation. A major goal of modular transformation is to decompose tightly integrated monolithic tools for modularity. The following two ingredients are essential to achieve the goal:

  1. 1.

    standard exchange formats for communicating via verification artifacts and

  2. 2.

    composable standalone transformers based on verification artifacts.

The terms verification artifact and transformer are taken from previous work on cooperative verification [13], and we will define and extend them for modular transformation in Sect. 2.

Modular transformation follows the well-known principles of single responsibility [15] and separation of concerns in component-based software engineering [16]. The merits of these principles have been witnessed in different application domains, including various utility programs in the Unix operating system [17] (i.e., the Unix philosophy [18]) and numerous dialects and translators in the Llvm/MLIR compilation infrastructure [19, 20]. In the area of formal verification, research works that follow the paradigm of modular transformation have contributed to

  1. 1.

    combine multiple tools via standard exchange formats for verification [21, 22], validation [23, 24], and testing [25, 26],

  2. 2.

    advance the state of the art by transforming verification tasks and applying tools or algorithms for other modeling languages [27, 28, 29] or specifications [30, 31, 32], and

  3. 3.

    enhance the explainability of verification results by transforming certificates [33, 34, 35].

1.2 Our Vision: Composition Through Transformation

How can modular transformation help to address the modern challenges of formal verification? Using the verification of emerging computational models as an example, we illustrate the paradigm’s potential below. Suppose we need to build a new verifier for computational models described in a domain-specific language (DSL), which is unsupported by any existing formal-methods tool. To leverage mature verification technology, for example, verifiers for the programming language C, we develop a standalone transformer to translate the DSL to the programming language C. Following the modular transformation paradigm, a new verifier for the DSL can be constructed by combining the translator with state-of-the-art verifiers for C programs in an off-the-shelf manner. This strategy uses the C language as the exchange format to connect the translator with verifiers for C programs and saves the effort to repeatedly implement a frontend for the DSL in every C verifier. Of course, it is also possible to develop a dedicated verifier from scratch that works directly on the DSL. This approach may leverage specific characteristics of the DSL and yield more powerful verifiers. However, the composition of the translator and existing, highly tuned verifiers for C programs can serve as baseline for performance comparison. For example, a recent study [27] compares dedicated verifiers for the Btor2 language [36], a DSL to describe model-checking problems of hardware circuits, to compositional verifiers formed by a Btor2-to-C translator and verifiers for C programs. While the dedicated Btor2 verifiers are better at proving the safety of hardware circuits, the compositional verifiers reveal bugs in hardware circuits that the dedicated verifiers overlook.

Fig. 1:
figure 1

Transformation network for joining forces via translating verification tasks

Next we explain how transformation networks bring us closer to the modular transformation paradigm, and how to join forces for formal verification. We illustrate this in Fig. 1 on the programming language C, the Llvm intermediate representation [19], and the hardware modeling language Btor2  [36] as examples. The transformation network bridges the gaps between various verification communities by translating frontend modeling languages and applying all available backend tools to verify a task. For example, a hardware circuit in the Btor2 language can be translated to a program in C or the Llvm  [19] intermediate representation (IR) and given to verifiers for C, e.g., CPAchecker  [37], or Llvm, e.g., SeaHorn  [38], respectively. A program in C or Llvm can also be translated to a Btor2 circuit and verified by hardware model checkers, such as AVR  [39].

In the transformation paradigm, an emerging new modeling language can leverage state-of-the-art verifiers for different languages once the corresponding translators are constructed, which also provides a common ground to evaluate future research advancements. Thanks to the emphasis on standard exchange formats and interfaces in the transformation paradigm, developing new tools as combinations of existing ones becomes easier. The principles of single responsibility and separation of concerns facilitate the participation of community members in the implementation and maintenance of transformers because they are compact and extensible.

While transformation procedures frequently appear in the verification literature, the modular transformation paradigm encourages to decompose the verification into modular components that are easier to develop and maintain. The scope of transformation procedures extends beyond models and specifications to certificates, invariants, and counterexamples. The modular transformation paradigm can significantly increase our performance in developing new verification tools: It will guide us to achieve a more reusable and robust infrastructure for formal verification, avoid bugs in the process of reimplementing standard verification components, and facilitate combination and cooperation between tools through standard exchange formats and interfaces (cf. community discussion [40, 41]).

We humbly contribute this article to the Festschrift on the occasion of Joost-Pieter Katoen’s 60th birthday, in order to trigger discussion in the community and draw attention to the modular-transformation paradigm as a potential solution to the modern challenges of formal verification. The rest of this article is organized as follows: Section 2 defines verification artifacts and transformers, Sect. 3 surveys various transformers, either modular or tightly integrated ones, in formal-methods tools, Sect. 4 highlights the construction of new verification tools using transformers, Sect. 5 discusses prospective benefits and impacts of the modular-transformation paradigm, before we conclude in Sect. 6.

2 Verification Artifacts and Transformers

As foundation for the discussion in the rest of this paper, we define verification artifacts and transformers in this section. The definitions below refine the notions used in the context of cooperative verification [13].

Definition 1

A verification artifact is (1) an input, output, or intermediate object consumed or produced by a formal-methods tool and (2) can be represented in an exchange format defined for communicating information among the tool’s internal components or with other formal-methods tools.

Note that a verification artifact is required to be representable in an exchange format devised for verification purposes. Although exchange formats do not need to be human-readable (e.g., the binary AIGER format [42] for hardware model checking), serializing an object to a byte stream does not qualify the object as a verification artifact because serialization protocols such as Java Object Serialization Specification are not designed specifically for formal verification.

Table 1 lists the most common and important types of verification artifacts. A model \(M\in \mathcal {M}\) is a description of the system under verification. It can be written in a high-level modeling language used by human designers or an IR used by tools. A specification \(\varphi \in \varPhi \) defines the expected behavior of the system under verification. A model \(M\) and a specification \(\varphi \) jointly form a verification task, which is given to a formal verifier as input. A verdict \(r\in \mathcal {R}\) is a verifier’s decision on a verification task. A verifier may produce three different verdicts: TRUE, meaning that \(M\,\models \,\varphi \); FALSE, meaning that \(M\,\not \models \,\varphi \); and UNKNOWN, meaning that the verification was inconclusive. A witness \(\omega \in \varOmega \) is a certificate produced by a tool to explain its verdict on a verification task. We interpret witnesses more flexibly than verification witnesses [43] for verifiers and also consider test cases as witnesses from test-case generators. Tools that produce witnesses for their analysis results are said to be certifying [44, 45]. A verification condition \( vc \in \mathcal{V}\mathcal{C}\) is an intermediate object produced by a verifier to encode a partial or the complete behavior of the model as a set of constraints in the logic of an underlying prover used to decide the satisfiability of \( vc \).

Table 1: Examples of important verification artifacts

Definition 2

A transformer is a procedure that consumes one or more verification artifacts as input and produces one or more verification artifacts as output in polynomial time.

To capture a transformer’s essence of being simple and fast compared to a prover or a verifier (called analyzer in cooperative verification [13, 14]), we require it to produce output artifacts in time polynomial in the size of the input artifacts. The definition can be relaxed in other scenarios, e.g., when a transformation component runs faster than the main verification process but not in polynomial time.

Table 2 lists the most common and important types of transformers. A translator consumes a model \(M\) and produces a behaviorally equivalent model \(M'\) in a different modeling language. An encoder consumes a model \(M\) and produces a verification condition \( vc \) that describes partial or complete behavior of \(M\). A specification transformer consumes a model \(M\) and a specification \(\varphi \) and produces a transformed model \(M'\) and a different specification \(\varphi '\) such that \(M\,\models \,\varphi \iff M'\,\models \,\varphi '\) (i.e., the two verification tasks are equisatisfiable). A witness transformer consumes a model \(M\) and a witness \(\omega \) and produces a transformed witness \(\omega '\) for other purposes, e.g., by making it more precise to replay a counterexample. A slicer cuts off some parts of the consumed model \(M\) that do not affect the satisfiability of the specification \(\varphi \) by \(M\) and produces a sliced model \(M'\). A splitter decomposes a verification task \((M,\varphi )\) into smaller tasks, where the number of split tasks is polynomial in the sizes of \(M\) and \(\varphi \). A pruner removes irrelevant parts of the consumed model \(M\) according to a witness \(\omega \) and produces a simplified model \(M'\) (e.g., by deleting paths marked as fully explored in \(\omega \) from \(M\)). An annotator augments the consumed model \(M\) according to a witness \(\omega \) and produces an augmented model \(M'\) (e.g., by labeling the invariants recorded in \(\omega \) as assumptions to \(M\)). A reducer consumes a model \(M\) and produces a model \(M'\) that is in the same modeling language and easier to verify. An instrumentor expands a model \(M\) and produces a model \(M'\) in the same modeling language to record information for further analysis, e.g., by adding run-time monitors to \(M\).

The key difference between a pruner and a reducer (resp. an annotator and an instrumentor) in the above classification is the presence of a witness as an input to the transformer. A pruner or an annotator relies on the information in the witness to transform the input model, whereas a reducer or an instrumentor transforms the model without taking a witness into account.

Table 2: Examples of important transformers

3 Transformers in Formal-Methods Tools

In this section, we survey transformation approaches and tools developed for formally verifying hardware and software computational models. Following the classification of transformers discussed in Sect. 2, our survey aims to cover representative transformers of different types in the literature. Table 3 lists the transformers discussed in this section (and the tools constructed by using transformers, which will be discussed in Sect. 4).

3.1 Translators

A translator consumes a model as input and produces a behaviorally equivalent model in a different modeling language. We first look at translators whose outputs are in a high-level language used by human engineers to design the system and then discuss translators whose outputs are an intermediate representation used in or consumed by other tools.

Translators can also be used to construct analyzers for the source language via their combinations with tools for the target language as backend [27, 28, 33, 46]. Such translators can help with comparing and bridging verification approaches for different computational models and will be covered in Sect. 4.1.

To High-Level Modeling Languages. Models for digital sequential circuits can be represented in hardware-description languages at the register-transfer level, such as Verilog  [47]. A Verilog circuit can be translated into a behaviorally equivalent C program to leverage analysis techniques for software. For example, Verilator  [48] translates Verilog circuits to multi-threaded  programs, which can be compiled and executed for efficient circuit simulation. VHD2CA  [49] translates VHDL  [50] circuits to counter automata, which can be dumped as C programs and checked by software analyzers.

To Intermediate Representations. Intermediate representations are used by tools to exchange information among their internal components or with external tools. They usually have a simple syntax to facilitate parser development of the tools and precise semantics to underpin formal reasoning of the models. Many IRs have been proposed as the input format for formal verifiers. For example, SMV  [51] is a language to describe finite-state transition systems and allows properties to be specified in CTL; AIGER  [42] is used to describe a bit-level and-inverter graph with memory elements; Btor2  [36] lifts AIGER to the word level by inheriting operations from the SMT-LIB 2 format [12]. These three formats are mainstream in the community of hardware model checking and often used as target languages by translators. For example, Verilog2SMV  [52] and Ver2Smv  [53] translate a Verilog circuit to an SMV model, which can be given to nuxmv  [54] for verification. A word-level circuit in the Btor2 language can be bit-blasted to an AIGER circuit by Btor2AIGER from the Btor2 tool suite [55] to leverage bit-level model checkers, such as ABC  [56]. CIRCT  [57] is an open-source tool chain for electronic design automation based on the MLIR  [20] framework, offering various IRs and tools to represent and translate hardware circuits.

In the community of software analysis, Boolean programs [58] and Goto programs have been used by pioneering verifiers, e.g., Slam  [2, 59] and Cbmc  [60], as the internal problem representation. To facilitate the analysis of C code, Cil  [61], the C Intermediate Language, decomposes complex constructs of C into simpler ones but still works at a higher level than the assembly code. Since the invention of Llvm  [19], some software analyzers rely on its rapidly-growing compilers, including Clang, as frontend to translate the source programming language to Llvm-IR, used as the internal data structure. For example, Llbmc  [62] and SeaBmc  [63] translate C to Llvm-IR and perform bit-precise BMC with SMT solving. Klee  [64] is an Llvm-based symbolic-execution engine for test-case generation. Symbiotic  [65, 66] extends Klee with program slicing and instrumentation to implement a full-fledged software verifier. SeaHorn  [38] uses Clang as its frontend and generates verification conditions from Llvm-IR. Smack  [67] translates Llvm-IR to Boogie  [68], an intermediate verification language, and relies on verifiers for Boogie as backend. MLIR  [20] is an extension of Llvm to flexibly define “dialects” of Llvm-IR and automatically generate translators between the dialects. In a similar spirit, the software verifier Infer  [5] uses an IR called SIL  [69] to decouple the frontend and backend development.

3.2 Encoders

An encoder consumes a model \(M\) and produces a verification condition \( vc \) that describes partial or complete behavior of \(M\) in terms of the logic used by some prover. Commonly used logics for formal verification include propositional logic [8], first-order logic with background theories [9], and constrained Horn clauses (CHC) [70], a fragment of first-order logic. Formulas in propositional logic are described in DIMACS format [71] and given to Boolean Satisfiability (SAT) solvers. Formulas in first-order logic modulo some background theory are described in SMT-LIB 2 format [12] and given to Satisfiability Modulo Theory (SMT) solvers that support the theory. CHC formulas are also described in SMT-LIB 2 format and given to Horn solvers [72, 73, 74, 75].

While prover-based verifiers have an encoding procedure from their IRs to the logic of the prover, the procedure is often tightly integrated with the verifiers and hardly reusable in other tools. We highlight several tools that can dump verification conditions and be used as standalone encoders. EBMC  [76] takes as input a Verilog circuit or an SMV model, unrolls the model, then generates verification conditions for the behavior of the unrolled model as SAT or SMT formulas. Llbmc  [62] and SeaBmc  [63] consume a program in Llvm-IR and produce formulas for bounded model checking as SMT formulas. SeaHorn  [38] consumes Llvm-IR and produces verification conditions as CHC formulas. Boogie  [68] and Viper  [77] are intermediate languages for verification, accompanied by encoders from a Boogie or Viper program to SMT formulas.

Besides the logics discussed above, linear programming and its variants have also been used to encode verification conditions for analyzing software programs [78] or neural networks [79, 80].

3.3 Specification Transformers

A specification transformer consumes a verification task \((M,\varphi )\) and produces another verification task \((M',\varphi ')\) such that (1) \(\varphi \ne \varphi '\) and (2) \({M\,\models \,\varphi } \iff {M'\,\models \,\varphi '}\). Since reachability-safety analysis has received much (if not most) attention, and many mature tools are available, several approaches have been invented to transform verification tasks with other specifications to reachability safety.

Schuppan and Biere proposed a transformation for finite-state transition systems with a liveness specification [30]. The key observation is that a counterexample to a liveness specification in a finite-state model has a “lasso” shape, which consists of a prefix of finite length leading to the entry of a loop. By enlarging the state space of the original model with a shadow variable to record visited states and introducing an additional input as an oracle to detect the entry of a lasso’s loop, the approach transforms a verification task with a liveness specification to another task with a reachability-safety specification to find a lasso.

CCured is a type system to infer the memory safety of a program [81]. For pointers whose safety cannot be statically inferred, CCured inserts run-time checks in the original program, which can be used as error locations and checked by model checkers, such as Blast  [31].

MonoCera [82] transforms programs with specifications in more expressive logics, such as first-order logic with quantifiers or separation logic [83], to equisatisfiable verification tasks with specifications expressed only by basic program operations, such as assertions, supported by many automatic software verifiers. MonoCera automates the transformation process by defining several transformation operators and their corresponding rewriting rules, which subsumes manual and error-prone transformations and bridges rich specification languages and automatic software verifiers.

Lal and Reps proposed a transformation from programs with a concurrency-safety specification to sequential programs with a single-thread reachability-safety specification [32]. The main idea is to limit the number of context switches in the concurrent program and simulate the behavior of the concurrent program with a sequential program.

Testability transformation [84] studies mappings from a model \(M\) and a coverage specification (also called test-adequacy criterion) \(\varphi \) to a model \(M'\) and a coverage specification \(\varphi '\) such that for any set T of tests, T is adequate for \(M\) w.r.t. \(\varphi \) if and only if T is adequate for \(M'\) w.r.t. \(\varphi '\). Testability transformation is used to facilitate generation of test cases.

It was also suggested to partition a conjunctive specification into its conjoined parts and verify the program several times with smaller specifications [85].

3.4 Witness Transformers

Verification witnesses [43] were introduced as justifications for the verification verdict. In particular, there is no prescribed level of abstraction for verification witnesses. For example, violation witnesses can be very abstract by only avoiding some parts of the program, or they can be as concrete as a test case by assigning explicit values to inputs. The model checker Blast contains a procedure to internally transform a counterexample, which can be seen as a violation witness in the form of a feasible error path, to test cases [86]. We will discuss several witness transformers and how they can be used to combine tools in Sect. 4.2.

3.5 Slicers

A slicer consumes a verification task \((M,\varphi )\) and produces a model \(M'\) by cutting off the parts in \(M\) that do not affect its satisfaction of \(\varphi \). For hardware model checking, slicing computes the cone of influence of the asserted signals and removes the circuitry outside the cones from the original circuit. For software verification, slicing [87] removes program operations that do not touch variables on which the assertions depend directly or indirectly.

Slicing has been shown to be an effective technique for software verification [88]. The tool Symbiotic  [65, 66] applies slicing and instrumentation as modular transformations, before a symbolic-execution backend like Klee  [64] is asked to perform a reachability analysis.

A project by Joost-Pieter Katoen studied the application of tools for software model checking to automotive code [89]. This project also investigated the impact of slicing, and confirmed the conclusion from the above study on Symbiotic. Slicing had a significant impact on the performance and effectiveness of several software verifiers [90, Sect. 4.8 “The Effect of Static Analysis”].

3.6 Splitters

A splitter decomposes a verification task \((M,\varphi )\) into several smaller tasks such that (1) \(M\,\models \,\varphi \) can be derived from the verification results of the smaller tasks and (2) the number of the smaller verification tasks is polynomial in the sizes of \(M\) and \(\varphi \).

Splitting was first studied for logic programs to decompose them into a bottom part and a top part to simplify the computation of answer sets [91]. It has been used also to scale symbolic execution by splitting program paths into several ranges (i.e., sets of paths) and exploring path ranges in parallel [92]. Ranged symbolic execution can be further generalized to run different analyses on different ranges [93]. Splitting has also been used to accelerate data-flow analysis by decomposing a control-flow graph into several subgraphs, applying data-flow analysis to each subgraph, and combining the resulting invariants from each subgraph [94, 95].

A program can also be decomposed into a set of straight-line programs that correspond to the verification conditions of a Hoare-style proof of correctness. This strategy has been implemented in the verification tool chain VST-T [96]. Later the same strategy has been used by the witness validator LIV  [24], which decomposes a C program into a set of C programs that can be verified with off-the-shelf verifiers for C. Also the verifier Bubaak-Split [97] splits programs into parts that can be verified in parallel. However, it is not clear if Bubaak-Split’s splitting can be done in polynomial time, because it involves a lightweight verification pass.

3.7 Pruners and Annotators

Pruners and annotators are dual to each other. While they share the same signature \(\mathcal {M}\times \varOmega \mapsto \mathcal {M}\), the former use the information in witnesses to remove or simplify irrelevant parts of the model, and the latter add hints to the model for further analyses.

For example, data-flow analysis can be combined with a pruner that transforms the control-flow graph [98]. Propagating variables with constant values discovered in data-flow analysis (which can be stored as a witness), the pruner simplifies the original control-flow graph to a subgraph to speedup the analysis. In Sect. 4.3, we will discuss how a pruner [22] can be used to turn any off-the-shelf model checker into a conditional model checker [99].

Program annotators have been used to coordinate static model checkers and test-case generators by explicitly documenting the assumptions made by model checkers in the program under verification to guide test-case generators, such as symbolic execution, to explore unverified parts of the program [100, 101]. They have also been used to orchestrate automatic and interactive verifiers [35, 102]. More details will be discussed in Sect. 4.3.

3.8 Reducers and Instrumentors

Reducers and instrumentors are dual to each other. While they share the same signature \(\mathcal {M}\mapsto \mathcal {M}\), the former simplify the model to make it easier to verify, and the latter expand the model by adding functionalities, e.g., run-time monitors, to record information for further analyses. In contrast to reducers, instrumentors usually slightly increase the workload for verifiers.

Acceleration [103, 104], shrinking [105], and abstraction [106, 107, 108] are three important techniques to reduce programs with loops. Loop acceleration replaces a loop with its transitive closure to speed up the convergence to a fixed point. Loop shrinking reduces a loop that processes a large array to an overapproximating but much smaller loop, which can be handled by bounded model checking. Loop abstraction overapproximates a loop by, for example, havocing variables modified in the loop. The key idea behind the three reducers is to produce a program with simplified computation in loops that overapproximates the original program, such that the correctness of the simplified program (which is easier to prove) implies the correctness of the original program.

Program instrumentors add auxiliary functionalities to a model to record more information for further analyses. For example, Symbiotic  [65, 66] checks memory safety by instrumenting C code that tracks allocated memory regions to the original program [109].

4 Constructing Formal-Methods Tools Using Transformers

In this section, we discuss how the modular-transformation paradigm outlined in Sect. 1.1, which emphasizes standardized exchange formats for verification artifacts and modular transformers, can help to construct new formal-methods tools in an agile and reliable way by combining existing ones with transformers. We will highlight tool combinations using translators and witness transformers, because they can help to unify and join forces of various verification techniques, such as hardware model checking vs. software verification and automatic model checking vs. interactive verification.

To facilitate tool combination, CoVeriTeam  [14] is a domain-specific language and tool to combine off-the-shelf executable components. Transformers can be composed with other components to implement a composite verification tool. For example, it was used to compose several components to construct tools using algorithm selection and portfolios [110]. CoVeriTeam has also been used to implement approaches like ranged analysis [93], circuit-based program verification [29], cooperative test-case generation [111], conditional testing [25], and component-based CEGAR [21].

4.1 Combining Tools with Translators

As mentioned in Sect. 1, one challenge to formal verification in the modern era is to handle systems with heterogeneous computational models, including hardware circuits, software programs, and cyber-physical devices. Although formal methods for different computational models share similar concepts and techniques, their alignment with distinct modeling languages creates gaps between the research communities. These gaps hinder one community from enjoying the advancements of the others, and the potential of leveraging techniques developed in related areas to improve the state of the art may be overlooked. Transformers, especially translators, can help to bridge the gaps between communities and provide a common ground to compare approaches from different communities.

For example, the tool v2c  [112] translates a Verilog circuit to a cycle-accurate and bit-precise C program, which can be consumed by software verifiers to analyze the properties of the Verilog circuit. A circuit model can also be represented in an intermediate representation designed for verification, e.g., the Btor2 modeling language [36], the input format in the Hardware Model-Checking Competitions (HWMCC) [113, 114]. The tool Btor2C  [27] translates a Btor2 model to an equivalent C program, enabling head-to-head comparison between hardware model checkers from HWMCC and software verifiers for C programs in the Competitions on Software Verification (SV-COMP) [115]. The evaluation shows that software verifiers can detect more hardware bugs than mature hardware model checkers [27]. Similarly, Btor2MLIR  [28] defines a dialect for Btor2 in the MLIR  [20] framework and translates a Btor2 model into Llvm-IR. Software analyzers consuming Llvm-IR, such as SeaHorn  [38], Smack  [67], and Klee  [64], can then be used to examine properties of Btor2 circuits.

Translation from the other direction, i.e., from software to hardware, has also been investigated. For example, c2v  [116] translates a C program to a Verilog circuit by first compiling it to Llvm-IR  [19] and translating the IR to Verilog. Hardware model checkers can be applied to the translated circuit to analyze properties of the original C program. The model checker Kratos2  [117] uses the K2 language as its intermediate representation for C programs and can be used as a translator to fold a C program into a sequential circuit in SMV, Btor2, or AIGER formats. Using Kratos2 as a translator, the software verifier CPV  [29] employs award-winning verifiers in HWMCC, such as ABC  [56] and AVR  [39], as its backend and participated in SV-COMP 2024. As a first-time participant, CPV performed better than many mature and fine-tuned software verifiers, indicating the potential of using hardware model checkers for program analysis.

VMT  [118] and MoXI  [46, 119] are two IRs for infinite-state transition systems. They both add constructs to define a transition system on top of the SMT-LIB 2 format [12] and offer translators from various modeling languages to themselves and vice versa. Their goal is to provide a common ground for comparing formal-methods tools and exchanging verification tasks: A verification task can be translated to other modeling languages through them and solved by tools from other domains. Tools for different modeling languages can be compared directly by using their translators as preprocessing. Recently, the first direct model checker for MoXI, MoXIchecker  [120], has been proposed, which supports various background theories to describe model-checking problems without translating them to other modeling languages.

4.2 Combining Tools with Witness Transformers

Witnesses can be transformed to improve the explainability and usability of analysis results. For example, a more abstract violation witness can be testified step by step to a more concrete one [121]. CPA-witness2test implements the approach of execution-based validation [34, 86] and transforms violation witnesses to test cases. With the help of CPA-witness2test, witness-generating software verifiers can be combined with debuggers to exempt programmers from figuring out how to interpret violation witnesses and maximize the benefits of formal-methods tools in a standard software-development environment. Note that, while stepwise testification may not run in polynomial time,Footnote 1 it is usually much faster than the verification process, and hence we also consider stepwise testification as a transformer.

Witness transformers can also convert the information of witnesses produced by verifiers for different modeling languages to other domains. For example, a standalone witness transformer from automata-based software witnesses [43] to hardware witnesses in the Btor2 language [36] is used to combine software verifiers and Btor2 witness validators into a certifying hardware model checker Btor2-Cert  [33]. The software verifier CPV  [29] also has a witness transformer from Btor2 witnesses to software witnesses to combine hardware model checkers and software witness validators.

Witness transformers can also be used to combine automatic and interactive software verifiers [35]. Invariants found by automatic verifiers and recorded in correctness witnesses can be transformed to program annotations, e.g., in the ACSL language [122], and verified by interactive verifiers like Frama-C  [123]. Vice versa, the proof obligations discharged by an interactive verifier can be transformed to a correctness witness, and an automatic witness validator can be invoked to check the proof obligations.

4.3 Combining Tools with Other Transformers

Transformers can be used to combine tools to cooperatively solve a complex problem via leveraging their unique strengths. For example, conditional model checking for software verification [99] uses condition automata (which can be seen as witnesses in our categorization) to mark the already-explored paths in a program and instruct subsequent verifiers to work on unexplored paths. To be used for conditional model checking, a verifier needs to take a condition automaton into account during its analysis, which incurs extra engineering effort and hinders a wider adoption of the idea.

Following the modular transformation paradigm, researchers proposed a standalone pruner to remove the explored parts from a program based on a condition automaton, which produces a residual program that can be consumed directly by off-the-shelf verifiers [22]. Using this pruner for preprocessing, any verifier can become a new conditional model checker without extra engineering effort. A similar pruner has also been developed for test-case generators [25], where already-hit test goals are removed from the original program by the pruner to form a residual program for subsequent test-case generation runs.

Transformers also enable verifiers to be used as building blocks for other applications beyond their original use cases, such as test-case generation and witness validation. Using a specification transformer to convert test goals to error locations for a reachability-safety specification, CoVeriTest  [26] generates test cases by invoking off-the-shelf software verifiers to find counterexamples that reach the error locations (i.e., the test goals). The witness validator MetaVal  [23] validates verification witnesses by using an annotator to label the information in a witness as assumptions (to guide the counterexample search for violation witnesses) or assertions (to check the invariants for correctness witnesses) in the original program and invoking software verifiers to analyze the annotated program. To validate correctness witnesses, LIV  [24] uses an annotator to label a program with invariants and a splitter to decompose the annotated program into several straight-line subprograms. An off-the-shelf verifier is invoked on the split programs to check the inductivity of the invariants.

Table 3: Transformers and formal-methods tools using transformers (column “Type” is the type of the transformer used by the tool)

Transformers can also benefit or facilitate the development of verifiers. For example, to leverage techniques for loop acceleration [103, 104], shrinking [105], or abstraction [106, 107], developers of verifiers often need to implement the successful approaches in their own tools again. To mitigate the risk of bugs for reinventing the wheel, CEGAR-PT  [108] is a standalone reducer that implements various abstraction techniques at the source-code level and can be used by other verifiers for program transformation. A transformed program is returned as an ordinary input program to a verifier and can be readily analyzed. To facilitate transforming witnesses produced by hardware model checkers to witnesses for programs, the software verifier CPV  [29] uses an instrumentor to augment input C programs before verification.

5 Applications of Transformation to Construct Modular Tools: Benefits and Impact

Table 3 summarizes the transformers and tools using transformers discussed so far in this paper. The collected entries in the table are not meant to be comprehensive because they are limited by the space in this article and by our knowledge. The table is an initial attempt to draw the community’s attention to the problem of decomposing verification tools by making transformation modular and explicit. We also call out the members in the verification community to contribute and maintain a data base for various transformers and verification tools. Such an effort has been started recently with a collection of tools for formal methods [124, 125], and we have added the used transformers to the meta data of the tools that are available there. In the following, we would like to mention a few example approaches of modular transformation together with their benefits and potential impact.

Modular Construction of Verification Tools. Modular construction from components ideally supports agile development processes, which are focussed on increments. If the solution consists of several independent components, the development can also be more independent. This has also two immediate consequences: First, a system built from loosely coupled components (ideally independent executables) is easy to extend, due to the reduced set of preconditions (such as having to use the same programming language for implementation). Second, a system that is modularly built using transformers can be more reliable, because the components might be developed, used, tested, and improved by different and large user groups.

Cooperative Verification. Cooperation between tools is a key to enable sharing the workload between components or tools. CoVeriTest  [26, 126, 127] is an approach to combine two different approaches to program analysis, where the two components work on a shared data structure to explore the reachable abstract states. One component is a predicate analysis and the other is an explicit-value analysis, both having different strengths and performance characteristics. Cooperation leads to joining forces and obtaining an overall stronger analysis.

Conditional model checking [99, 129] is a technique to instruct a conditional model checker that parts described by a given input condition are to be considered correct and the model checker should verify only the state space that is outside the state space described by the input condition. The conditional model checker also produces an output condition, which describes the state space that it proved correct. The model checker does not make any claims about the state space outside the condition, and leaves this (cooperatively) to another conditional model checker, which is in turn fed with the produced condition. Besides this sequential composition of conditional model checkers, the state space can also be split into two parts, one described by a condition and the other by its negation. Then, two conditional model checkers can run in parallel and verify the state space given to them in isolation, while contributing to a modular proof of correctness. The idea of conditional model checking was later also applied to testing [25]. Pruner-based conditional model checking [22] uses a transformation to prune a given C program according to a given condition to the state space that is not yet proven correct. Conditional model checking can be used to split a verification tasks into several smaller ones (see also [93]) and also to let each verifier verify the part of the program that matches its strengths.

Verifier Development. Already the first software model checker, Slam  [130], was developed using the transformation approach, where the information exchange was done using Boolean programs. CEGAR was executed as follows: In a given CEGAR step, the component C2BP abstracted the C program to a Boolean program, using a given set of abstraction predicates (the precision). Then, the component Bebop [131] performed a model-checking pass on the Boolean program. If Bebop found a program path to the error, the path was checked for feasibility. If the path was feasible, a bug was found, if not, the infeasible path was further analyzed and the reason for infeasibility was used to refine the Boolean program, which was then given to the model checker. This approach has an easy-to-understand architecture, but the next decades set a high priority on performance tuning, for which it was easier to use a monolithic implementations in one tool, such as in Blast  [132] or Cbmc  [60]. We hope for a renaissance of transformation-based verification.

The software model checker for termination analysis Terminator [133, 134] also uses existing technology as components: Terminator uses the tool RankFinder [135] to generalize the counterexample, and then it refines the transition invariants with this generalization. For checking the validity of the transition invariants, Terminator reduces the problem to a reachability query, which can be solved by standard reachability verifiers. This work has shown that for tackling a problem as hard as analysis of software termination it is imperative to build on existing components, and that such an architecture can be successful.

C-CEGAR [21] demonstrates how to decompose counterexample-guided abstraction refinement (CEGAR) [130, 136] into its three major components, abstract-model exploration, feasibility check, and precision refinement. Instead of integrating those components inside one tool, the three components are implemented as three executable and independent tools, with a well-defined interface (in contrast to Slam, information is passed from component to component as verification witnesses [43], or using special-purpose file formats for paths and precisions). This construction from executable components makes it easier to substitute improved versions or alternatives for each of the three components.

CEGAR-based loop abstraction [107] is a technique to abstract and refine the control flow: in a coarse abstract model, loops are represented by their abstractions, and if the model turns out to be too imprecise, then the loops are refined, perhaps by approximations, and on the most precise level by their original implementation. The tool CEGAR-PT  [108] implements this approach as a sequence of program transformation, where the loop abstraction is applied to the original program, then the abstract program is given to a software verifier for exploration, then, if an infeasible counterexample is found, the program is transformed to a more refined program by using a more precise loop abstraction, and then given to the software verifier again, and so on. Implementing the loop abstraction as program transformation makes it possible to apply the loop abstraction as a preprocessing step to any standard software verifier. The transformation separates the concern of loop abstraction from the concern of program analysis.

Validator Development. After the first four validators for verification witnesses had been implemented, the first transformation-based witness validator was developed: MetaVal  [23]. The idea was to reduce the problem of witness validation to the problem of reachability-safety analysis, by annotating the information from the witness into the program. The overall tool confirms a correctness (resp. violation) witness if the verifier can prove (resp. disprove) the safety of the program using the annotated information from the witness. This approach has the advantage that any safety verifier for C programs can be used as backend for MetaVal. This means, the development of witness validators can concentrate on the annotation, while the highly-tuned and already available verifiers can be used for verification.

LIV  [24] goes one step further: It splits the input C program into straight-line programs [96], which are loop-free C programs, and those straight-line programs are handed off to a verification tool. While normal SMT-based verification tools compose verification conditions in a logic format, such as SMT-LIB 2 [12], LIV uses the standard programming language C to write the verification conditions in the form of straight-line programs. Since no loop is contained, even bounded model checkers can be used for verifying straight-line programs. Furthermore, this approach can be parallelized, because each straight-line program can be verified independently from the others.

Bridging Verification Communities. Formal verification is a large research area, and there are many different communities (according to the application domain) to which formal methods can be applied. It is understood in the research area that knowledge transfer between the communities is necessary. Such a transfer between communities can lead to unification and improvements of the state of the art. The transfer can occur in two forms, either the knowledge is transferred and applied to different domains (algorithms for hardware verification might be re-implemented for software verification, e.g., PDR [137], IMC [138], DAR [139]), or off-the-shelf tools are adapted to different domains.

Applying Software Verifiers to Hardware Problems. Btor2C  [27] is a translator that takes as input a system written as hardware circuit in the language Btor2  [36] and produces as output a system written as a software program in the language C [10]. This makes it possible to use many verifiers for C programs (68 such verifiers are listed in an overview paper [140]), and experiments show that such new verifiers for hardware (that are based on transformation and software verification) can find bugs in hardware circuits that state-of-the-art hardware verifiers such as ABC  [56] and AVR  [39] were not able to find. This creates a new problem: Witnesses [43, 141] (also known as certificates) for correctness or violation must be transformed back to refer to the original model, but there are solutions for this problem [33]. The transformation paradigm is a solution that makes it possible to apply advancements in software verification immediately to the hardware domain, without the need of transferring the implementations of the algorithms to the new domain.

Applying Hardware Verifiers to Software Problems. CPV is a software verifier for C programs that uses Btor2 as intermediate language and solves the given input verification problem by a two-step approach: First, it translates the input C program to a Btor2 circuit using Kratos2  [117]. Second, it uses the hardware model checkers ABC  [56] and AVR  [39] to solve the verification problem. Verification witnesses are transferred back to witnesses that refer to the original input C program. The hardware model checkers are configured to run several different algorithms, including IC3/PDR, interpolation-based model checking, and k-induction, in a portfolio manner. The transformation paradigm is a solution that makes it possible to apply advancements in hardware verification immediately to the software domain, again, without the need of transferring the implementations of the algorithms to the new domain.

Communicating via Verification Witnesses. Verification witnesses were originally introduced with the goal to justify the verification result and to be able to independently validate the result using the witness (with tools called witness checkers [44, 121, 142]). But it was quickly discovered that witnesses are much more important, enabling explainability and understandability by visualizing the content of the witnesses (e.g., supporting debugging [143]), and enabling communication between verification components (e.g., as explained above for C-CEGAR [21], or in CoVEGI [144]). All those approaches increase the overall usability of verification technology. In the following, we explain a few examples.

Witness to Test. Once a verification tool proves that the specification is violated, and a violation witness describes an error path through the program, it should be interesting to transform the violation witness to a test vector. The execution of the program on the test vector then exposes the specification violation at runtime. This approach of execution-based witness validation [34] is implemented in the tools CPA-witness2test (based on the CPAchecker verification framework) and CProver-witness2test (based on the CProver verification framework). This approach makes it possible to bridge the gap between verification and testing. Testing is already well integrated into development workflows, and the results of verification runs that resulted in discovered bugs can be integrated easily into the existing testing workflow by adding the generated test vectors to the test suite.

Translating Software Witnesses to Hardware Witnesses. Witnesses are also important to communicate the justification of verification results from one language for systems to others. A witness always refers to the model (e.g., to values or relations of the variables used in the model). For example, Btor2-Cert  [33] has a component that translates the software witness produced by a software verifier (referring to the C program that the verifier analyzed) to a hardware witness (referring to the original Btor2 circuit). In the opposite direction, the tool CPV  [29] must deliver software witnesses, but uses hardware model checkers as backend, and thus, needs to translate hardware witnesses to software witnesses, such that the resulting witness refers to the input C program.

Cooperation of Automatic and Interactive Verifiers. Verification witnesses also open up many opportunities to connect automatic and interactive verifiers. For example, automatic verifiers write their invariants into witnesses, while interactive verifiers ask the users to insert their invariants as annotations into the program. Interactive verifiers can benefit from invariants that were generated from automatic verifiers. Researchers developed annotators [35] that, for example, take as input a C program and a correctness witness (with invariants) and produce as output a C program with ACSL [122] annotations. This annotated program can be given as input to the interactive verifier Frama-C  [123]. The experiments of the study [35] report that Frama-C can verify more programs with the help of the automatically produced annotations. This translation makes it possible to use any automatic verifier to produce invariants (or contracts) in order to help the users of an interactive verifier with information that can be derived automatically.

6 Conclusion

Formal verification is a hard problem and we need to leverage all possible approaches to solve this important problem. We advocate the approach of modular transformation, which uses standalone components for transformation in order to apply verification technology. We introduce the necessary notions and survey several approaches and tools that use transformation, in order to illustrate that this paradigm can be useful. We make a case for verification by transformation by explaining some applications of transformation to construct modular tools for formal verification and give hints on their benefits and impact. We hope that readers find this view on formal verification inspiring and either contribute more transformations or use some of the existing transformations.

Data-Availability Statement. We have added the transformers used by the tools in the FM-Tools Repository to their respective meta data under key used_actors. A generated web site is available at: https://fm-tools.sosy-lab.org/.

Funding Statement. This project was funded in part by the Deutsche Forschungsgemeinschaft (DFG) – 536040111 (Bridge) and 378803395 (ConVeY), as well as by the LMU PostDoc Support Fund.