This guide covers how to run, extend, and understand the Creusot formal
verification proof suite for elicitation.
Current status: 281 SMT goals proved across 13 de-trusting batches. The
proof suite began as all-#[trusted] scaffolding and has been progressively
strengthened so that Alt-Ergo/cvc5 discharge real proof obligations for the
majority of contract types. See Current Status for the
complete picture.
For Creusot applied specifically to
VerifiedStateMachinetypes, seeCREUSOT_FOR_VSMS.md.
Creusot is a deductive verification
tool for Rust. It translates Rust code with contracts (#[requires],
#[ensures], #[invariant]) into WhyML for discharge via the Why3 platform
and SMT solvers (Alt-Ergo, Z3, CVC5).
The contract language is Pearlite — a pure, mathematical subset of Rust used in spec annotations. Pearlite is evaluated at the type-logic level, not at runtime.
Creusot 0.10.x is not on crates.io. Clone and build it locally:
git clone https://github.com/creusot-rs/creusot.git ~/repos/creusot
cd ~/repos/creusot
./INSTALLThis installs cargo-creusot and creusot-rustc into your PATH via rustup.
The justfile automates this:
just setup # installs creusot among other toolsOn Arch/Manjaro, just setup installs the opam prerequisite via pacman
when it is missing.
cargo creusot version
# Creusot version: 0.10.x
# Rust toolchain: nightly-YYYY-MM-DDcreusot-std is not published to crates.io. The workspace Cargo.toml uses
a path dependency:
# Cargo.toml (workspace root)
creusot-std = { path = "/home/erik/repos/creusot/creusot-std" }If you clone creusot to a different location, update this path.
cargo creusot prove bundles two distinct operations:
- Translation —
cargo creusotinvokescreusot-rustcto translate Rust → Why3/COMA. Output goes toverif/{crate}_rlib/. - Proving —
why3find provedischarges the COMA goals via SMT solvers (Alt-Ergo, CVC5, Z3). Proof caches are stored asverif/**/{fn}/proof.json.
You can run them together (cargo creusot prove) or separately.
Important for downstream VSM crates: use the workspace proof-crate pattern. Generate Creusot companions into a dedicated proof crate and prove that crate with the normal workspace
cargo creusot proveflow viaelicitation prove --creusot. SeeCREUSOT_FOR_VSMS.md.
Why3 and why3find are installed under the Creusot prefix, not in your system PATH by default. Three variables are required:
export PATH="${HOME}/.local/share/creusot/bin:${PATH}"
export DUNE_DIR_LOCATIONS="why3find:lib:${HOME}/.local/share/creusot/share/why3find"
export WHY3CONFIG="${HOME}/.config/creusot/why3.conf"Add these to your shell profile or prefix every Creusot command with them.
The just recipes already set them.
| Binary | Location |
|---|---|
cargo creusot |
via rustup cargo extension |
creusot-rustc |
~/.local/share/creusot/bin/creusot-rustc |
why3find |
~/.local/share/creusot/bin/why3find |
why3 |
~/.local/share/creusot/bin/why3 |
| Solver config | ~/.config/creusot/why3.conf |
| Why3find libs | ~/.local/share/creusot/share/why3find/ |
creusot-std source |
~/repos/creusot/creusot-std/ |
cargo creusot -- -p elicitation_creusotThis runs the Rust → COMA translation for elicitation_creusot and writes
output to verif/elicitation_creusot_rlib/. No SMT solving happens. Use this
to check that contracts parse and the crate translates cleanly before running
a slow prove step.
For downstream VSM companions:
cargo creusot -- -p <pkg>PATH="${HOME}/.local/share/creusot/bin:${PATH}" \
DUNE_DIR_LOCATIONS="why3find:lib:${HOME}/.local/share/creusot/share/why3find" \
WHY3CONFIG="${HOME}/.config/creusot/why3.conf" \
why3find prove -p creusot verif/elicitation_creusot_rlib/gallery/level29/*.comaThis is the fastest iteration loop when debugging a specific function — no full cargo build.
PATH="${HOME}/.local/share/creusot/bin:${PATH}" \
DUNE_DIR_LOCATIONS="why3find:lib:${HOME}/.local/share/creusot/share/why3find" \
WHY3CONFIG="${HOME}/.config/creusot/why3.conf" \
cargo creusot prove -- -p elicitation_creusotcargo creusot prove -- -p PKG regenerates verif/PKG_rlib/ then proves
everything in verif/. The -p flag controls which crate is re-translated,
not which COMA files are proved.
For downstream VSM crates this is usually too broad. Prefer:
elicitation prove --creusotwhich performs:
cargo creusot initin a sanitized shadow workspacecargo creusot -- -p <pkg>for translationwhy3find proveon the generated companion root only
just check elicitation_creusotThis compiles with the regular Rust toolchain (no Creusot, no Why3). Use this to verify contracts parse, imports resolve, and Rust types are correct before spending time on a full prove run.
When why3find reports a goal unproved, find the COMA file:
find verif -path '*proofs/creusot/generated*' -o -path '*creusot/generated*'
cat verif/elicitation_creusot_rlib/gallery/level29/some_goal.comaLook for {false} in a value binding — the signature of an unmodeled call:
s6 = {false} any (* String::new() has no model *)
Cross-reference the binding number (s6) with the surrounding let-chain to
find which source expression produced it.
Creusot uses target/creusot/ separately from target/. Stale fingerprints
can cause COMA files not to regenerate after source changes:
rm -rf target/creusot/debug/.fingerprint/<pkg>-*
rm -rf target/creusot/debug/deps/lib<pkg>*cargo clean -p <pkg> only cleans the regular target, not the
Creusot target. Always delete fingerprints manually when debugging missing
or stale .coma files.
If elicitation prove --creusot prints only:
🔬 Running cargo creusot prove…
📝 Logging to ./prove_creusot.log
then your shell is using an older installed elicitation binary. Reinstall it:
cargo install --path crates/elicitation --features cli --bin elicitation --forcejust verify-creusot-trackedThis runs all 28 modules via cargo creusot and writes results to
creusot_verification_results.csv. Output:
[1/28] 🔬 Checking bools... ✅ PASS (1s)
[2/28] 🔬 Checking chars... ✅ PASS (0s)
...
[28/28] 🔬 Checking datetimes_jiff... ✅ PASS (19s)
📊 Compilation Summary:
Total: 28
Passed: 28 ✅
Failed: 0 ❌
just verify-creusot-summaryjust verify-creusot-listcargo creusot -- -p elicitation_creusot
cargo creusot -- -p elicitation_creusot --features uuid
cargo creusot -- -p elicitation_creusot --features regexThe -- separates cargo creusot arguments from the cargo build arguments
passed through to the underlying build.
crates/
├── elicitation/ # Main library (contract types)
│ └── src/verification/
│ ├── creusot_runner.rs # Orchestrator: runs & tracks all modules
│ └── types/ # Shared verification types
│ └── uuid_bytes.rs # Raw byte types (cfg(any(kani, creusot)))
└── elicitation_creusot/ # Proof crate (Creusot only)
└── src/
├── lib.rs # Module declarations
├── logic_fns.rs # Trusted logic wrappers (critical)
├── bools.rs # Core proofs (10 modules)
├── chars.rs
├── ...
├── ipaddr_bytes.rs # Byte-level proofs (5 + 1 unix)
├── macaddr.rs
├── ...
└── uuids.rs # Feature-gated proofs (10 modules)
└── uuid_bytes.rs
└── ...
This crate exists solely for Creusot verification. It has no runtime code. Key points:
creusot-stdas a dependency triggers Creusot'sToWhytranslation mode- The main
elicitationcrate must NOT depend oncreusot-std(it contains async code which cannot be translated to WhyML) - All proof functions are gated with
#[cfg(creusot)] - Feature-gated modules mirror the features in
elicitation
| Category | Count | Description |
|---|---|---|
| Core contract modules | 10 | bools, chars, collections, durations, floats, integers, networks, paths, strings, tuples |
| Byte-level wrappers | 6 | ipaddr_bytes, macaddr, mechanisms, socketaddr, utf8, pathbytes (unix-only) |
| Feature-gated | 10 | uuids, uuid_bytes, values, urls, urlbytes, regexes, regexbytes, datetimes_chrono, datetimes_time, datetimes_jiff |
When cargo creusot is run:
-
If the crate being compiled depends on
creusot-std→ToWhymode: full WhyML translation, contracts are checked,#[logic]functions are translated. This is what happens toelicitation_creusot. -
Otherwise →
WithoutContractsmode +--cfg=creusot: compiled as ordinary Rust withcfg(creusot)set. This is what happens toelicitationwhen it is compiled as a dependency ofelicitation_creusot.
This means elicitation (which has async code) is safe — it is compiled in
WithoutContracts mode and async code is never touched by the WhyML
translator.
Pearlite logic context (#[requires], #[ensures]) can ONLY call #[logic]
functions. Ordinary Rust methods are program functions and cannot be
called in logic context, even if they look pure.
// ❌ COMPILE ERROR: called program function in logic context
#[ensures(result.is_loopback())]
pub fn verify_loopback(addr: Ipv4Bytes) -> bool { ... }logic_fns.rs contains #[trusted] #[logic(opaque)] wrapper functions for
every elicitation method that needs to appear in contract specifications:
#[cfg(creusot)]
#[trusted]
#[logic(opaque)]
pub fn ipv4_is_loopback(_addr: Ipv4Bytes) -> bool {
dead // body is unreachable; semantics are trusted
}Then in proof files:
use crate::*; // brings logic_fns into scope
#[ensures(ipv4_is_loopback(result))]
pub fn verify_loopback() -> Ipv4Bytes { ... }Key rules for logic_fns.rs:
- Must use
#[logic(opaque)](NOT#[logic]) —deadrequires opaque - Must use
#[trusted]— there is no body to verify - Body must be
dead(Pearlite's unreachable term) - All parameters prefixed with
_(unused) - Everything gated with
#[cfg(creusot)]
| Wrapper | Wraps | Type |
|---|---|---|
ipv4_octets(addr) |
Ipv4Bytes::octets() |
→ [u8; 4] |
ipv4_is_loopback(addr) |
Ipv4Bytes::is_loopback() |
→ bool |
ipv4_is_unspecified(addr) |
Ipv4Bytes::is_unspecified() |
→ bool |
ipv4_is_broadcast(addr) |
Ipv4Bytes::is_broadcast() |
→ bool |
ipv4_is_multicast(addr) |
Ipv4Bytes::is_multicast() |
→ bool |
ipv6_* |
Ipv6Bytes methods |
various |
mac_* |
MacAddr methods |
various |
v4_port(addr) |
SocketAddrV4Bytes::port() |
→ u16 |
v6_port(addr) |
SocketAddrV6Bytes::port() |
→ u16 |
utf8_len(s) |
Utf8Bytes::len() |
→ usize |
utf8_is_empty(s) |
Utf8Bytes::is_empty() |
→ bool |
path_len(p) |
PathBytes::len() |
→ usize |
path_is_empty(p) |
PathBytes::is_empty() |
→ bool |
i8pos_inner(v) |
I8Positive::into_inner() |
→ i8 |
i8pos_get(v) |
I8Positive::get() |
→ i8 |
These patterns come up repeatedly in the proof files.
Use @ to convert program-world types to Pearlite math types:
// Slice/array → Seq<T>
bytes@.len() // ✓ bytes.len() would be a program function call
// Vec → Seq<T>
options@.len() // ✓
options@[i@] // index with usize i also needing @
// usize → Int (mathematical integer)
N@ // where N is a usize const generic
// Comparison: when mixing usize result and Int
path_len(path)@ == bytes@.len()These are program functions. Use match expressions instead:
// ❌ Program function
#[ensures(result.is_ok())]
// ✓ Pearlite match
#[ensures(match result { Ok(_) => true, Err(_) => false })]
#[ensures(match result { Err(_) => true, Ok(_) => false })]// exists
#[ensures(exists<i: usize> options@[i@] == Some(value))]
// forall
#[ensures(forall<i: usize> i < options@.len() ==> options@[i@].is_some())]Literals in contracts over usize-typed params work without annotation, but
when mixing with Int-valued expressions, be explicit:
#[requires(bytes@.len() > 0)] // ✓ 0 is Int here
#[requires(bytes@.len() <= MAX_LEN@)] // ✓ coerce const with @// crates/elicitation_creusot/src/mytype.rs
use crate::*;
#[cfg(creusot)]
use elicitation::MyType;
/// Verify: valid input is accepted
#[cfg(creusot)]
#[requires(/* precondition */)]
#[ensures(match result { Ok(_) => true, Err(_) => false })]
pub fn verify_mytype_valid(input: SomeInput) -> Result<MyType, MyError> {
MyType::new(input)
}// crates/elicitation_creusot/src/lib.rs
#[cfg(feature = "myfeature")]
mod mytype;For feature-gated modules, add the feature to Cargo.toml:
[features]
myfeature = ["dep:mylib", "elicitation/myfeature"]// crates/elicitation/src/verification/creusot_runner.rs
// In CreusotModule::all():
Self::with_feature("mytype", "myfeature"),cargo creusot -- -p elicitation_creusot --features myfeatureA method in #[ensures]/#[requires] is not a #[logic] function.
Fix: Add a #[trusted] #[logic(opaque)] wrapper to logic_fns.rs.
#[pure] is not a Creusot attribute. Remove it. Use #[logic] for pure
math functions or #[trusted] for trusted axioms.
exists<...> is only valid inside #[ensures]/#[requires]/#[logic]
bodies. Move the quantifier into a contract annotation.
Often caused by blanket From impls on Error types. Add explicit type
annotations or .map_err(|e| MyError::from(e)).
Types gated with #[cfg(kani)] in elicitation are invisible to Creusot.
Fix by extending the gate: #[cfg(any(kani, creusot))] in both the type
definition and its pub-use export.
Newtype wrappers (e.g., UuidV4Bytes(UuidBytes)) only expose methods
explicitly defined on impl UuidV4Bytes. Delegation to the inner type
requires .get().method() or .into_inner().method().
cargo creusot sets RUSTC=creusot-rustc but doesn't set LD_LIBRARY_PATH.
The creusot_runner.rs resolves this automatically by querying
cargo creusot version for the toolchain channel and finding the rustup lib
directory. If running manually and hitting this error:
export LD_LIBRARY_PATH=$(rustup run nightly-YYYY-MM-DD rustc --print sysroot)/lib:$LD_LIBRARY_PATHThe Creusot suite requires the locally-built creusot-std path dependency,
so it is intentionally excluded from the standard workspace checks:
just check-all # excludes elicitation_creusot
just verify-creusot-tracked # run separately, requires creusot installedOn CI, Creusot verification is an optional step gated on creusot being in
PATH. The just status recipe reports whether Creusot is available.
The proof suite evolved through 13 batches from all-#[trusted] scaffolding to
real SMT proofs. The core technique: use extern_spec! blocks as trusted
axioms about constructor behavior, then remove #[trusted] from the
verify_* witness functions so the solver discharges real obligations.
// extern_specs.rs — trusted axiom about constructor behavior
extern_spec! {
impl<const MAX_LEN: usize> Utf8Bytes<MAX_LEN> {
#[ensures(bytes@.len() == 0 ==> match result { Ok(_) => true, Err(_) => false })]
#[ensures(bytes@.len() > MAX_LEN@ ==> match result { Err(_) => true, Ok(_) => false })]
fn from_slice(bytes: &[u8]) -> Result<Utf8Bytes<MAX_LEN>, ValidationError>;
}
}
// utf8.rs — now a REAL proof obligation, not trusted
#[requires(true)]
#[ensures(match result { Ok(_) => true, Err(_) => false })]
// No #[trusted] — Alt-Ergo discharges this
pub fn verify_utf8_empty_valid() -> Result<Utf8Bytes, ValidationError> {
Utf8Bytes::from_slice(&[])
}The extern_spec says "I trust the constructor does X". The verify function
says "and here is a witness that satisfies that contract". The solver checks
the witness against the spec — no #[trusted] needed on the witness.
Functions with no extern_spec generate opaque Why3 contracts. Calling them
from a verified function produces unprovable obligations even for trivial
postconditions. The fix is always #[ensures(true)] at minimum:
// Without this, any call to infallible_accessor() creates an unprovable goal
extern_spec! {
impl SocketAddrV4Bytes {
#[ensures(true)]
fn ip(&self) -> &Ipv4Bytes;
}
}b"http" in a witness function body compiles to a &'static [u8; 4] in MIR
— a pointer to a static allocation. Creusot cannot model static allocation
references:
Error: Unsupported constant value: Scalar(alloc) of type &[u8; 4]
Fix: Replace byte string literals with local char arrays:
// ❌ Fails: generates Scalar(alloc) in MIR
let bytes = b"http";
// ✅ Works: individual b'x' literals are scalar constants
let bytes = [b'h', b't', b't', b'p'];
let slice: &[u8] = &bytes;Individual b'x' character literals (not slices) are scalar constants and
are fully supported by Creusot.
f32 and f64 do not implement OrdLogic in Creusot's type system. Float
comparisons (>, <, >=, <=) cannot appear in #[ensures]/#[requires]
annotations or extern_spec postconditions. The float_ops.rs test in the
Creusot repo marks these as // FIXME / // NO_REPLAY. All floats.rs
functions remain #[trusted].
str::view() (the @ operator on &str) is #[logic(opaque)] in
creusot-std — meaning "hello"@ has unknown length in Pearlite. Only
String::new() (empty via result@ == Seq::empty()) is provable. String
witnesses based on non-empty literals ("hello", "short", etc.) remain
#[trusted].
Third-party crates (egui, ratatui) define types whose internals are opaque
to Creusot — the solver cannot see through dependency function bodies. To
verify our From wrapper logic, we use a three-layer trusted surface
pattern that minimises what is axiomatically trusted while enabling real
SMT-verified proofs.
Layer 1: Logic accessors — model field access in the logic domain
Layer 2: Extern specs / — link constructors to logic accessors
trusted ctors
Layer 3: Bridge functions — connect runtime reads to logic accessors
#[logic(opaque)] functions that represent field access purely in Pearlite.
The solver treats them as uninterpreted functions — it knows they exist but
cannot inspect their bodies.
#[trusted]
#[logic(opaque)]
pub fn padding_left(_p: ratatui::layout::Padding) -> u16 {
dead
}One accessor per field. Return type matches the field type (u16, u8,
i8, etc.).
For types with named constructor functions (e.g., Padding::new()), use
extern_spec! to axiomatise the constructor:
extern_spec! {
impl ratatui::layout::Padding {
#[ensures(padding_left(result) == left
&& padding_right(result) == right
&& padding_top(result) == top
&& padding_bottom(result) == bottom)]
fn new(left: u16, right: u16, top: u16, bottom: u16) -> Self;
}
}For types constructed via struct literals (no constructor function), Creusot cannot reason about foreign struct field assignment. Use a trusted constructor wrapper instead:
#[trusted]
#[ensures(corner_radius_nw(result) == nw && corner_radius_ne(result) == ne
&& corner_radius_sw(result) == sw && corner_radius_se(result) == se)]
pub fn make_corner_radius(nw: u8, ne: u8, sw: u8, se: u8) -> egui::CornerRadius {
egui::CornerRadius { nw, ne, sw, se }
}Trusted program functions that connect runtime field reads to logic accessors. These allow verified proof functions to read fields and have the solver reason about the result.
#[trusted]
#[ensures(padding_left(*p) == result)]
pub fn read_padding_left(p: &ratatui::layout::Padding) -> u16 {
p.left
}Key: bridge functions take &T references, but logic accessors take values.
Use *p (dereference) in the #[ensures] clause to match types.
With all three layers in place, the actual proof functions need no
#[trusted] annotation — the SMT solver verifies them:
/// Prove Padding roundtrip: construct → read fields → reconstruct.
#[requires(true)]
#[ensures(padding_left(result) == left && padding_right(result) == right
&& padding_top(result) == top && padding_bottom(result) == bottom)]
pub fn verify_ratatui_padding_roundtrip(
left: u16, right: u16, top: u16, bottom: u16,
) -> ratatui::layout::Padding {
let original = ratatui::layout::Padding::new(left, right, top, bottom);
let l = read_padding_left(&original);
let r = read_padding_right(&original);
let t = read_padding_top(&original);
let b = read_padding_bottom(&original);
ratatui::layout::Padding::new(l, r, t, b)
}The proof inlines the wrapper logic (field reads + constructor calls)
instead of calling From::from(), because From::from() is an external
function call that Creusot cannot see through.
- Logic accessors return the program type (e.g.,
u16), notInt - Comparing two values of the same type:
padding_left(result) == left✓ - Comparing logic result to an integer literal:
padding_left(result)@ == 1✓ (use@to convert both sides toInt) - Bridge functions:
#[ensures(padding_left(*p) == result)]— bothu16, no@needed
| Field type | Can de-trust? | Reason |
|---|---|---|
u8, u16, u32, u64 |
✅ Yes | Integer fields fully supported |
i8, i16, i32, i64 |
✅ Yes | Signed integers supported |
f32, f64 |
❌ No | No OrdLogic, float comparison opaque |
String, &str |
❌ No | str::view() opaque |
| Bitflags | ❌ No | contains() is opaque |
| Type | Fields | Goals | Notes |
|---|---|---|---|
ratatui Padding |
u16 ×4 |
8 | Padding::new() via extern_spec |
ratatui Margin |
u16 ×2 |
6 | Margin::new() via extern_spec |
egui Color32 |
u8 ×4 |
8 | from_rgba_unmultiplied() via extern_spec |
egui CornerRadius |
u8 ×4 |
8 | Struct literal via trusted ctor |
egui Margin |
i8 ×4 |
8 | Struct literal via trusted ctor |
| Total | 38 |
Creusot uses its own target directory (target/creusot/), separate from the
regular cargo target. When modifying proof source files, .coma files may
not regenerate due to stale fingerprints. Force recompilation:
rm -rf target/creusot/debug/.fingerprint/elicitation_creusot-*
rm -rf target/creusot/debug/deps/libelicitation_creusot*
cargo creusot -- -p elicitation_creusot --features <feature>cargo clean -p elicitation_creusot only cleans the regular target,
not the Creusot target. Always delete fingerprints manually when debugging
missing .coma files.
| Metric | Value |
|---|---|
| Total modules | 32 |
| Passing (compilation) | 32 ✅ |
| SMT goals proved | 281 |
| De-trusting batches | 13 |
| Creusot version | 0.10.x |
All 32 modules compile. 281 proof obligations are now discharged by Alt-Ergo rather than accepted on trust.
| Module | Goals | Notes |
|---|---|---|
bools |
✅ all | First de-trusted module |
chars |
✅ all | |
integers |
✅ all | 47 functions |
durations |
✅ all | |
ipaddr_bytes |
✅ all | Uses logic_fns for octet/loopback wrappers |
macaddr |
✅ all | |
mechanisms |
✅ all | |
http |
✅ all | StatusCodeValid behind reqwest feature |
utf8 |
✅ all | |
networks |
✅ all | IPv4/IPv6 private/public/loopback |
socketaddr |
✅ all | Accessors via #[ensures(true)] extern_specs |
urlbytes |
✅ all | Byte literals rewritten to char arrays |
pathbytes |
✅ all | |
regexbytes |
✅ all | |
collections |
partial | Box/Arc/Rc/Array proved; HashMap/BTree/LinkedList trusted |
strings |
partial | String::new() invalid case proved; literals opaque |
tuples |
✅ all | All #[ensures(true)] — trivially proved |
ratatui_types |
partial | Padding/Margin roundtrip+concrete proved (14 goals); select enums, Style, Borders trusted (string/bitflag opacity) |
egui_types |
partial | Color32/CornerRadius/Margin roundtrip+concrete proved (24 goals); float composites, select enums trusted |
| File | Count | Reason |
|---|---|---|
serde_boundary.rs |
35 | No formal model for serde_json deserialization |
floats.rs |
12 | f32/f64 missing OrdLogic in Creusot |
collections.rs |
11 | HashMap/BTreeMap/LinkedList — no ShallowModel in creusot-std |
urls.rs |
10 | Runtime URL parsing (url crate) |
regexes.rs |
10 | Runtime regex compilation |
paths.rs |
8 | Filesystem existence checks |
datetimes_* |
14 | Runtime datetime comparison |
values.rs |
6 | serde_json::Value discriminant — no Why3 model |
uuids.rs |
4 | Uuid::parse_str is opaque |
strings.rs |
3 | Non-empty string literals — str::view() opaque |
ratatui_types.rs |
28 | Select enum string roundtrips, Style/Modifier bitflags, Borders |
egui_types.rs |
~60 | Select enum string roundtrips, float-field composites (Pos2, Vec2, Rect, Stroke, Shadow, FontId) |
clap_types.rs |
16 | String literal opacity + third-party builder types (see below) |
sqlx_types.rs |
9 | String literal opacity (roundtrips/rejection) + #[non_exhaustive] From totality; label_count proofs de-trusted |
The clap_types.rs module contains 22 proof functions, all currently #[trusted].
There is a partial de-trusting path available for the label count invariants.
Each Select enum proves labels().len() == options().len(). This is a pure
length comparison on two Vec<T> values. Vec has a ShallowModel as Seq<T>
in creusot-std, so length proofs are tractable.
De-trusting path:
Write extern_spec! blocks in extern_specs.rs specifying the lengths of
labels() and options() for each type:
extern_spec! {
impl elicitation::Select for clap::ColorChoice {
#[ensures(result@.len() == 3)]
fn labels() -> Vec<String>;
#[ensures(result@.len() == 3)]
fn options() -> Vec<clap::ColorChoice>;
}
}Then remove #[trusted] from verify_color_choice_label_count() — Alt-Ergo
will discharge the goal 3 == 3 trivially.
This approach works for all 5 Select enums (ColorChoice=3, ArgAction=8, ValueSource=3, ErrorKind=17, ValueHint=9 variants) — up to 5 de-trusted goals.
Caution: Requires clap-types feature in elicitation_creusot and the
extern_spec! macro to accept trait impls on foreign types. Verify the syntax
compiles before removing #[trusted].
The from_label roundtrip and unknown rejection proofs all depend on string
literal matching. This hits the string literal content wall:
str::view()is#[logic(opaque)]in creusot-std — the solver cannot know what"Auto (detect terminal)"contains.
Even with an extern_spec for from_label, you cannot write:
#[ensures(result.is_some())] // ❌ Cannot prove — string content opaque
fn from_label(label: &str) -> Option<Self>;These 16 proof functions remain #[trusted] until creusot-std provides a
ShallowModel for &str that exposes character content to the solver.
The 6 verify_clap_*_trusted() functions are explicit architectural decisions
that clap builder types (Arg, ArgGroup, Command, Id, PossibleValue,
ValueRange) are trusted third-party types. These will never be de-trusted —
they are axioms by design, not implementation gaps.