A model of Rust's core and alloc libraries, packaged as:
- Rust crates (
core-models,alloc,rust_primitives) that mirror thecore::*andalloc::*items downstream verified-Rust code uses. - A Lean library (
CoreModels) extracted from those crates by Aeneas, suitable for downstream Aeneas-extracted Lean projects to depend on as a drop-incoremodel. - A test suite (
tests/) split into two surfaces:tests/client_test/— a regression "client" crate that exercises items fromcore::*/std::*end-to-end. Its only assertion is that the Aeneas extraction of the crate elaborates against ourCoreModelsshims; it does not check behavioural agreement.tests/rust_lean_equiv_test/— a rust↔lean equivalence framework. Each test pins one concrete behavioural observation (e.g.Some(7u8).is_some() == true), checked once on the Rust side viacargo testand once on the Lean side via a#guardagainst the Aeneas extraction. Divergence between Rust std and our model surfaces here.
Verified-Rust pipelines need a model of core and alloc to elaborate
against. Writing that model in Rust (rather than directly in each
verification tool's logic) has three advantages:
- Easy to extend: adding a new
core::*item is just a Rust source edit, no proof-assistant boilerplate. - Cross-testable against the real Rust core library: the model is
ordinary Rust, so we can compile and run it side-by-side with
stdand check behavioral agreement. - Shareable across verification tools: a single Rust model feeds
multiple downstream backends — currently hax-F*, hax-Lean, and
Aeneas-Lean — instead of each tool maintaining its own shadow
core.
CI verifies that the committed extracted Lean files in
lean/CoreModels/{Funs,Types,…}.lean match what a fresh extraction produces
against the pinned toolchain. That means a downstream Lean consumer can
just lake update this repo without installing the Rust toolchain.
.
├── core-models/ # main Rust crate: model of `core::*`
├── alloc/ # model of `alloc::*` (separate crate so it
│ # can be extracted with charon's
│ # `alloc_models` rename trick — see Makefile)
├── rust_primitives/ # tiny crate of helpers (slice/array primitives)
├── lean/ # the distributed Lean library
│ ├── lakefile.toml
│ ├── lean-toolchain
│ └── CoreModels/ # hand-written + extracted, both committed
├── tests/ # test suite (workspace; see Testing section)
│ ├── client_test/ # client-surface extraction smoke test
│ └── rust_lean_equiv_test/ # rust↔lean equivalence framework
│ ├── source/ # crate carrying `#[rust_lean_test]` fns
│ ├── macro/ # proc-macro defining the attribute
│ ├── gen_lean_tests.py # scans source/, emits #guards
│ └── lean/ # lake project consuming the extraction
├── patch_lean.py # post-processes Aeneas's output of the
│ # parent library to match our package layout
│ # (see comment block at top of the file)
├── Makefile # extraction + build orchestration
└── .github/workflows/ci.yml
- Rust toolchain pinned by
rust-toolchain.toml. charonandaeneasonPATH(the upstream nix flakes are the recommended build path; CI usesnix build github:AeneasVerif/{charon,aeneas}). Override the Makefile lookup withmake CHARON=/path/to/charon AENEAS=/path/to/aeneas.elanfor Lean.
make lean # extract Rust → Lean, patch, build the Aeneas library
make tests # full test suite (both client_test/ and rust_lean_equiv_test/)
make clean # remove all generated Lean + LLBC, keep hand-writtenmake lean is idempotent: re-running without source changes is a no-op
modulo Lake's incremental build.
To run just one test surface in isolation:
make -C tests/client_test # smoke-test extraction
make -C tests/rust_lean_equiv_test # rust↔lean equivalencetests/client_test/ is a "what a downstream user touches" probe.
Its src/lib.rs calls a representative slice of core::* / std::*
items; the test passes iff Aeneas can extract the resulting LLBC and
the result elaborates against our CoreModels shims. Failures here
mean the extraction pipeline is broken for some surface — they say
nothing about whether Rust and Lean agree behaviourally.
tests/rust_lean_equiv_test/ is the rust↔lean equivalence
framework. Each test pins one concrete observation and checks it
twice:
- Rust side —
cargo testruns every#[rust_lean_test] pub fn test_xxx() -> bool { ... }, generated by therust_lean_test_macrocrate, and asserts the body returnstrue. - Lean side —
gen_lean_tests.pyscanssource/src/**/*.rsfor the same attribute, finds each function's Aeneas-emitted name inFuns.lean, and emits one#guard <fully-qualified-name> == .ok trueintolean/RustLeanTests/LeanTests.lean. Lake fails the build for any guard whose Aeneas-evaluated body is notResult.ok true.
Both halves must pass. Together they say: "for this concrete input,
Rust std and the Lean translation of core_models give the same
answer." Disagreements show up as a failed #guard (Lean side knows
the truth) or a failed cargo test assertion (Rust side knows the
truth) — same code, different oracle.
When you add an item to core-models/src/core/foo.rs (or alloc/src/...):
- Add one property-based test (
proptest!) in the same file's#[cfg(test)] mod tests { ... }block. This is the broad randomized check that the model matches Rust std across the input domain. - Add several point tests in
tests/rust_lean_equiv_test/source/src/{core,alloc}/foo.rs(mirroring the source file's path undercore-models/). Each point test pins one concrete behaviour with a#[rust_lean_test]attribute. Cover boundaries: zero,MIN,MAX, empty, single element, signed/unsigned edges, theNone/Errcase, etc.
The point tests in (2) are what catch extraction bugs — the property-based test in (1) only knows about Rust, while the equivalence test exercises Aeneas's translation of the same item.
- Typed
None: Aeneas drops theTparameter ofNone::<T>in zero-arg functions, leavingOption.Nonepolymorphic in the extracted Lean. Use the helpers intests/rust_lean_equiv_test/source/src/helpers.rs(none_u8,none_i32, …) rather than inlineNone. Add anone_<T>if your type isn't covered. - Closures: tests that rely on
|x| ...(e.g.map,and_then,unwrap_or_else) currently extract poorly. Comment them out with// TODO(closure-extraction): .... - Excluded items: things listed in
CHARON_EXCLUDES/ALLOC_CHARON_EXCLUDES(core::mem::swap,core::slice::index::*, mostVecindexing,BinaryHeap, …) come from hand-written Lean definitions inlean/CoreModels/{Funs,Types}External.lean. Their equivalence tests live in the same file as the rest of the items in the same module (e.g.core::mem::swaptests live insource/src/core/mem.rs) — flagged with a section header noting they exercise a manual Lean def.
Add to your downstream lakefile.toml:
[[require]]
name = "CoreModels"
git = "https://github.com/cryspen/rust-core-models"
rev = "COMMIT_HASH_HERE"
subDir = "lean"Use aeneas with the -core-models-lib option (currently only available with the following aeneas branch: https://github.com/cryspen/aeneas/tree/core-models-option)
Then the Aeneas-extracted code that uses std::* types will resolve through this library's
core.* / alloc.* shims.
PRs welcome. Please:
- Run
cargo fmt --allandmake lean testsbefore opening a PR (CI enforces both). - For every new
core::*/alloc::*item:- Add one property-based test in the model crate's
#[cfg(test)]block (see existingproptest!blocks incore-models/src/core/option.rsetc. for the pattern). - Add several
#[rust_lean_test]point tests intests/rust_lean_equiv_test/source/src/...covering corner cases of the input. See the Testing section for the motivation and the pitfalls.
- Add one property-based test in the model crate's
- If your item is excluded from extraction (added to
CHARON_EXCLUDES), the equivalence tests still go in the file that mirrors the item'score::*/alloc::*location — flag them with a section header like// ----- foo (manually defined in Lean, not extracted) -----so a reader knows the Lean side is hitting a hand-written definition inlean/CoreModels/FunsExternal.leanrather than the extraction.
[fill in]