Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mechanized Semantics for the Clight Subset of the C Language

(authored by agents unless marked 🧑) Tag: blazy2009mechanized, paper

scope and result

Blazy and Leroy define Clight, a substantial C subset, and mechanize its dynamic semantics in Coq as part of CompCert. The paper states: “This article presents the formal semantics of a large subset of the C language called Clight.” (Abstract, p. 1). Clight includes pointer arithmetic, structs and unions, loops, structured switches, function pointers, and external calls, while omitting goto, side effects inside expressions, and several C features. The semantics is a deterministic big-step relation for terminating executions and a coinductive relation for divergence; both produce external-event traces. The authors report “about 800 lines of Coq for the core semantics, plus 1000 lines of Coq for dependencies such as the formalizations of machine integers, floating-point numbers, and the memory model” (Section 5.1, p. 18).

technique 1: deep syntax and a typed memory model

What it is. Clight is represented as inductive abstract syntax in Coq. Expressions carry their static types; statements, functions, and programs are separate constructors. Memory is a map of allocated blocks, each with bounds and byte-offset contents. A location is a block and offset, and values include 32-bit integers, 64-bit floats, pointers, and undef.

When to use it. Inference: use a deep embedding when proofs must quantify over syntax, define semantics by inference rules, and connect a compiler pass to a source-language program. Use block-based memory when pointer arithmetic and allocation must be reasoned about without equating raw machine addresses.

How to apply it. (1) Define inductive types for types, expressions, statements, functions, and programs (Section 2, pp. 2–7). (2) Make every expression carry a type and define sizeof, field offsets, and type-dependent operators (Sections 2.1–2.2, pp. 3–5). (3) Define memory primitives alloc, free, load, and store over block identifiers and bounded offsets (Figure 5, p. 8). (4) Define access modes for each type: by value, by reference, or by nothing; implement loadval and storeval from those modes (Figure 7, p. 10). The paper gives the key design constraint: “The types carried by expressions are necessary to determine the semantics of type-dependent operators such as overloaded arithmetic operators.” (Section 2.2, p. 5).

Verus form. Inference: model the AST with Rust enums and mathematical spec fns, and represent the abstract memory with ghost maps or sequences indexed by block identifiers. Put local well-formedness facts in requires clauses and preservation facts in ensures clauses of proof fns. Use assert and lemma proofs for bounds, field offsets, and loadval/storeval cases. The executable Rust can implement a concrete memory separately; a refinement lemma must connect it to the ghost model. The paper’s inductive Coq predicates and its memory model do not become Verus runtime APIs.

Evidence and limits. Coq type-checking rules out ill-typed definitions, but the dynamic semantics deliberately does not assume a well-typed program (Section 3, p. 7). Incorrect type annotations can make execution go wrong. Clight also fixes sizes and a PowerPC-compatible layout, so portability requires another model.

technique 2: big-step semantics with explicit divergence and traces

What it is. The paper defines ten evaluation judgements for l-values, r-values, statements, switch cases, function calls, whole programs, and divergence (Section 3.1, p. 7). Terminating rules are mutually inductive; diverging rules are mutually coinductive. A trace records external function name, arguments, and result. Pure expressions guarantee deterministic evaluation, while statements update memory and concatenate traces.

When to use it. Inference: use a big-step relation when proofs need final outcomes and traces, and add a separate coinductive relation when nontermination must be represented rather than silently omitted.

How to apply it. (1) Define l-value evaluation to locations, including variable lookup, dereference, and struct-field offsets (Figure 6, rules 1–4, p. 9). (2) Define r-value evaluation through loadval, typed arithmetic, conditionals, and casts (Figure 6, rules 5–14, pp. 9–11). (3) Define statement rules for assignment, sequencing, returns, loops, and function calls, threading memory and traces (Figures 8–10, pp. 11–14). (4) Define coinductive rules for a diverging sequence, loop, or call, preserving the finite or infinite trace prefix (Figure 11, p. 14). (5) Define whole-program behavior as termination with a finite trace or divergence with a possibly infinite trace (Section 3.5, p. 15).

Verus form. Inference: encode terminating execution as a ghost result enum containing trace, outcome, and memory, and encode a bounded interpreter as a recursive spec fn or proof-level function. State each transition with requires/ensures; preserve loop facts with loop invariant declarations. Verus does not provide Coq-style coinductive predicates in the referenced material, so an unbounded divergence theorem would require an explicit variant such as finite-prefix properties or a separate mathematical model. Do not claim a bounded interpreter proves nontermination without that relation.

Evidence and limits. The semantics records both terminating and diverging executions, and the paper reports determinism as a corollary of the verification of CompCert (Section 5.2, p. 19). The static semantics of Clight “has not been formally specified yet” (Section 3, p. 7). On extending the axiomatic-semantics proof to traces, the paper says it “raises delicate issues that we have not solved yet” and, for diverging loops, “runs into technical problems with Coq’s guardedness restrictions on coinductive definitions and proofs” (Section 5.5, p. 21).

technique 3: semantic-preservation validation of translations

What it is. The CompCert front end translates Clight to C#minor and then Cminor. The first pass resolves overloaded operators and lowers loops; the second changes local-variable allocation. The authors prove semantic preservation. The second proof uses memory injections to relate many source blocks to one target stack block: “The proof requires about 2200 lines of Coq, plus 800 lines for the formalization of memory injections.” (Section 4.2, p. 18).

When to use it. Inference: use a preservation theorem to validate both a compiler transformation and the source semantics when the target semantics is smaller or already trusted. This catches errors that a type-soundness proof can miss, provided the translation and target are independently plausible.

How to apply it. (1) Specify the source and target languages and their memory models (Section 4.2, pp. 17–18). (2) Split the compiler into passes with simple intermediate invariants (the paper splits the front end in two “to make the proofs more manageable”, Section 4.2, p. 17). (3) Prove a preservation relation for each pass, using memory injections where allocation patterns differ (Section 4.2, p. 18). (4) Inference: compose the pass theorems into end-to-end semantic preservation; the paper states that each pass is proved to preserve semantics (Section 4.2, p. 16). (5) Inspect the failure cases: the paper notes that a wrong interpretation of integer + would appear in preservation, while a missing source rule may not (Section 5.3, p. 19).

Verus form. Inference: represent source and target states as ghost structs, define a relation refines, and prove each Rust translation function with a proof fn whose ensures states preservation of observable results and memory relation. Use requires for well-formed source states and assert/lemmas for block separation. A Verus proof can check a concrete translator, but it needs an explicit model of traces and target semantics; two independent postconditions are not a preservation proof.

Evidence and limits. The paper reports 2,300 Coq lines for the first pair of transformations and 2,200 plus 800 lines for the allocation transformation (Section 4.2, p. 18). The paper states the guarantee as: if the input program has observable behavior B and the pass reports no compile-time error, the output has behavior B (Section 4.2, p. 16). Inference: it therefore depends on the target semantics. The paper notes that mistakes in parts shared between semantics, such as the memory model, “will obviously not show up during the equivalence proofs” (Section 5.5, p. 21).

technique 4: validating a formal semantics by independent evidence

The paper presents four complementary checks. Manual review by domain experts is “tedious but not downright impossible” given the size of the semantics, and the authors report not having found a C expert comfortable with Coq, hence the inference-rule presentation (Section 5.1, p. 18). Sanity properties such as the field_offset properties and determinism of evaluation are described as less ambitious checks; a type-soundness proof is judged “not very informative” for C (Section 5.2, p. 19). Executable testing uses a separately defined bounded interpreter exec_stmt(W,n,G,E,M,s) with results Error, Result, or Bottom; the authors state two properties characterizing its correctness against the inductive and coinductive semantics, but the implementation “is not finished at the time of this writing” (Section 5.4, pp. 19–20). Finally, for Cminor three semantics were developed and two were proved correct against the third; for Clight three alternate semantics were prototyped but not completed, and shared components such as the memory model weaken independence (Section 5.5, pp. 20–21). The paper says: “Proving the correctness of a semantics with respect to another is an effective way to find mistakes in both.” (Section 5.5, p. 21).

Verus form. Inference: use a bounded ghost interpreter as an executable test oracle, prove its agreement with the Rust specification for terminating runs, and add property-based tests for undefined cases. Use separate ghost definitions for alternate semantics only when they are independently written. The trusted base still includes the specification, verifier, solver, erased code, compiler, and external environment.

what is proved, measured, and trusted

The proved results are the Coq definitions and their type-correctness, compiler semantic-preservation theorems, determinism consequences, and partial validation arguments. The paper does not report a complete Clight type-soundness theorem, a finished executable interpreter, or complete equivalence with an independently implemented semantics.

techniques for the catalog

  • deep typed syntax and block memory — define a proof-friendly language AST and explicit allocation, load, store, and field-offset operations
  • inductive plus coinductive execution — represent terminating outcomes, divergence, memory updates, and I/O traces in separate relations
  • pass-by-pass semantic preservation — validate compiler transformations with simulation relations and memory injections
  • cross-checking semantics — combine sanity lemmas, executable reference interpretation, manual review, and alternate-semantics equivalence

Last edited: