CoCompiler: Bidirectional C-Lustre Compiler
- CoCompiler is a bidirectional compiler/lifter that relationally maps between C and Lustre to facilitate DSL recovery in reactive systems.
- It integrates a relational port of the verified Vélus compiler with Haskell canonicalization passes, addressing both vertical and horizontal lifting challenges.
- The system enables transformation of legacy reactive C programs into normalized Lustre, supporting subsequent conversion to graphical behavioral models.
Searching arXiv for the primary CoCompiler paper and closely related context. CoCompiler is a prototype bidirectional compiler and lifter between C and Lustre, where Lustre is a synchronous dataflow language used for reactive systems. Its central premise is that if a compiler is implemented as a relation rather than as a traditional function, the same artifact can be run forward for compilation and backward for lifting. In CoCompiler, this idea is realized by rewriting the verified Lustre-to-C compiler Vélus in the Walrus relational programming language, then supplementing the relational core with semantics-preserving C-to-C canonicalization passes in Haskell. The resulting system addresses what it calls the vertical lifting problem, partially addresses the horizontal problem, supports lifting real reactive C code into Lustre, and can continue onward into graphical behavioral models such as SCADE block diagrams (Spargo et al., 30 Sep 2025).
1. Conceptual basis
CoCompiler is organized around the DSL lifting problem: translating low-level or legacy code into a domain-specific language so that the recovered program is easier to understand, analyze, verify, and modify. Its target domain is reactive systems, for which Lustre is a natural DSL because it makes time, streams, and synchronous execution explicit (Spargo et al., 30 Sep 2025).
The system distinguishes two problems. The vertical lifting problem is the problem of moving upward in abstraction from canonical C into Lustre. This is the part solved by the relationalized compiler core. The horizontal problem is the problem of handling real C programs that are outside the compiler’s image but are still “morally” reactive programs; CoCompiler addresses this with semantics-preserving canonicalization passes that rewrite C into a more canonical form before lifting (Spargo et al., 30 Sep 2025).
The paper also characterizes an ideal bidirectional compiler/lifter relation as many-to-many, because many source and target programs can be semantically equivalent. CoCompiler does not implement that ideal relation in full. Instead, its practical design is closer to “one to one” + canonicalization: a relational core that works well on canonicalized inputs, plus one-direction normalization passes that expand the reachable C surface (Spargo et al., 30 Sep 2025).
2. Relational compilation
The key technical move is to encode compilation relationally. Instead of a one-way mapping from Lustre to C, CoCompiler defines compilation in Walrus in a form approximated by:
1 |
compile :: Lustre -> C -> Goal () |
In this form, compilation succeeds when the two programs satisfy the compilation relation. The same relation can be executed in two directions:
1 |
(run 1 (cFile) (compile lustreFile cFile)) |
for compilation, and
1 |
(run 1 (lustreFile) (compile lustreFile cFile)) |
for lifting (Spargo et al., 30 Sep 2025).
Walrus is used as a miniKanren-style relational language embedded in Haskell. CoCompiler ports the core structure of Vélus into this setting, preserving much of its compilation phases, AST designs, helper functions, and call-graph structure. That choice is central to the system’s methodology: the lifting relation is not invented independently of the compiler, but obtained by repurposing the compiler itself (Spargo et al., 30 Sep 2025).
This yields a concrete form of bidirectional compilation. The same artifact can be used as a compiler from Lustre to C or as a lifter from canonical C to Lustre. In the paper’s formulation, this makes it possible to lift everything in compile’s image, which is the essence of its solution to the vertical problem (Spargo et al., 30 Sep 2025).
3. Architecture and intermediate languages
CoCompiler inherits the core structure of Vélus and ports four phases into Walrus:
- Lustre ↔ NLustre
- NLustre ↔ Stc
- Stc ↔ Obc
- Obc ↔ Clight
Here, NLustre is normalized Lustre, Stc is synchronous transition code, Obc is object code, and Clight is the C-like intermediate language used by CompCert (Spargo et al., 30 Sep 2025).
In the forward direction, the intended flow is: parse Lustre, elaborate it with type and clock information, bridge into the internal AST, run the relational compiler forward, and pretty-print Clight/C. In the backward direction, which is the more developed use case, the flow is:
- parse C with CompCert’s
clightgeninto Clight, - apply Haskell canonicalization passes,
- run the relational compiler backward,
- emit normalized Lustre,
- optionally translate onward to SCADE and then to
.xscade(Spargo et al., 30 Sep 2025).
The Haskell canonicalization stage is the system’s main answer to the horizontal problem. The examples given are deliberately simple and mechanical, including re-associating sequences of statements and inserting no-op skip commands. These rewrites do not add semantic interpretation; they exist to make Clight structurally resemble the compiler image expected by the relational core (Spargo et al., 30 Sep 2025).
Because normalization and scheduling are not themselves made bidirectional in the current implementation, CoCompiler does not accept arbitrary Lustre as forward input, and the backward direction produces normalized Lustre rather than a source-level reconstruction faithful to the original stylistic surface form (Spargo et al., 30 Sep 2025).
4. Lifted structure and reactive-state recovery
The system’s worked example is the Lustre node count, which implements cumulative summation over time. The handwritten source is:
1 2 3 4 |
node count (i:int) returns (o:int) let o = (0 fby o) + i; tel |
The corresponding canonical C representation consists of a state struct, a step function, and a reset function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct count { int norm1$1; }; int fun%%%%0%%%%count(struct count *obc2c$self, int i) { register int o; o = (*obc2c%%%%1%%%%1 + i; (*obc2c%%%%2%%%%1 = o; return o; } void fun%%%%3%%%%count(struct count *obc2c$self) { (*obc2c%%%%4%%%%1 = 0; return; } |
CoCompiler lifts this into normalized Lustre:
1 2 3 4 5 6 |
node count (i : int32) returns (o : int32) var norm1$1 : int32; let o = norm1$1 + i; norm1$1 = 0 fby o; tel |
The recovered program is less readable than the handwritten version because it preserves normalized state variables such as norm1$1, but it reconstructs the essential reactive structure: reset becomes the initialization for 0 fby, and the step function becomes the per-tick Lustre equations (Spargo et al., 30 Sep 2025).
This example is representative of CoCompiler’s target class. It does not attempt to infer arbitrary high-level intent from arbitrary C. Instead, it recognizes a very specific organization of reactive code—explicit state, reset, step, and canonical Clight structure—and lifts that organization into Lustre’s stream-based form (Spargo et al., 30 Sep 2025).
5. Correctness model and scope boundaries
CoCompiler’s trust argument is inherited indirectly from Vélus. Vélus provides syntax and semantics for each intermediate representation in Rocq, and each compilation phase is accompanied by a machine-checked proof that it preserves semantics. This is one reason CoCompiler is built by porting Vélus rather than by designing a decompiler from scratch (Spargo et al., 30 Sep 2025).
At the same time, CoCompiler itself is not presented as a fully verified system. The Walrus relational port is manual, the Haskell canonicalization passes are not machine-checked, and the Lustre-to-SCADE path is also not end-to-end verified. The strongest claim is therefore not formal verification of the full bidirectional tool, but confidence derived from the verified source compiler and the close structural correspondence of the port (Spargo et al., 30 Sep 2025).
Its limitations are explicit. The current system:
- lifts only C in or near Vélus’s image,
- relies on limited, example-driven horizontal transformations,
- produces normalized Lustre rather than denormalized source-like Lustre,
- does not yet support some Lustre features such as
mergeandreset, - omits bidirectional normalization, scheduling, and some optimization passes,
- and remains brittle with respect to small syntactic changes that place the C program outside the expected canonical form (Spargo et al., 30 Sep 2025).
The paper is especially clear that any syntactic change that moves a program outside Vélus’s image may render it unliftable by the current relation. That brittleness is not incidental; it follows directly from the fact that the implemented relation approximates a semantic many-to-many correspondence by starting from a particular compiler image and widening it only partially through canonicalization (Spargo et al., 30 Sep 2025).
6. Significance and broader implications
CoCompiler’s main significance is methodological. It proposes rapid DSL lifter prototyping by relationalizing an existing compiler, rather than hand-building a decompiler for a DSL from first principles. In the paper’s account, the core relational port required just over three engineer-weeks, with four compiler passes each taking approximately 32 hours, even though some of that effort also advanced Walrus itself (Spargo et al., 30 Sep 2025).
This suggests a broader pattern: if a domain already has a compiler from DSL to language , then rewriting that compiler relationally can yield an compiler/lifter pair, with additional canonicalization used to widen the reachable subset of . The paper presents this as modular, language-agnostic, and fast to implement (Spargo et al., 30 Sep 2025).
For reactive systems specifically, the benefit is immediate. CoCompiler provides a path from legacy or low-level C back to Lustre, and from there onward to graphical behavioral models such as SCADE. That enables existing verification and model-based workflows to be applied to code that would otherwise remain trapped at the imperative level (Spargo et al., 30 Sep 2025).
The obvious next steps, all identified by the paper, are to broaden the horizontal canonicalization repertoire, port more of Vélus, improve readability through denormalization and naming recovery, and extend the same relational-compilation idea beyond C↔Lustre to other DSL ecosystems. In that sense, CoCompiler is less a finished universal decompiler than a concrete demonstration that relational programming offers a practical foundation for building DSL lifters by repurposing existing compilers (Spargo et al., 30 Sep 2025).