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.
COVERAGE.md reports, per top-level module, how much of the
real core / alloc public API the model crates provide. It is generated by
tools/core-coverage (comparing rustdoc JSON of the
real std crates against the model crates). Run make coverage to regenerate it
(it also runs as part of the default make target). The numbers are an
approximate, periodically-refreshed snapshot — not a CI-enforced invariant,
because the model's rustdoc (via hax-lib proc macros) isn't bit-reproducible
across machines.
.
├── 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)
├── tools/ # auxiliary tooling
│ └── core-coverage/ # generates COVERAGE.md (rustdoc-JSON based)
├── COVERAGE.md # per-module core/alloc coverage report (generated)
├── 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.
See lean/README.md
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]