Rust and Lean
In the new edition, a function can say what it does, not only what it touches: requires and ensures, written in Rust. ra-jit translates each function's MIR to Lean as it compiles it, and Lean checks the claim. Easy claims are proved by tactics with no help. Hard ones carry a Lean proof next to the code, in the same file. Rust stays the language programs are written in; Lean is the language proofs are written in.+
No comments yet.
Comments are not set up yet.
Motivation+
No comments yet.
Comments are not set up yet.
The charter says checkable over short: a few more words are a good trade when they let the compiler prove something. Types and capabilities prove a lot, but not that sort sorts or that an index is in bounds. Proofs do.+
No comments yet.
Comments are not set up yet.
Proofs used to be the expensive part. Language models change that. They write tactic proofs well, and Lean tells them exactly what is left to prove, so they can try, read the goal and try again. Writing a proof is becoming as cheap as writing a test, and a proof covers every input.+
No comments yet.
Comments are not set up yet.
Today, proving things about Rust means a separate tool: a fork of the compiler, a translator run by hand, a different spec language, proofs in another repository. The proof drifts from the code, and almost nobody runs it.123+
No comments yet.
Comments are not set up yet.
ra-jit owns the compiler. It already lowers every function to MIR and compiles it one function at a time. It can translate each function to Lean at the same point, keep the result in the same cache, and check the proof in the same edit loop as the type check.+
No comments yet.
Comments are not set up yet.
Design sketch+
No comments yet.
Comments are not set up yet.
Specs are Rust. requires and ensures clauses go after the signature. A spec fn is ordinary Rust that may only be used in specs: it is never compiled to machine code, so it may use unbounded integers and quantifiers. Syntax is illustrative; see the open issues.1+
No comments yet.
Comments are not set up yet.
Each clause becomes a Lean theorem about the function's Lean definition. ra-jit first tries a fixed list of tactics: omega for integer arithmetic, bv_decide for bit operations, simp and grind for the rest. Most obligations in ordinary code end there, and the author writes nothing.+
No comments yet.
Comments are not set up yet.
fn max(a: u32, b: u32) -> u32
ensures result >= a, result >= b, result == a || result == b
{
if a >= b { a } else { b }
}
// ra-jit generates three theorems; omega proves each one.+No comments yet.
Comments are not set up yet.
When the tactics fail, the function carries a proof: a proof by lean { ... } block after its body, in the same .rs file. Inside it is plain Lean, with the obligation as the goal. The proof moves with the code, is reviewed in the same diff, and fails the build when the code changes under it.+
No comments yet.
Comments are not set up yet.
spec fn sorted(v: &[i32]) -> bool {
Silent
forall |i: usize, j: usize| i < j && j < v.len() ==> v[i] <= v[j]
}
fn merge(a: &[i32], b: &[i32]) -> Vec<i32>
requires sorted(a), sorted(b)
ensures sorted(&result), result.len() == a.len() + b.len()
{
let (mut i, mut j, mut out) = (0, 0, Vec::with_capacity(a.len() + b.len()));
while i < a.len() || j < b.len()
invariant sorted(&out), out.len() == i + j,
forall |k: usize| k < out.len() ==> (i < a.len() ==> out[k] <= a[i])
&& (j < b.len() ==> out[k] <= b[j])
{
if j == b.len() || (i < a.len() && a[i] <= b[j]) { out.push(a[i]); i += 1; }
else { out.push(b[j]); j += 1; }
}
out
}
proof by lean {
-- the goal: the loop invariant holds after each push
intro i j out hinv hcond
unfold sorted at *
split <;> grind [List.getElem_append]
}+No comments yet.
Comments are not set up yet.
Import both ways+
No comments yet.
Comments are not set up yet.
Specs can name existing Lean definitions and theorems. use lean::... brings a Lean name into scope for specs only, so a spec can say "sorted" the way Mathlib says it and a proof can use every lemma Mathlib already has about it.4+
No comments yet.
Comments are not set up yet.
use lean::List::SortedLE; // Mathlib.Data.List.Sort
use lean::List::Perm; // Lean core
pub fn sort(v: &mut Vec<i32>)
ensures SortedLE(view(v)), Perm(view(v), view(old(v)))
{
// ...
}+No comments yet.
Comments are not set up yet.
The other way, every Rust function appears in Lean as a definition, under its crate and module path. A Lean proof can unfold it, rewrite with it and prove lemmas about it, and other proofs can use those lemmas. max from the first example becomes this:+
No comments yet.
Comments are not set up yet.
def max (a b : U32) : Result U32 :=
if a >= b then ok a else ok b+No comments yet.
Comments are not set up yet.
Optionally, Lean code compiled to C could be called from Rust at run time, through a capability (RFC 0001), so a verified Lean decision procedure could run inside a Rust program. This is an open question, below.5+
No comments yet.
Comments are not set up yet.
MIR to Lean, one function at a time+
No comments yet.
Comments are not set up yet.
The translation follows Aeneas. Safe Rust's ownership rules make it possible to translate without a model of the heap: a shared borrow becomes the value, and a mutation becomes a functional update. A &mut argument becomes a value passed in plus a backward function that returns its new value to the caller. Loops become recursive functions. A panic becomes Result.fail.26+
No comments yet.
Comments are not set up yet.
What MIR means is defined once, in Lean, starting from MiniRust, Ralf Jung's precise specification of a Rust core close to MIR. The translation for each kind of MIR statement is then checked against that model, so the Lean definition of a function is known to mean what its MIR means. Without the model, the translation would be one more thing to trust.7+
No comments yet.
Comments are not set up yet.
Obligations the compiler adds+
No comments yet.
Comments are not set up yet.
Some theorems are generated without a spec:+
No comments yet.
Comments are not set up yet.
- No panics where none are allowed. A function that says it cannot panic gets an obligation at each arithmetic operation that can overflow and each index that can be out of bounds.+
No comments yet.
Comments are not set up yet.
- Loop invariants. An
invariantclause holds on entry and is preserved by each iteration.+No comments yet.
Comments are not set up yet.
- Effect claims from RFC 0001. A function with no
Allocgets a theorem that no path reaches the allocator, which the translation can state because calls to it are explicit in MIR.+No comments yet.
Comments are not set up yet.
The proof cache+
No comments yet.
Comments are not set up yet.
A proof is cached under the hash of the function's MIR plus the specs of the functions it calls, not their bodies. Changing a callee's body without changing its spec does not recheck its callers. This is the same unit as ra-jit's function cache, and it uses the same store.8+
No comments yet.
Comments are not set up yet.
Lean inside the compiler+
No comments yet.
Comments are not set up yet.
ra-jit embeds Lean from the start. Lean 4 is written in Lean and compiles to C, and its runtime is a C library with a C header, lean.h. ra-jit links Lean's runtime and elaborator into its own binary, the way a C program links any library.5+
No comments yet.
Comments are not set up yet.
At startup, ra-jit initializes the Lean runtime and the modules it needs, Lean's own and Mathlib's, once. The environment they build, every definition and theorem Mathlib has, then stays in memory for as long as ra-jit runs. Each obligation is a call into the elaborator and the kernel: no process boundary, no pipe and no JSON, and nothing to load again between edits.+
No comments yet.
Comments are not set up yet.
The community repl, which runs Lean as a subprocess and talks JSON on stdin, stays useful as a debugging fallback: an obligation can be written out and replayed there by hand. It is not how ra-jit checks proofs.9+
No comments yet.
Comments are not set up yet.
In the editor+
No comments yet.
Comments are not set up yet.
ra-jit's language server shows the proof state inline: the goal at the cursor inside a proof by lean block, which obligations were proved automatically and by which tactic, and which ones failed. A failed obligation is a diagnostic on the clause it came from.+
No comments yet.
Comments are not set up yet.
What a proof rests on+
No comments yet.
Comments are not set up yet.
unsafe functions and extern functions cannot be translated. They state an assumed ensures instead, which is trusted, like unsafe impl Send. For every proof, the compiler lists every assumption it rests on: assumed specs, axioms used by the Lean proof, and the MIR model itself. A reviewer reads that list, not the proof.+
No comments yet.
Comments are not set up yet.
Terms+
No comments yet.
Comments are not set up yet.
Lean here is the verification backend, not a code-generation backend. It checks claims and produces nothing that runs. Machine code still comes from ra-jit's normal backends, from the same MIR, and a function with a spec is compiled the same way as one without.+
No comments yet.
Comments are not set up yet.
What it buys+
No comments yet.
Comments are not set up yet.
- Claims about behaviour, checked on every build: sorted, in bounds, never overflows, never allocates.+
No comments yet.
Comments are not set up yet.
- Specs and proofs in the same file and the same diff as the code, so they cannot drift.+
No comments yet.
Comments are not set up yet.
- Mathlib's definitions and lemmas, usable in Rust specs, instead of writing a new library of facts.+
No comments yet.
Comments are not set up yet.
- Incremental proof checking: an edit rechecks the functions whose MIR or callee specs changed, and nothing else.+
No comments yet.
Comments are not set up yet.
- A loop language models are good at: write the proof, read the goal Lean shows, fix it. The compiler is the referee.+
No comments yet.
Comments are not set up yet.
- Review by assumptions: a diff that adds an assumed spec or an axiom shows up in the list a proof rests on.+
No comments yet.
Comments are not set up yet.
Prior art+
No comments yet.
Comments are not set up yet.
- Verus: specs and proofs in the Rust file,
spec fn,requiresandensures, checked by an SMT solver. The surface syntax here follows it.+No comments yet.
Comments are not set up yet.
- Aeneas and Charon: Rust to Lean from MIR, with backward functions for
&mut. The translation here follows it.+No comments yet.
Comments are not set up yet.
- Creusot: Rust specs in a Rust-like language, proved through Why3.+
No comments yet.
Comments are not set up yet.
- Prusti: Rust specs checked by the Viper verifier.+
No comments yet.
Comments are not set up yet.
- Kani: bounded model checking of Rust, used on std.+
No comments yet.
Comments are not set up yet.
- Flux: refinement types for Rust: the checkable claim lives in the type.+
No comments yet.
Comments are not set up yet.
- hax: Rust to F*, Lean and Rocq, for cryptographic code.+
No comments yet.
Comments are not set up yet.
- MiniRust: a precise specification of a Rust core close to MIR, the model the translation is checked against.+
No comments yet.
Comments are not set up yet.
- verify-rust-std: the AWS-led challenge to verify Rust's standard library, and the specs it has written so far.+
No comments yet.
Comments are not set up yet.
- Lean 4 reference: foreign function interface and Mathlib: the Lean side. leanprover-community/repl: Lean as a subprocess that answers JSON, the usual way tools drive Lean today.+
No comments yet.
Comments are not set up yet.
Open issues+
No comments yet.
Comments are not set up yet.
Interior mutability and dyn Open+
No comments yet.
Comments are not set up yet.
The Aeneas translation needs ownership to be visible. Cell, RefCell, Mutex and dyn Trait hide it: the first three mutate through a shared borrow, and a trait object hides which function is called. Options: a heap model for these types only, specs on each trait method, or no proofs through them.+
No comments yet.
Comments are not set up yet.
Closures that capture &mut Open+
No comments yet.
Comments are not set up yet.
A closure that captures a &mut needs a backward function of its own, and a higher-order function that calls it an unknown number of times needs a spec that says what it may do to its captures. Aeneas does not support this in general yet.+
No comments yet.
Comments are not set up yet.
Concurrency Open+
No comments yet.
Comments are not set up yet.
Threads, atomics and channels are outside a sequential translation. Proving things about concurrent code needs a separation logic, as Verus and Iris do, and is out of scope for the first version. Sequential functions called from threads are still provable.+
No comments yet.
Comments are not set up yet.
Specs for std Open+
No comments yet.
Comments are not set up yet.
Every proof that calls Vec::push needs a spec for it. How much of std gets specs, who writes them, and are they proved or assumed? The verify-rust-std challenge is writing some; Aeneas ships a Lean model of parts of std. Start with what those already have.14+
No comments yet.
Comments are not set up yet.
Proofs across Lean and Mathlib versions Open+
No comments yet.
Comments are not set up yet.
Lean and Mathlib change often, rename lemmas and change what simp does. A proof that passes today can fail after an upgrade. Pin a Lean toolchain and a Mathlib commit per edition, as the issue on one Lean version per ra-jit asks? And who fixes broken proofs in dependencies?+
No comments yet.
Comments are not set up yet.
Proof checking time in the edit loop Open+
No comments yet.
Comments are not set up yet.
grind and bv_decide can take seconds; ra-jit's rebuilds take milliseconds. Proofs probably run in the background after the type check, with results shown when they arrive, and block only the final build. Needs measuring on real code.+
No comments yet.
Comments are not set up yet.
Lean syntax inside Rust files Open+
No comments yet.
Comments are not set up yet.
A proof by lean { ... } block is not Rust tokens: Lean has its own comments, Unicode operators and indentation rules. The tokenizer has to treat the block as one opaque token, found by matching braces, and editors need to highlight it as Lean. Alternatives: a raw string, or a .lean file next to the .rs file.+
No comments yet.
Comments are not set up yet.
Calling Lean code at run time Open+
No comments yet.
Comments are not set up yet.
Lean compiles to C and has an FFI, so Rust could call Lean functions, gated by a capability. That pulls in Lean's runtime and reference counting. Is it worth it, and is it this RFC or another?5+
No comments yet.
Comments are not set up yet.
Trusting the MIR semantics Open+
No comments yet.
Comments are not set up yet.
The proofs are only as good as the Lean model of MIR. MiniRust is precise but not finished, and MIR changes between Rust versions. Who maintains the model, and how is it tested against what ra-jit's backends actually generate?7+
No comments yet.
Comments are not set up yet.
unsafe Open+
No comments yet.
Comments are not set up yet.
Assumed specs make unsafe code usable from proofs but prove nothing about it. Proving unsafe code correct needs a memory model in Lean, which MiniRust has. Start with assumed specs, listed in every proof that uses them, and add proofs for unsafe later.+
No comments yet.
Comments are not set up yet.
Binary size and build time Open+
No comments yet.
Comments are not set up yet.
Embedding Lean puts its toolchain inside ra-jit, and the proofs need Mathlib's compiled .olean files, several gigabytes of them. Is Lean linked into every ra-jit, or loaded only when a crate uses specs? Are Mathlib's files downloaded prebuilt, as Mathlib's own cache does, and how much does that add to installing ra-jit and to building it?+
No comments yet.
Comments are not set up yet.
Lean's runtime and ra-jit's threads Open+
No comments yet.
Comments are not set up yet.
Lean's objects are reference counted, and an object shared between threads has to be marked for atomic counts first. Each thread that calls into Lean has to be set up for it. ra-jit compiles on many threads. Which threads may call Lean, how a shared environment is handed to them, and what happens to the compiler when Lean panics or runs out of memory?+
No comments yet.
Comments are not set up yet.
One Lean version per ra-jit Open+
No comments yet.
Comments are not set up yet.
With Lean linked in, the Lean version is the one ra-jit was built with, and Mathlib has to match it exactly. Upgrading ra-jit upgrades Lean and Mathlib for every crate at once. Does each ra-jit release pin one Lean and one Mathlib, tied to the edition, and how do crates keep proofs that an upgrade breaks?+
No comments yet.
Comments are not set up yet.
Loading .olean files Open+
No comments yet.
Comments are not set up yet.
Lean loads a module's compiled .olean file by mapping it into memory. With Mathlib that is a large part of startup. Can ra-jit map them once and share them across its processes and across restarts, and does the mapping need a fixed address that a host process may already be using?+
No comments yet.
Comments are not set up yet.
References
- Verus Verified Rust: specs and proofs in the Rust file, checked by an SMT solver.
- Aeneas Translates safe Rust to Lean as pure functions; &mut becomes a value and a backward function.
- Creusot Deductive verification of Rust through Why3.
- Mathlib Lean 4's library of mathematics.
- Lean 4 reference: foreign function interface Calling C from Lean and Lean from C; marked unstable.
- Charon Extracts a crate's MIR into a form other tools read; Aeneas's front end.
- MiniRust Ralf Jung's precise specification of a Rust core close to MIR.
- ra-jit: the function cache #[ra_jit::cache], and what makes a function ineligible.
- leanprover-community/repl A JSON REPL for Lean 4 that reports goals, errors and sorries.
- Prusti A static verifier for Rust, built on Viper.
- Kani A bounded model checker for Rust.
- Flux Refinement types for Rust.
- hax Translates a subset of Rust to Lean, F* and Rocq.
- verify-rust-std The challenge to verify Rust's standard library, announced by AWS in November 2024.
Discussion
Comments on the page as a whole. To comment on one paragraph, use the button beside it.
Comments are not set up yet.