Carry the automatically translatable fragment of Rocq (formerly Coq) into Lean 4.
The translation has no correctness guarantee. It moves kernel terms structurally and marks everything it will not carry. A translated theorem is a Lean theorem of a translated statement, proved by a translated Rocq proof t erm; whether the two statements mean the same thing depends on the small, explicit mapping table described below, not on a proof.
flowchart LR
V["file.v"] -->|"rocq2lean-extract<br/>links the Rocq kernel"| IR["file.json<br/>kernel terms + sites"]
IR -->|"rocq2lean<br/>Lean 4 executable"| L["file.lean"]
L -->|"lake env lean"| Check["Lean elaboration"]
- Extractor (
extractor/, OCaml): opens a real Rocq session, interprets the file sentence by sentence, and after the file is finished reads every declaration back out of the final global environment. Reading at the end is what makes sections work: a constant declared inside a section is read in its discharged form, with the section hypotheses already products of its type. - Translator (
Rocq2Lean/, Lean 4): parses that JSON, resolves names, and emits Lean source. It is a normal Lean executable, so the second half of the pipeline needs no other language.
Proofs cross as kernel proof terms, not as tactics. induction, rewrite
and the rest run inside Rocq; what crosses the boundary is the term Rocq's
kernel checked. There is no tactic translation, and therefore no guessing at
what a proof script meant.
A second, syntax-directed pass turns a proof term into a tactic script when it recognises the shape, and leaves the term otherwise:
| Term shape | Script |
|---|---|
leading fun binders |
intro x y |
Eq.refl |
rfl |
an induction principle (_ind, _rect, _rec) applied to motive, branches and a scrutinee |
induction x with | ctor … => … |
eq_ind/eq_ind_r (and eq_rect, eq_rec) |
rw [e]; …, with the term as fallback |
a single-constructor Case on a hypothesis |
rcases h with ⟨a, b⟩; … |
(fun x y => body) a b with variables a, b |
body in the extended scope |
| anything else | exact <term> |
The fallback is always the term, so an unrecognised shape still compiles. Two kernel details matter and are handled:
- Rocq's branch telescope interleaves constructor arguments and induction
hypotheses (
l IHl r IHr); Lean'sinductionpattern takes the arguments first (l r IHl IHr). - Lean names
inductionalternatives by the constructor's short name (zero, notNat.zero).
Covered: the mapped nat, bool, list, option eliminators and and/or/
ex, plus a file's own _ind/_rect/_rec for a single, non-indexed,
non-mutual inductive. Nested induction works when the inner eliminator is fully
applied to its own arguments; an eliminator whose result is then applied to a
hypothesis falls back to the term. Multi-branch Case terms keep their match
because focusing several generated goals in one inline script needs bullets;
indexed induction is still skipped. Reduction is bounded (fuel) so a chain of
rewrites cannot multiply the output without limit.
Every script is emitted as first | <script> | exact <term>: a shape the
reducer gets wrong degrades to the term instead of breaking the file.
--strict-scripts omits that net, and tests/run.sh translates every case both
ways — so on this suite the scripts are known to elaborate on their own, while a
user's file keeps the fallback.
$ nix develop
$ tests/run.shtests/run.sh re-enters the dev shell itself, so it also works from outside:
$ ./tests/run.shOne file by hand:
$ nix develop -c bash -c '
extractor/_build/default/bin/main.exe -- path/File.v build/File.json
lake exe rocq2lean build/File.json build/File.lean
lake env lean build/File.lean'Rocq flags go before -- (so -Q, -R, -include keep working); the input
and output paths go after it. The extractor reads the standard library from
ROCQPATH, which the dev shell sets. Rocq 9 names the library Stdlib, so
sources should say From Stdlib Require Import Lists.List. (From Coq … still
resolves through the compatibility alias.)
A file's constants are named by its library name, derived from the output
path and the -R/-Q flags exactly as rocq compile does it. That is what
makes compcert.lib.Maps.PTree.get mean the same constant in every file. A file
whose name is not a valid Rocq identifier falls back to the interactive
document, whose constants are Top.*.
Several files can be translated into one Lean file, in dependency order:
$ rocq2lean --join build/All.lean build/Coqlib.json build/Maps.json--join is what makes cross-file references resolve: the translator registers
every declaration in the joined unit, and the stable topological sort orders
them. With a large library, three options narrow the unit:
$ rocq2lean --join out.lean --root compcert.lib. \
--statements-only --axiomatise-theorems \
stdlib.json compcert.json--root <prefix>keeps only declarations reachable from names with that prefix.--root compcert.lib.selects Coqlib/Maps and the stdlib declarations they actually use, instead of every lemma of every loaded module.--statements-onlykeeps every definition body and replaces only theorem proofs with a markedsorry.--axiomatise-theoremsemits proof-valued declarations asaxioms under the markerROCQ2LEAN-ASSUMED, so the trusted base is an enumerable list rather thansorryAx.--axiomatise-prefix <prefix>assumes declarations even when they are definitions, for stdlib modules whose computation the target does not need.
The extractor's --no-proof-bodies drops the body of every proof-valued
constant while extracting, which is what keeps a stdlib module's JSON to its
statements.
Everything the translation refuses to carry is marked with the token
ROCQ2LEAN-UNTRANSLATED, in one of three forms.
| Situation | Generated Lean | Consequence |
|---|---|---|
| Type cannot be translated | block comment with the reason; declaration omitted | Lean reports the name at each use site |
| Body cannot be translated, type can | def … := by + -- ROCQ2LEAN-UNTRANSLATED: reason + sorry |
file elaborates; #print axioms shows sorryAx |
| A reference has no declaration and no mapping | emitted as its flattened name, listed in the file's report | Lean reports the unknown identifier |
The file also ends with an ## Assumptions section when a declaration was
emitted but depends on Lean inferring something the translation does not write
down (today: the motive of an indexed match). That is not a failure marker; it
is the short list to read first when Lean rejects a generated declaration.
The translator also prints a summary and every marker to stderr. Omitted declarations are loud on purpose: a file that references one fails to elaborate, rather than compiling against an invented axiom.
Terms (kernel constructors):
| Rocq | Lean |
|---|---|
Rel, Var |
scoped names (rendered uniquely; Rocq shadowing is renamed) |
Sort |
Prop, Set/Type → Type |
Cast |
erased |
Prod, Lambda, LetIn, App |
→, fun, let, application |
Const, Ind, Construct |
mapped or flattened names; @ keeps kernel arity |
Case |
match (the kernel's return predicate is not emitted; Lean infers it) |
Fix |
let rec (single function), with the kernel's Γ, f₁ … fₖ, args scope; mutual blocks are marked |
Proj |
Record.field |
Int, Float, Array, Meta, Evar, CoFix |
untranslated marker |
SProp, QSort |
untranslated marker |
Vernacular:
Definition,Fixpoint,CoFixpoint(marked),Inductive,Record,Theorem/Lemma/Corollary/Example,Axiom/Parameter/Conjecture.- Sections are interpreted and their constants read back discharged.
Require/Importare executed so the file can be checked; the imported declarations are not emitted. References to them resolve only through the mapping table, otherwise they are reported as unresolved.- Notations,
Ltac, options, hints and the rest of the proof-engine harness are interpreted but produce no Lean output. Terms are already elaborated, so notations are not needed to express them. - There is no translation of modules, functors, type classes, or
Instancedeclarations beyond the constants they generate. - Files are translated one at a time; a
Requireof another translated file is an unresolved reference, not animportof the generated Lean.
Rocq2Lean/Names.lean holds three tables (mappedTypes, mappedCtors,
mappedConstants) and Rocq2Lean/Prelude.lean holds the Lean side of every
constant in them. An entry asserts that the Rocq global and the Lean global are
the same theory for translation purposes. That is an assumption, not a theorem.
Two rules keep the assumption usable:
- The Lean side keeps Rocq's argument order and explicitness. Kernel applications are passed through, so a binder Lean would hide has to be written.
- Definitions are mapped only to a Lean definition with the same recursion
scheme.
Nat.addrecurses on its second argument in Lean and on its first in Rocq, soCorelib.Init.Nat.addmaps toRocq2Lean.add, a Rocq-shaped definition; mapping it toNat.addwould carry proof terms across a definition they do not match.
Mapped today: the Init logic (eq, and, or, ex, True, False, their
eliminators, f_equal), nat, bool, unit, prod, sum, list, option,
and add/mul. Extend the table and the prelude together, then add a test
that elaborates the result.
- Universes are erased. Lean re-infers them from the carried term. This is usually invisible and occasionally wrong in a way Lean cannot see.
SetisType. The predicativity difference is not modelled.- A
match's motive is dropped. Lean infers it. When the expected type does not mention the index — a proof built byinversionis the common case — Lean cannot infer the motive, rejects the generatedmatch, and the declaration is listed under## Assumptions. It is not marked automatically, because the translation is syntax-directed and does not typecheck its output. - Mutually recursive fixed points are marked, not emitted. Lean's
let rec … and …is layout-sensitive inside an expression, and emitting syntax that parses only at the top level would be worse than a marker. - Opaque proofs are read with the kernel's opaque accessor. The term is
there; the deprecation warning is acknowledged in
extractor/lib/readback.ml. - Rocq's derived
_sindeliminator (SProp elimination) is generated for every inductive and is always marked: Lean has no SProp. Expect one marker per inductive, and expect nothing to depend on it. - No imports between generated files.
--jointurns a dependency-ordered set of files into one Lean file; there is no per-moduleimportemission yet. - Toolchain: Rocq 9.1.1 with the
Stdliblibrary, Lean 4.35.0, OCaml 4.14 (the compiler Rocq was built with).
extractor/ OCaml extractor; links rocq-runtime
Rocq2Lean/
IR.lean typed view of the JSON intermediate; strict parsing
Names.lean flattening and the trusted mapping tables
Translate.lean kernel term → Lean term
Emit.lean declarations, ordering, markers, report
Prelude.lean Rocq-shaped Lean definitions for the mapping
Main.lean CLI
tests/cases/*.v end-to-end cases
tests/run.sh extract, translate, elaborate, check markers and axioms
tests/run.sh is the contract:
- every case is extracted, translated, and elaborated by Lean;
- every generated
theoremis checked with#print axioms— a translated proof must not depend onsorryAxor on any axiom; - cases that must be partially untranslated are asserted to carry a marker, and fully translatable cases are asserted to carry none.
The intended target is CompCert. It is not translated yet, but two measurable
milestones are reached: Coqlib + Maps and common/AST.v elaborate.
| A1 | A2 | |
|---|---|---|
| Target | lib/Coqlib.v + lib/Maps.v |
common/AST.v |
| Declarations in the closure | 476 | 244 |
| Unresolved references | 0 | 0 |
| Lean errors | 0 | 0 |
| Assumed declarations | 269 | 37 |
| Untranslated markers | 7 | 11 |
tests/compcert-a1.sh reproduces A1 end to end (extract, join, elaborate) in
about 75 seconds: A1: 476 declarations, 269 assumed, 0 unresolved, 0 Lean errors. tests/compcert-a2.sh reproduces A2 in about eight minutes: it builds
CompCert with its own Makefile (which needs coqdep; the dev shell appends the
caller's PATH for it), extracts the closure, and prints A2: 244 declarations, 37 assumed, 0 unresolved, 0 Lean errors. Three Flocq Prop/* modules are
skipped: they are proof-only, are not in AST's closure, and trip an internal
Rocq assertion when processed as a library.
A2's closure is the AST file plus the declarations it actually mentions from
Errors, Integers, Floats, Flocq, Archi, String, and SpecFloat; the
whole Reals/proof side of Flocq is not pulled. The 37 assumptions are the AST
theorems (proofs are A3), four more decision procedures, and the same class of
ZArith_dec/sumbool functions as A1.
The assumptions named so far: all CompCert theorems; ZArith_dec's
Z_lt_dec/Z_le_dec/Z_le_gt_dec; Int.eq_dec, Int64.eq_dec,
Ptrofs.eq_dec, bool_dec, calling_convention_eq, signature_eq,
Beq_dec; and the six dependent-match definitions in Coqlib/Maps. Each is a
ROCQ2LEAN-ASSUMED declaration with its translated statement, so the trusted
base is enumerable with #print axioms.
What is translated exactly in that unit: the numeric tower (positive, N,
Z, Pos.add/mul, Z.add/mul/compare/div…), lists, sumbool,
comparison, the class hierarchy, and the definitions of Coqlib/Maps
including PTree, ZMap, and the PTree functor instantiations. Pos.add and
Pos.add_carry are emitted as a Lean mutual block; mutually recursive
fixpoints no longer need to be marked.
The assumed dependent-match definitions (option_eq, list_norepet_dec,
tree_ind', Equal_dec, fold_ind, fold_ind_aux, and the decision
procedures above) need an explicit Case motive; a later step will carry them,
or write them by hand, rather than leave them axiomatic.
The remaining path:
- A3 — proofs in
lib/andcommon/, starting withCoqlibandMaps. Each assumed CompCert theorem'saxiomis replaced by its translated proof; the name and statement do not change. This also removes the need for the decision-procedure assumptions where a proof only uses them. - A4 — definitions beyond AST:
Values,Memory, the backend and frontend, still statements-only. - A5 — semantic layers and the correctness theorem.
Known hard points beyond the numeric tower: coinduction (7 files), Program
obligations (27), Derive, and proof terms from lia/ring whose lemma
statements pull the micromega hierarchy. Each is either translated or becomes a
named assumption.
Apache-2.0. Copyright 2026 CR Scidonia Limited. See LICENSE and NOTICE.