Hax is a powerful, production-ready tool for high-assurance translations of Rust code into formal verification languages. It bridges the gap between high-performance systems programming and mathematical proof, enabling developers to write idiomatic, verifiable Rust code and automatically translate it to proof assistants like F*, Lean4, Coq/Rocq, and ProVerif.
This document serves as the comprehensive top-level guide to the entire hax ecosystem, covering the core library (hax-lib), the procedural macro system (hax-lib-macros), the command-line toolchain (cargo-hax), the translation engine (hax-engine), and all supported verification backends.
Unlike traditional verification approaches that require:
- Rewriting your program in a specialized verification language
- Maintaining two separate codebases (implementation + specification)
- Learning complex proof assistant syntax from scratch
Hax enables you to:
- Write your code once in idiomatic Rust
- Add formal specifications using Rust macros and attributes
- Automatically extract to multiple proof assistants
- Maintain a single source of truth for both implementation and verification
- Leverage Rust's type system and borrow checker alongside formal proofs
- Generate high-performance binaries and formally verified models from the same source
This README provides a complete overview of all hax capabilities. For deeper dives into specific topics:
- hax-lib-api.md: Exhaustive API reference for every function, macro, and type
- macro-system-complete.md: Complete documentation of all macros and their implementations
- examples-patterns.md: Comprehensive examples and design patterns from basic to advanced
- verification-techniques.md: In-depth guide to proof strategies and verification patterns
- cli-backends.md: Complete CLI reference and backend-specific documentation
- internal-architecture.md: Deep dive into hax's internal implementation
- errors-troubleshooting.md: Complete error code reference and troubleshooting guide
- INDEX.md: Documentation index and navigation guide
-
- Key Features
- Architecture and Data Flow
- Verification Workflow
- Design Philosophy
-
- Component Overview
- hax-lib (Core Library)
- hax-lib-macros (Procedural Macros)
- cargo-hax (CLI Tool)
- hax-engine (Translation Engine)
- Backend Translators
-
- Installation Methods
- Project Setup
- First Verified Program
- Development Workflow
-
Type System and Mathematical Abstractions
- Mathematical Integers (Int)
- Logical Propositions (Prop)
- Abstraction and Concretization
- Type Conversions and Lifting
-
- assert! - Provable Assertions
- assume! - Trusted Assumptions
- assert_prop! - Logical Propositions
- debug_assert! - Runtime-Only Checks
- Best Practices and Safety
-
- Universal Quantification (forall!)
- Existential Quantification (exists!)
- Logical Implication (implies!)
- Proposition Combinators
- Writing Complex Specifications
-
- Refinement Type Theory
- The Refinement Trait
- Creating Custom Refinements
- Refinement Type Patterns
- Advanced Usage
-
- Preconditions (#[requires])
- Postconditions (#[ensures])
- Multiple Contracts
- Contract Composition
- Advanced Contract Patterns
-
- Loop Invariants (#[loop_invariant])
- Termination Measures (#[decreases])
- Ghost Variables
- Invariant Design Patterns
- Complex Loop Examples
-
- Opacity and Modularity (#[opaque])
- Lemmas and Auxiliary Proofs (#[lemma])
- Visibility Control (#[exclude], #[include])
- Type-Level Specifications
- Backend-Specific Attributes
-
- cargo-hax Overview
- Complete Command Reference
- Configuration Options
- Environment Variables
- Project Organization
-
- F* (Stable)
- Lean4 (Active Development)
- Coq/Rocq (Experimental)
- ProVerif (Protocol Verification)
- SSProve and EasyCrypt (Cryptographic Proofs)
- Backend Comparison Matrix
-
- Basic Verification Patterns
- Cryptographic Algorithms
- Mathematical Operations
- Data Structures
- Protocol Verification
-
- Backend-Specific Code Injection
- Proof Optimization
- Modularity and Scaling
- Performance Considerations
- Integration with External Tools
-
- Core Types and Traits
- All Macros (Declarative and Procedural)
- Functions and Methods
- Constants and Type Aliases
-
- External Links
- Contributing Guidelines
- Roadmap
- License Information
Hax enables you to write normal Rust code with added specifications:
use hax_lib as hax;
/// A simple verified function with preconditions and postconditions
#[hax::requires(x < 100 && y < 100)]
#[hax::ensures(|result| result == x + y && result > x && result > y)]
pub fn verified_add(x: u32, y: u32) -> u32 {
// This is regular Rust code
let result = x + y;
// Add assertions to help the verifier
hax::assert!(result > x);
hax::assert!(result > y);
result
}This code:
- Compiles to a normal, fast Rust binary
- Extracts to F*/Lean/Coq for formal verification
- Proves absence of overflows, panics, and logic errors
Hax can prove that your code will never panic:
use hax_lib as hax;
use hax_lib::int::Int;
/// Proven safe: cannot panic due to division by zero
#[hax::requires(divisor != 0)]
#[hax::ensures(|result|
Int::from(result) * Int::from(divisor) + Int::from(dividend % divisor)
== Int::from(dividend)
)]
pub fn safe_divide(dividend: u32, divisor: u32) -> u32 {
dividend / divisor // Provably safe due to precondition
}
/// Alternative: using Option for total functions
#[hax::ensures(|result| match result {
Some(val) => divisor != 0 &&
Int::from(val) * Int::from(divisor) == Int::from(dividend),
None => divisor == 0
})]
pub fn checked_divide(dividend: u32, divisor: u32) -> Option<u32> {
dividend.checked_div(divisor)
}Reason about your code using unbounded mathematical integers:
use hax_lib::int::{Int, ToInt};
/// Verify correctness using mathematical integers
#[hax::requires(
Int::from(x) + Int::from(y) <= Int::from(u32::MAX) &&
Int::from(x) + Int::from(y) >= Int::from(u32::MIN)
)]
#[hax::ensures(|result| Int::from(result) == Int::from(x) + Int::from(y))]
pub fn overflow_safe_add(x: u32, y: u32) -> u32 {
// The preconditions prove this operation is safe
x + y
}
/// Alternative: using Int directly in specifications
fn demonstrate_int_arithmetic() {
// Create mathematical integers (unbounded)
let a = hax::int!(1_000_000_000_000);
let b = hax::int!(2_000_000_000_000);
let c = a + b; // No overflow in mathematical integers
hax::assert!(c == hax::int!(3_000_000_000_000));
// Convert back to machine integers (with bounds checking)
let concrete: u64 = c.concretize(); // Verified to fit
}Define custom types with built-in invariants:
use hax_lib as hax;
/// A non-zero integer type
#[hax::refinement_type]
pub struct NonZero {
#[hax::refinement]
value: i32,
}
impl hax::Refinement for NonZero {
type InnerType = i32;
/// The invariant: value must not be zero
fn invariant(value: i32) -> hax::Prop {
hax::Prop::from(value != 0)
}
}
/// This function is provably safe from division by zero
/// just from the type signature!
pub fn divide_by_nonzero(dividend: i32, divisor: NonZero) -> i32 {
// The type system guarantees *divisor != 0
dividend / *divisor
}
/// Smart constructor with runtime check
impl NonZero {
#[hax::ensures(|result| match result {
Some(nz) => *nz == value && value != 0,
None => value == 0
})]
pub fn new(value: i32) -> Option<Self> {
if value != 0 {
Some(NonZero { value })
} else {
None
}
}
}Use opacity to hide implementation details and scale verification:
/// Mark this function as opaque to verifier
#[hax::opaque]
#[hax::requires(n > 0)]
#[hax::ensures(|result| result > 0)]
fn expensive_computation(n: u32) -> u32 {
// Complex implementation that's verified once
// Callers only see the contract, not the implementation
// This makes verification much faster
(0..n).sum()
}
/// This function's verification doesn't need to re-verify
/// expensive_computation's implementation
fn caller(x: u32) -> u32 {
if x > 0 {
let result = expensive_computation(x);
hax::assert!(result > 0); // Follows from contract
result
} else {
1
}
}Extract the same Rust code to different proof assistants:
# Extract to F* for SMT-based verification
cargo hax into fstar
# Extract to Lean for interactive proofs and panic-freedom
cargo hax into lean
# Extract to Coq for functional verification
cargo hax into coq
# Extract to ProVerif for protocol analysis
cargo hax into pro-verifThe hax system consists of multiple components working together in a pipeline:
┌─────────────────────────────────────────────────────────────┐
│ Rust Source Code │
│ (with hax-lib annotations) │
└────────────────────────┬────────────────────────────────────┘
│
│ cargo hax into <backend>
▼
┌─────────────────────────────────────────────────────────────┐
│ Rust Compiler (rustc) │
│ - Type checking and inference │
│ - Borrow checking │
│ - Macro expansion │
│ - HIR (High-level IR) generation │
│ - THIR (Typed High-level IR) generation │
└────────────────────────┬────────────────────────────────────┘
│
│ THIR + Metadata
▼
┌─────────────────────────────────────────────────────────────┐
│ Hax Frontend (Rust) │
│ - THIR extraction via rustc plugin │
│ - Serialization to JSON/CBOR │
│ - Type information preservation │
│ - Macro metadata collection │
└────────────────────────┬────────────────────────────────────┘
│
│ JSON/CBOR AST
▼
┌─────────────────────────────────────────────────────────────┐
│ Hax Engine (OCaml/Rust) │
│ - AST simplification and normalization │
│ - Pattern desugaring │
│ - Trait resolution │
│ - Type erasure/elaboration │
│ - Control flow analysis │
│ - Backend-agnostic transformations │
└────────────────────────┬────────────────────────────────────┘
│
│ Simplified AST
▼
┌─────────────────────────────────────────────────────────────┐
│ Backend Translators │
│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ F* │ Lean4 │ Coq │ ProVerif │ SSProve │ │
│ │ (SMT) │ (Tactic)│ (Rocq) │ (Proto) │ (Crypto) │ │
│ └──────────┴──────────┴──────────┴──────────┴──────────┘ │
└────────────────────────┬────────────────────────────────────┘
│
│ Generated Code
▼
┌─────────────────────────────────────────────────────────────┐
│ Verification Tools │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ F*.exe, Lean, coqc, proverif, easycrypt, ... │ │
│ │ - SMT solving (Z3, CVC5, etc.) │ │
│ │ - Interactive proof development │ │
│ │ - Symbolic execution │ │
│ │ - Cryptographic game proofs │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
1. Write Rust Code
├─→ Use hax-lib types (Int, Prop)
├─→ Add specifications (#[requires], #[ensures])
├─→ Include assertions (assert!, assume!)
└─→ Define refinement types as needed
2. Test Normally
├─→ cargo test (standard Rust tests)
├─→ cargo run (standard execution)
└─→ Debug with normal Rust tools
3. Extract and Verify
├─→ cargo hax into fstar (or other backend)
├─→ Review generated code
├─→ Run verification tool (fstar.exe, lean, etc.)
└─→ Fix any proof failures
4. Iterate
├─→ Strengthen specifications
├─→ Add loop invariants
├─→ Refine types
└─→ Improve proofs
5. Production
├─→ cargo build --release (optimized binary)
├─→ Formally verified code
└─→ High assurance
# 1. Set up project
cargo new --lib my-verified-crate
cd my-verified-crate
# 2. Add hax-lib dependency
cat >> Cargo.toml << EOF
[dependencies]
hax-lib = { version = "*", features = ["macros"] }
EOF
# 3. Write verified code in src/lib.rs
cat > src/lib.rs << 'EOF'
use hax_lib as hax;
#[hax::requires(x < 100)]
#[hax::ensures(|result| result > x)]
pub fn increment(x: u32) -> u32 {
x + 1
}
EOF
# 4. Test normally
cargo test
# 5. Extract to F*
cargo hax into fstar
# 6. Verify with F*
cd proofs/fstar/extraction
fstar.exe --include $HACL_HOME/lib *.fst
# 7. If verification fails, iterate on specifications
# Edit src/lib.rs, then repeat steps 4-6Hax doesn't force you to write verification-specific code. It works with normal Rust:
// ✅ GOOD: Normal Rust with added specifications
#[hax::requires(v.len() > 0)]
pub fn first_element(v: &[u32]) -> u32 {
v[0] // Normal array indexing
}
// ❌ BAD: Inventing special verification syntax
// (This is what other tools might do, but hax avoids it)
pub fn first_element(v: &[u32]) -> u32 {
get_verified_element(v, 0) // No special functions needed
}Specifications serve as executable, verified documentation:
/// Returns the maximum element in the array.
///
/// # Specification
/// - Requires: The array is non-empty
/// - Ensures: Result is greater than or equal to all elements
/// - Ensures: Result is equal to at least one element
#[hax::requires(arr.len() > 0)]
#[hax::ensures(|result|
forall(|i: usize| implies(i < arr.len(), arr[i] <= result)) &&
exists(|i: usize| i < arr.len() && arr[i] == result)
)]
pub fn find_max(arr: &[u32]) -> u32 {
// Implementation...
}Start simple, add details incrementally:
// Level 1: Basic implementation
pub fn safe_add(x: u32, y: u32) -> Option<u32> {
x.checked_add(y)
}
// Level 2: Add type-level specification
use hax_lib::int::Int;
#[hax::ensures(|result| match result {
Some(sum) => Int::from(sum) == Int::from(x) + Int::from(y),
None => Int::from(x) + Int::from(y) > Int::from(u32::MAX)
})]
pub fn safe_add(x: u32, y: u32) -> Option<u32> {
x.checked_add(y)
}
// Level 3: Add overflow-free variant
#[hax::requires(Int::from(x) + Int::from(y) <= Int::from(u32::MAX))]
#[hax::ensures(|result| Int::from(result) == Int::from(x) + Int::from(y))]
pub fn guaranteed_add(x: u32, y: u32) -> u32 {
x + y
}Large proofs scale through information hiding:
// Verify complex function once
#[hax::opaque]
#[hax::ensures(|result| result >= input)]
fn complex_transform(input: u32) -> u32 {
// 1000 lines of complex logic
// Verified once with all its details
// ...
}
// Use it many times without reverification cost
fn client_code_1(x: u32) -> u32 {
let y = complex_transform(x);
hax::assert!(y >= x); // Fast: uses contract only
y + 1
}
fn client_code_2(x: u32) -> u32 {
let y = complex_transform(x);
hax::assert!(y >= x); // Fast: uses contract only
y * 2
}The hax project comprises several tightly integrated components, each with a specific role in the verification pipeline.
hax/
├── hax-lib/ # Core library (runtime + specifications)
│ ├── src/
│ │ ├── lib.rs # Main exports
│ │ ├── int.rs # Mathematical integers
│ │ ├── refinement.rs # Refinement type traits
│ │ └── ...
│ └── macros/ # Declarative macros
│
├── hax-lib-macros/ # Procedural macros
│ ├── src/
│ │ ├── lib.rs # Macro definitions
│ │ ├── attrs.rs # Attribute macros
│ │ └── ...
│ └── Cargo.toml
│
├── cli/ # Command-line interface
│ ├── driver/ # cargo-hax implementation
│ ├── subcommands/ # Subcommand handlers
│ └── options/ # CLI option parsing
│
├── engine/ # Translation engine (OCaml)
│ ├── lib/ # Core engine library
│ ├── backends/ # Backend translators
│ │ ├── fstar/ # F* backend
│ │ ├── lean/ # Lean backend
│ │ ├── coq/ # Coq backend
│ │ └── ...
│ └── utils/ # Utility modules
│
└── frontend/ # Rust frontend
└── exporter/ # THIR extraction
hax-lib is the runtime library that provides:
- Mathematical types (
Int,Prop) - Abstraction traits (
Abstraction,Refinement) - Declarative macros (
assert!,assume!,forall!,exists!) - Backend-specific helpers
// Core module organization
pub mod hax_lib {
// Main types
pub use int::Int; // Mathematical integers
pub use props::Prop; // Logical propositions
// Abstraction system
pub use abstraction::{
Abstraction, // Trait for lifting to abstract types
Concretization, // Trait for lowering to concrete types
RefineAs, // Refinement conversion
};
// Refinement types
pub use refinement::Refinement;
// Declarative macros
pub use macros::{
assert, // Proof obligation
assume, // Unsafe assumption
assert_prop, // Logical proposition assertion
forall, // Universal quantifier
exists, // Existential quantifier
implies, // Implication
int, // Int literal
};
// Procedural macros (re-exported from hax-lib-macros)
pub use hax_lib_macros::{
requires, // Precondition attribute
ensures, // Postcondition attribute
loop_invariant, // Loop invariant
decreases, // Termination measure
opaque, // Opacity marker
lemma, // Lemma designation
refinement_type, // Refinement type macro
// ... more macros
};
// Backend-specific modules
pub mod fstar; // F* helpers
pub mod lean; // Lean helpers
pub mod coq; // Coq helpers
}Why a separate library? The hax-lib crate serves multiple purposes:
- Type Definitions: Provides
Int,Prop, and other specification types - Trait Abstractions: Defines
Abstraction,Refinement, etc. - Macro Foundations: Declarative macros that procedural macros build upon
- Runtime Stubs: No-op implementations for regular compilation
- Backend Adaptations: Backend-specific utilities and helpers
hax-lib behaves differently depending on compilation context:
// In normal compilation (cargo build):
// - Int operations are no-ops or simple wrappers
// - Assertions compile to debug_assert! or nothing
// - Specifications are ignored
pub fn example(x: u32) -> u32 {
hax::assert!(x < 100); // No-op in release, debug_assert! in debug
x + 1
}
// In hax extraction (cargo hax into fstar):
// - Int is unbounded mathematical integer
// - Assertions become proof obligations
// - Specifications are extracted as formal contracts
// Generated F*:
// val example (x: u32) : Pure u32
// (requires x <. 100ul)
// (ensures fun result -> result >. x)hax-lib-macros is a separate proc-macro crate providing attribute and function-like macros.
Rust requires procedural macros to be in their own crate with proc-macro = true:
# hax-lib-macros/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"This separation allows:
- Independent versioning
- Focused dependencies
- Clear separation of concerns
- Faster compile times (can be cached separately)
The procedural macros fall into several categories:
// 1. FUNCTION CONTRACTS
#[requires(precondition)] // Preconditions
#[ensures(|result| postcondition)] // Postconditions
#[lemma] // Mark as lemma
// 2. LOOP SPECIFICATIONS
#[loop_invariant(property)] // Invariant preserved
#[loop_decreases(measure)] // Termination measure
// 3. TYPE SPECIFICATIONS
#[refinement_type] // Create refinement type
#[refinement] // Mark refinement field
// 4. VISIBILITY AND EXTRACTION
#[opaque] // Hide implementation
#[exclude] // Don't extract
#[include] // Force extraction
// 5. BACKEND-SPECIFIC
#[fstar::options("...")] // F* options
#[fstar::verification_status(lax)] // Skip verification
#[lean::type("...")] // Lean type override
// 6. PROTOCOL (ProVerif)
#[protocol_messages] // Define protocol messages
#[pv_constructor] // ProVerif constructor
#[process_init] // Process definitionFor complete documentation, see macro-system-complete.md.
cargo-hax is the CLI tool that orchestrates the verification pipeline.
cargo hax into fstar
│
├─→ Parse CLI arguments
│
├─→ Detect Rust toolchain
│
├─→ Set up rustc driver
│
├─→ Invoke rustc with special flags
│ └─→ --cfg hax
│ └─→ Load hax frontend plugin
│
├─→ Extract THIR from rustc
│ └─→ Serialize to JSON/CBOR
│
├─→ Invoke hax-engine
│ ├─→ Parse AST
│ ├─→ Transform
│ └─→ Generate F* code
│
└─→ Write output files
# Basic extraction
cargo hax into fstar
# With options
cargo hax into fstar \
--output-dir ./proofs \
--z3rlimit 200 \
--include "crypto::*" \
--exclude "tests::*"
# Extract AST only (for debugging)
cargo hax json --pretty
# Show what would be extracted
cargo hax into fstar --dry-run
# Verbose output
RUST_LOG=debug cargo hax into fstar# Cargo.toml
[package.metadata.hax]
# Global settings
include = ["src/verified/**"]
exclude = ["src/tests/**"]
# Backend-specific settings
[package.metadata.hax.into]
[package.metadata.hax.into.fstar]
z3rlimit = 100
fuel = 2
ifuel = 1
[package.metadata.hax.into.lean]
version = 4
mathlib = trueFor complete CLI documentation, see cli-backends.md.
hax-engine is the core translation engine written primarily in OCaml.
JSON/CBOR Input
│
├─→ 1. PARSING
│ └─→ Deserialize AST from frontend
│
├─→ 2. VALIDATION
│ ├─→ Check AST well-formedness
│ └─→ Validate type information
│
├─→ 3. SIMPLIFICATION
│ ├─→ Desugar pattern matching
│ ├─→ Resolve trait calls
│ ├─→ Simplify control flow
│ └─→ Normalize expressions
│
├─→ 4. TRANSFORMATION
│ ├─→ Extract specifications
│ ├─→ Process loop invariants
│ ├─→ Handle refinement types
│ └─→ Backend-specific rewrites
│
├─→ 5. BACKEND SELECTION
│ └─→ Choose F*/Lean/Coq/etc.
│
├─→ 6. CODE GENERATION
│ ├─→ Translate types
│ ├─→ Translate expressions
│ ├─→ Generate specifications
│ └─→ Format output
│
└─→ Generated Code
Pattern Desugaring:
// Rust input
match option {
Some(x) => x + 1,
None => 0,
}
// Simplified representation
let __match_tmp = option;
if is_some(__match_tmp) {
let x = unwrap(__match_tmp);
x + 1
} else {
0
}Trait Resolution:
// Rust input
fn generic<T: Add>(x: T, y: T) -> T {
x + y
}
// After monomorphization
fn generic_u32(x: u32, y: u32) -> u32 {
U32::add(x, y) // Explicit trait call
}Loop Invariant Injection:
// Rust input with loop_invariant
#[loop_invariant(sum <= i * MAX)]
for i in 0..n {
sum += arr[i];
}
// Transformed representation (conceptual)
let mut i = 0;
assert!(sum <= i * MAX); // Initial invariant
while i < n {
let old_i = i;
let old_sum = sum;
sum += arr[i];
i += 1;
assert!(sum <= i * MAX); // Preserved invariant
}For detailed architecture information, see internal-architecture.md.
Each backend translator is a module in the hax-engine that generates target-language code.
All backends must:
- Type Translation: Map Rust types to target types
- Expression Translation: Convert Rust expressions to target syntax
- Specification Extraction: Translate contracts and invariants
- Module System: Handle Rust modules and visibility
- Name Mangling: Ensure valid identifiers in target language
| Feature | F* | Lean4 | Coq | ProVerif |
|---|---|---|---|---|
| Maturity | Stable | Active | Experimental | PoC |
| Specifications | Full | Full | Partial | Protocol-specific |
| SMT Integration | Yes (Z3) | Limited | No | Symbolic |
| Loop Invariants | Yes | Yes | Limited | N/A |
| Refinement Types | Yes | Yes | Limited | No |
| Panic Proofs | Manual | Automatic | Manual | N/A |
| Best For | Crypto, Systems | Functional correctness | Mathematical proofs | Protocols |
For complete backend documentation, see cli-backends.md.
The easiest and most reproducible installation method:
# Install Nix with flakes (one-time setup)
curl --proto '=https' --tlsv1.2 -sSf -L \
https://install.determinate.systems/nix | sh -s -- install
# Run hax directly without installing
nix run github:hacspec/hax -- into fstar
# Or install globally
nix profile install github:hacspec/hax
# Verify installation
cargo hax --helpFor development or customization:
# 1. Clone repository
git clone https://github.com/hacspec/hax.git
cd hax
# 2. Install dependencies
# - OCaml 5.1.1+ (for engine)
# - Rust nightly (for frontend)
# - Node.js (for JS-compiled engine option)
# Install OPAM (OCaml package manager)
bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
opam init
opam switch create 5.1.1
# 3. Run setup script
./setup.sh
# This will:
# - Install OCaml dependencies
# - Build hax-engine
# - Build cargo-hax CLI
# - Set up environment variables
# 4. Add to PATH
export PATH="$PATH:$(pwd)/target/release"
# 5. Verify
cargo hax --helpFor containerized environments:
# Build Docker image
docker build -f .docker/Dockerfile . -t hax
# Run in container
docker run -it --rm -v /path/to/your/crate:/work hax bash
# Inside container:
cd /work
cargo hax into fstar# Create new library crate
cargo new --lib my-verified-lib
cd my-verified-lib
# Add hax-lib dependency
cat >> Cargo.toml << 'EOF'
[dependencies]
hax-lib = { version = "*", features = ["macros"] }
[package.metadata.hax]
# Optional: Configure hax settings
[package.metadata.hax.into.fstar]
z3rlimit = 100
EOF
# Write some verified code
cat > src/lib.rs << 'EOF'
use hax_lib as hax;
/// A simple verified function
#[hax::requires(x < 100)]
#[hax::ensures(|result| result > x)]
pub fn increment(x: u32) -> u32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_increment() {
assert_eq!(increment(5), 6);
assert_eq!(increment(99), 100);
}
}
EOF
# Test normally
cargo test
# Extract and verify
cargo hax into fstar# Cargo.toml
[dependencies]
# Add hax-lib
hax-lib = { version = "*", features = ["macros"] }
# Keep existing dependencies
# ...// src/lib.rs or src/main.rs
// Add to existing code
use hax_lib as hax;
// Gradually add specifications to existing functions
#[hax::requires(input.len() > 0)]
pub fn existing_function(input: &[u8]) -> u8 {
// Your existing implementation
input[0]
}Let's write a complete, verified program from scratch:
We'll implement a safe bounded addition that never overflows.
// src/lib.rs
use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
/// Adds two numbers if the result fits in u32, otherwise returns None
///
/// # Specification
/// - If the sum fits in u32, returns Some(x + y)
/// - If the sum would overflow, returns None
/// - The function never panics
pub fn bounded_add(x: u32, y: u32) -> Option<u32> {
x.checked_add(y)
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
/// Adds two numbers if the result fits in u32, otherwise returns None
#[hax::ensures(|result| match result {
Some(sum) => {
// If we return Some, the sum is correct
Int::from(sum) == Int::from(x) + Int::from(y) &&
// And it fits in u32
Int::from(sum) <= Int::from(u32::MAX)
},
None => {
// If we return None, the sum would overflow
Int::from(x) + Int::from(y) > Int::from(u32::MAX)
}
})]
pub fn bounded_add(x: u32, y: u32) -> Option<u32> {
x.checked_add(y)
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_overflow() {
assert_eq!(bounded_add(10, 20), Some(30));
assert_eq!(bounded_add(0, 0), Some(0));
assert_eq!(bounded_add(u32::MAX - 1, 1), Some(u32::MAX));
}
#[test]
fn test_overflow() {
assert_eq!(bounded_add(u32::MAX, 1), None);
assert_eq!(bounded_add(u32::MAX, u32::MAX), None);
assert_eq!(bounded_add(u32::MAX - 1, 2), None);
}
}# Run tests
cargo test
# Extract to F*
cargo hax into fstar
# The generated F* file will be in:
# proofs/fstar/extraction/My_verified_lib.fst
# Verify with F* (requires F* installation)
cd proofs/fstar/extraction
fstar.exe My_verified_lib.fst(* Generated F* code *)
val bounded_add (x y: u32) :
Pure (option u32)
(requires True)
(ensures fun result ->
match result with
| Some sum ->
v sum == v x + v y /\
v sum <= v u32_max
| None ->
v x + v y > v u32_max)
let bounded_add x y =
U32.checked_add x y┌─────────────────────────────────────────┐
│ 1. Write Implementation │
│ - Focus on correctness │
│ - Use standard Rust idioms │
│ - Add normal Rust tests │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. Add Basic Specifications │
│ - Add #[requires] for preconditions │
│ - Add #[ensures] for postconditions │
│ - Keep specifications simple │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. Test and Debug │
│ - cargo test (standard tests) │
│ - cargo build (check compilation) │
│ - Debug with normal tools │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. Extract and Attempt Verification │
│ - cargo hax into fstar │
│ - Review generated code │
│ - Attempt verification │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 5. Fix Verification Failures │
│ - Read error messages │
│ - Strengthen specifications │
│ - Add intermediate assertions │
│ - Add loop invariants │
│ - Simplify complex proofs │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 6. Iterate Until Verified │
│ - Refine specifications │
│ - Improve invariants │
│ - Add helper lemmas │
│ - Use opaque for modularity │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 7. Production │
│ - cargo build --release │
│ - Fully verified code │
│ - High assurance guarantees │
└─────────────────────────────────────────┘
- Start Small: Begin with simple functions before tackling complex algorithms
- Test First: Use standard Rust tests to catch bugs early
- Incremental Specifications: Add specifications gradually
- Read Errors: hax error messages often tell you exactly what to fix
- Use Examples: Study the examples/ directory in the hax repository
- Ask for Help: Join the Zulip chat when stuck
The hax type system extends Rust's type system with mathematical abstractions that enable precise reasoning about program behavior.
The Int type represents unbounded mathematical integers, as opposed to Rust's fixed-width integer types.
Rust's integer types wrap on overflow:
// Rust's u32 wraps around
let x: u32 = u32::MAX;
let y: u32 = 1;
let z = x + y; // z == 0 (wraps around!)
// This makes reasoning difficult:
// x + y < x (true due to wrapping!)Mathematical integers don't wrap:
use hax_lib::int::Int;
// Mathematical integers never wrap
let x = Int::from(u32::MAX);
let y = Int::from(1u32);
let z = x + y; // z == 2^32 (mathematical result)
// Reasoning is straightforward:
// x + y > x (always true for positive y)use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
fn int_creation_examples() {
// Method 1: Using int! macro (for literals)
let a = hax::int!(42);
let b = hax::int!(1_000_000_000_000); // Arbitrary size
// Method 2: Using Int::from (for variables)
let x: u32 = 42;
let a = Int::from(x);
// Method 3: Using .to_int() or .lift() trait method
let x: u32 = 42;
let a = x.to_int(); // or x.lift()
// Method 4: From expressions
let x: u32 = 10;
let y: u32 = 20;
let sum_concrete = x + y;
let sum_math = Int::from(sum_concrete);
// All methods create the same mathematical integer
hax::assert!(a == hax::int!(42));
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
fn int_operations() {
let x = hax::int!(10);
let y = hax::int!(20);
// ARITHMETIC OPERATIONS
let sum = x + y; // Addition: 30
let diff = x - y; // Subtraction: -10
let product = x * y; // Multiplication: 200
let quotient = x / y; // Division: 0 (truncated)
let negation = x.neg(); // Negation: -10
// COMPARISON
hax::assert!(x < y); // Less than
hax::assert!(x <= y); // Less than or equal
hax::assert!(y > x); // Greater than
hax::assert!(y >= x); // Greater than or equal
hax::assert!(x != y); // Not equal
// SPECIAL OPERATIONS
let power = x.pow(3); // Exponentiation: 1000
let abs = diff.abs(); // Absolute value: 10
// Euclidean remainder (always non-negative)
let rem = x.rem_euclid(3); // 10 % 3 = 1
// Check if representable in concrete type
hax::assert!(sum.in_bounds::<u32>());
}use hax_lib::int::{Int, ToInt};
fn concretization_examples(x: u32, y: u32) {
let math_sum = Int::from(x) + Int::from(y);
// Method 1: concretize() - requires proof that value fits
let concrete: u32 = math_sum.concretize();
// This will fail verification if math_sum might be > u32::MAX
// Method 2: Safe conversion with precondition
if math_sum <= Int::from(u32::MAX) {
let concrete: u32 = math_sum.concretize();
// Now provably safe
}
// Method 3: Checked conversion
if let Some(concrete) = try_concretize::<u32>(math_sum) {
// Use concrete value
}
}
// Helper for safe concretization
fn try_concretize<T: TryFrom<Int>>(value: Int) -> Option<T> {
value.try_into().ok()
}Pattern 1: Overflow-Safe Arithmetic
use hax_lib::int::{Int, ToInt};
#[hax::requires(Int::from(x) + Int::from(y) <= Int::from(u32::MAX))]
#[hax::ensures(|result| Int::from(result) == Int::from(x) + Int::from(y))]
pub fn safe_add(x: u32, y: u32) -> u32 {
// The precondition proves this won't overflow
x + y
}
// Usage
fn caller() {
let a = 100u32;
let b = 200u32;
// Must prove the precondition holds
hax::assert!(Int::from(a) + Int::from(b) <= Int::from(u32::MAX));
let c = safe_add(a, b);
}Pattern 2: Mathematical Specification
use hax_lib::int::{Int, ToInt};
/// Computes n! (factorial)
#[hax::decreases(n)]
#[hax::ensures(|result| Int::from(result) == math_factorial(n.to_int()))]
pub fn factorial(n: u32) -> u64 {
if n == 0 {
1
} else {
n as u64 * factorial(n - 1)
}
}
// Specification function (not extracted, only for verification)
#[hax::exclude]
fn math_factorial(n: Int) -> Int {
if n <= hax::int!(0) {
hax::int!(1)
} else {
n * math_factorial(n - hax::int!(1))
}
}Pattern 3: Loop Invariants with Int
use hax_lib::int::{Int, ToInt};
fn sum_array(arr: &[u32]) -> u64 {
let mut sum = 0u64;
let mut i = 0usize;
#[hax::loop_invariant(
// sum equals the mathematical sum of first i elements
Int::from(sum) == array_sum(arr, i)
)]
#[hax::loop_decreases(arr.len() - i)]
while i < arr.len() {
sum += arr[i] as u64;
i += 1;
}
sum
}
// Specification: mathematical sum of first n elements
#[hax::exclude]
fn array_sum(arr: &[u32], n: usize) -> Int {
if n == 0 {
hax::int!(0)
} else {
Int::from(arr[n-1]) + array_sum(arr, n-1)
}
}The Prop type represents logical propositions that can be used in specifications.
use hax_lib::{Prop, forall, exists, implies};
fn proposition_examples() {
// From boolean expressions
let p = Prop::from(true);
let q = Prop::from(1 < 2);
// From quantifiers
let all_positive = forall(|x: u32| x >= 0);
let has_zero = exists(|x: i32| x == 0);
// From implications
let imp = implies(Prop::from(true), Prop::from(true));
}use hax_lib::Prop;
fn prop_operations(x: bool, y: bool) {
let p = Prop::from(x);
let q = Prop::from(y);
// LOGICAL OPERATIONS
let conjunction = p.and(q); // p ∧ q
let disjunction = p.or(q); // p ∨ q
let negation = p.not(); // ¬p
let implication = p.implies(q); // p → q
// COMPARISONS
let equal = p.eq(q); // p ≡ q
let not_equal = p.ne(q); // p ≠ q
}use hax_lib as hax;
fn using_propositions(arr: &[u32], target: u32) -> bool {
// Assert a proposition
hax::assert_prop!(forall(|i: usize| {
implies(i < arr.len(), arr[i] <= u32::MAX)
}));
// Check if target exists in array
let exists_target = exists(|i: usize| {
i < arr.len() && arr[i] == target
});
// We can't evaluate propositions directly,
// but we can use them in specifications
unimplemented!("Prop is for specifications only")
}The abstraction system provides a bridge between concrete machine types and abstract mathematical types.
pub trait Abstraction {
/// The abstract representation of this type
type AbstractType;
/// Convert from concrete to abstract
fn lift(self) -> Self::AbstractType;
}
// Example implementations
impl Abstraction for u32 {
type AbstractType = Int;
fn lift(self) -> Int {
Int::from(self)
}
}
impl Abstraction for bool {
type AbstractType = Prop;
fn lift(self) -> Prop {
Prop::from(self)
}
}pub trait Concretization<T> {
/// Convert from abstract to concrete
/// May fail if the abstract value doesn't fit in the concrete type
fn concretize(self) -> T;
}
// Example implementation
impl Concretization<u32> for Int {
fn concretize(self) -> u32 {
// Verification will fail if self > u32::MAX
u32::try_from(self).unwrap()
}
}use hax_lib::int::{Int, ToInt};
fn abstraction_example(x: u32) -> u32 {
// Lift to Int
let abstract_x: Int = x.lift(); // or x.to_int()
// Work in abstract domain
let abstract_result = abstract_x * hax::int!(2);
// Prove result fits in u32
hax::assert!(abstract_result <= Int::from(u32::MAX));
// Concretize back to u32
let concrete_result: u32 = abstract_result.concretize();
concrete_result
}In specifications, hax automatically lifts concrete values:
#[hax::ensures(|result| result > x)] // Automatically lifts to Int
pub fn increment(x: u32) -> u32 {
x + 1
}
// Equivalent to:
#[hax::ensures(|result| Int::from(result) > Int::from(x))]
pub fn increment(x: u32) -> u32 {
x + 1
}Sometimes you need explicit lifting for clarity:
use hax_lib::int::{Int, ToInt};
#[hax::ensures(|result|
Int::from(result) == Int::from(x) + Int::from(y)
)]
pub fn add(x: u32, y: u32) -> u32 {
x.wrapping_add(y) // Even wrapping_add can be specified!
}use hax_lib::int::Int;
/// Convert u32 to i64, proving no overflow
#[hax::ensures(|result| Int::from(result) == Int::from(x))]
pub fn u32_to_i64(x: u32) -> i64 {
x as i64 // Safe because u32::MAX < i64::MAX
}
/// Saturating subtraction specification
#[hax::ensures(|result| {
if Int::from(x) >= Int::from(y) {
Int::from(result) == Int::from(x) - Int::from(y)
} else {
result == 0
}
})]
pub fn saturating_sub(x: u32, y: u32) -> u32 {
x.saturating_sub(y)
}Assertions and assumptions are the building blocks of verification.
The assert! macro generates a proof obligation that the verifier must prove.
use hax_lib as hax;
pub fn simple_assert(x: u32) {
// The verifier must prove x < u32::MAX
hax::assert!(x < u32::MAX);
// This always holds for u32
// Verification succeeds
}
pub fn failing_assert(x: u32, y: u32) {
// The verifier must prove x + y never overflows
hax::assert!(x + y > x);
// This might not hold (overflow)
// Verification fails without more context
}use hax_lib as hax;
pub fn assertion_runtime() {
hax::assert!(true); // No-op in release builds
// debug_assert! in debug builds
hax::assert!(false); // Panics in debug builds
// No-op in release builds
// Verification fails
}use hax_lib as hax;
/// Use assert! to help the verifier prove postconditions
#[hax::ensures(|result| result > x)]
pub fn increment(x: u32) -> u32 {
let result = x + 1;
// Help the verifier by asserting intermediate steps
hax::assert!(result > x);
result
}
/// Use assert! to document assumptions about code
pub fn process_array(arr: &[u32]) {
hax::assert!(arr.len() <= 1000); // Document size assumption
for i in 0..arr.len() {
// Process elements
hax::assert!(i < arr.len()); // Bounds check proof
}
}
/// Use assert! to break down complex proofs
pub fn complex_computation(x: u32, y: u32) -> u32 {
hax::assert!(x < 100); // Step 1
hax::assert!(y < 100); // Step 2
let sum = x + y;
hax::assert!(sum < 200); // Step 3: follows from 1 and 2
sum
}The assume! macro tells the verifier to trust a condition without proof. This is unsafe and should be used sparingly.
use hax_lib as hax;
pub fn unsafe_assumption(x: u32, y: u32) -> u32 {
// Tell the verifier to trust that y != 0
// WITHOUT PROOF
hax::assume!(y != 0);
// Now the division is "proven" safe
// But this is unsound if y could actually be 0!
x / y
}Use Case 1: Trusting Unverified Code
use hax_lib as hax;
/// External function we can't verify
#[hax::exclude]
extern "C" {
fn external_function(x: u32) -> u32;
}
pub fn call_external(x: u32) -> u32 {
let result = unsafe { external_function(x) };
// We trust (but can't prove) that external_function
// never returns 0
hax::assume!(result != 0);
100 / result
}Use Case 2: Breaking Circular Dependencies
use hax_lib as hax;
pub fn mutually_recursive_a(x: u32) -> u32 {
if x > 0 {
mutually_recursive_b(x - 1)
} else {
0
}
}
pub fn mutually_recursive_b(x: u32) -> u32 {
if x > 0 {
mutually_recursive_a(x - 1)
} else {
// Assume termination (trust)
hax::assume!(x == 0);
1
}
}Use Case 3: Axioms and Mathematical Assumptions
use hax_lib as hax;
pub fn prime_theorem(n: u32) -> bool {
// Assume the prime number theorem
hax::assume!(prime_density_bound(n));
// Use this assumption in further proofs
is_prime(n)
}
#[hax::exclude]
fn prime_density_bound(n: u32) -> bool {
// Mathematical property we trust
true
}use hax_lib as hax;
/// ⚠️ DANGER: Unsound proof!
pub fn unsound_example(x: u32) -> u32 {
// This is FALSE but we assume it
hax::assume!(x > 100);
// Now we can "prove" anything
hax::assert!(x > 50); // "Verified" but wrong!
// This code could panic at runtime
x - 50
}
/// The verifier thinks this is safe, but it can crash!
fn caller() {
let result = unsound_example(10); // PANIC! 10 - 50 underflows
}The assert_prop! macro asserts logical propositions using quantifiers.
use hax_lib as hax;
pub fn prop_assertion(arr: &[u32]) {
// Assert a property over all array elements
hax::assert_prop!(forall(|i: usize| {
implies(i < arr.len(), arr[i] >= 0)
}));
// This always holds for u32 (always non-negative)
// Verification succeeds
}use hax_lib as hax;
pub fn complex_proposition(arr: &[i32], target: i32) {
// Existential quantification
hax::assert_prop!(exists(|i: usize| {
i < arr.len() && arr[i] == target
}));
// Universal with implication
hax::assert_prop!(forall(|i: usize| {
implies(i < arr.len() && arr[i] == target, i < 100)
}));
// Nested quantifiers
hax::assert_prop!(forall(|i: usize| {
implies(i < arr.len(),
forall(|j: usize| {
implies(j < arr.len() && i != j, arr[i] != arr[j])
})
)
}));
}use hax_lib as hax;
fn comparison(x: u32) {
// assert! - for concrete, computable conditions
hax::assert!(x < 100); // Can evaluate at runtime
// assert_prop! - for logical, possibly non-computable propositions
hax::assert_prop!(forall(|y: u32| y >= 0)); // Can't evaluate at runtime
}Standard Rust debug_assert! is completely ignored by hax.
use hax_lib as hax;
pub fn debugging_example(x: u32) -> u32 {
// This is ONLY for runtime debugging
// Generates NO proof obligation
debug_assert!(expensive_check(x));
// hax doesn't see this assertion at all
x + 1
}
fn expensive_check(x: u32) -> bool {
// Expensive computation for testing
(0..x).all(|i| i < 1000)
}pub fn use_debug_assert(arr: &[u32]) {
// Use debug_assert! for:
// 1. Expensive runtime checks
debug_assert!(arr.iter().all(|&x| is_valid(x)));
// 2. Checks that are redundant with verification
for i in 0..arr.len() {
hax::assert!(i < arr.len()); // Verified
debug_assert!(i < arr.len()); // Runtime check (redundant)
}
// 3. Development/testing assertions
debug_assert_eq!(arr.len(), expected_len());
}
#[hax::exclude]
fn is_valid(x: u32) -> bool { x < 1000 }
#[hax::exclude]
fn expected_len() -> usize { 10 }debug_assert! ← Only runtime, no verification
↓
assert! ← Proof obligation (verifier must prove)
↓
assert_prop! ← Logical proposition (with quantifiers)
↓
assume! ← Trust (no proof, UNSAFE)
use hax_lib as hax;
pub fn choosing_assertions(x: u32, arr: &[u32]) {
// Use assert! for simple, provable facts
hax::assert!(x < u32::MAX);
// Use assert_prop! for logical properties
hax::assert_prop!(forall(|i: usize| {
implies(i < arr.len(), arr[i] < 1000)
}));
// Use assume! ONLY when necessary (rare!)
#[cfg(feature = "unsafe_trust")]
{
hax::assume!(x > 100); // Trust external invariant
}
// Use debug_assert! for expensive runtime checks
debug_assert!(complex_property_holds(x, arr));
}
#[hax::exclude]
fn complex_property_holds(x: u32, arr: &[u32]) -> bool {
// Expensive check
true
}use hax_lib as hax;
/// ❌ WRONG: Using assume! instead of proving
pub fn wrong_division(x: u32, y: u32) -> u32 {
hax::assume!(y != 0); // Lazy! Should prove this
x / y
}
/// ✅ CORRECT: Prove with precondition
#[hax::requires(y != 0)]
pub fn correct_division(x: u32, y: u32) -> u32 {
x / y
}
/// ❌ WRONG: Asserting unprovable facts
pub fn wrong_assert(x: u32) {
hax::assert!(x == 42); // Can't prove this for arbitrary x!
}
/// ✅ CORRECT: Only assert provable facts
#[hax::requires(x == 42)]
pub fn correct_assert(x: u32) {
hax::assert!(x == 42); // Now provable from precondition
}Logical specifications use quantifiers and logical connectives to express properties.
The forall! macro expresses that a property holds for all values.
use hax_lib::{forall, implies};
fn forall_examples() {
// All u32 values are non-negative
let prop1 = forall(|x: u32| x >= 0);
// All array indices are in bounds
let prop2 = forall(|i: usize|
implies(i < arr.len(), arr[i] <= u32::MAX)
);
// All pairs satisfy a relation
let prop3 = forall(|x: u32| forall(|y: u32|
x + y >= x
));
}use hax_lib as hax;
/// Ensure all array elements are positive
#[hax::requires(forall(|i: usize|
implies(i < arr.len(), arr[i] > 0)
))]
pub fn all_positive(arr: &[u32]) {
// We can assume all elements are positive
for i in 0..arr.len() {
hax::assert!(arr[i] > 0);
}
}
/// Find minimum element
#[hax::requires(arr.len() > 0)]
#[hax::ensures(|result|
forall(|i: usize| implies(i < arr.len(), result <= arr[i])) &&
exists(|i: usize| i < arr.len() && result == arr[i])
)]
pub fn find_min(arr: &[u32]) -> u32 {
let mut min = arr[0];
for i in 1..arr.len() {
if arr[i] < min {
min = arr[i];
}
}
min
}use hax_lib as hax;
/// Matrix property: all rows have equal length
#[hax::requires(
forall(|i: usize| forall(|j: usize|
implies(
i < matrix.len() && j < matrix.len(),
matrix[i].len() == matrix[j].len()
)
))
)]
pub fn rectangular_matrix(matrix: &[Vec<u32>]) {
// Can assume all rows have same length
if matrix.len() > 0 {
let width = matrix[0].len();
for i in 0..matrix.len() {
hax::assert!(matrix[i].len() == width);
}
}
}The exists! macro expresses that a property holds for at least one value.
use hax_lib::exists;
fn exists_examples(arr: &[u32], target: u32) {
// There exists a zero in the array
let prop1 = exists(|i: usize|
i < arr.len() && arr[i] == 0
);
// There exists an element equal to target
let prop2 = exists(|i: usize|
i < arr.len() && arr[i] == target
);
// There exist two different indices with same value
let prop3 = exists(|i: usize| exists(|j: usize|
i < arr.len() && j < arr.len() && i != j && arr[i] == arr[j]
));
}use hax_lib as hax;
/// Search for target in array
#[hax::ensures(|result| match result {
Some(idx) => idx < arr.len() && arr[idx] == target,
None => forall(|i: usize|
implies(i < arr.len(), arr[i] != target)
)
})]
pub fn search(arr: &[u32], target: u32) -> Option<usize> {
for i in 0..arr.len() {
if arr[i] == target {
return Some(i);
}
}
None
}
/// Check if array contains duplicates
#[hax::ensures(|result|
result == exists(|i: usize| exists(|j: usize|
i < arr.len() && j < arr.len() && i < j && arr[i] == arr[j]
))
)]
pub fn has_duplicates(arr: &[u32]) -> bool {
for i in 0..arr.len() {
for j in (i+1)..arr.len() {
if arr[i] == arr[j] {
return true;
}
}
}
false
}The implies! macro expresses logical implication: "if A then B".
use hax_lib::implies;
fn implies_examples(x: u32) {
// If x > 10, then x > 5
let prop1 = implies(x > 10, x > 5);
// If x is even, then x % 2 == 0
let prop2 = implies(x % 2 == 0, is_even(x));
// Chained implications
let prop3 = implies(x > 100, implies(x > 50, x > 25));
}
fn is_even(x: u32) -> bool {
x % 2 == 0
}use hax_lib as hax;
/// Bounded array property
#[hax::requires(forall(|i: usize|
implies(i < arr.len(), arr[i] <= bound)
))]
#[hax::ensures(|result| result <= bound)]
pub fn max_bounded(arr: &[u32], bound: u32) -> u32 {
let mut max = 0;
for i in 0..arr.len() {
// From precondition: arr[i] <= bound
hax::assert!(implies(i < arr.len(), arr[i] <= bound));
if arr[i] > max {
max = arr[i];
}
}
max
}
/// Conditional property
#[hax::ensures(|result|
implies(result, forall(|i: usize|
implies(i < arr.len(), arr[i] > 0)
))
)]
pub fn all_positive_check(arr: &[u32]) -> bool {
for i in 0..arr.len() {
if arr[i] == 0 {
return false;
}
}
true
}Combine propositions using logical operators.
use hax_lib::Prop;
fn conjunction_example(x: u32, y: u32) -> Prop {
let p = Prop::from(x > 0);
let q = Prop::from(y > 0);
// Both x and y are positive
p.and(q)
}
/// Function with multiple requirements
#[hax::requires(x > 0 && y > 0 && x < 100 && y < 100)]
pub fn both_positive(x: u32, y: u32) -> u32 {
x + y
}use hax_lib::Prop;
fn disjunction_example(x: u32) -> Prop {
let p = Prop::from(x == 0);
let q = Prop::from(x > 0);
// x is either zero or positive
p.or(q)
}
/// At least one positive
#[hax::requires(x > 0 || y > 0)]
pub fn at_least_one_positive(x: u32, y: u32) -> bool {
x > 0 || y > 0
}use hax_lib::Prop;
fn negation_example(x: u32) -> Prop {
let p = Prop::from(x == 0);
// x is not zero
p.not()
}
/// x is non-zero
#[hax::requires(x != 0)]
pub fn non_zero(x: u32) -> u32 {
100 / x
}use hax_lib as hax;
/// Check if array is sorted
#[hax::ensures(|result|
result == forall(|i: usize| forall(|j: usize|
implies(
i < arr.len() && j < arr.len() && i < j,
arr[i] <= arr[j]
)
))
)]
pub fn is_sorted(arr: &[u32]) -> bool {
for i in 0..(arr.len().saturating_sub(1)) {
if arr[i] > arr[i + 1] {
return false;
}
}
true
}use hax_lib as hax;
/// Partition array around pivot
#[hax::ensures(|pivot_idx|
// All elements before pivot are <= pivot value
forall(|i: usize| implies(
i < pivot_idx,
arr[i] <= arr[pivot_idx]
)) &&
// All elements after pivot are >= pivot value
forall(|j: usize| implies(
j >= pivot_idx && j < arr.len(),
arr[j] >= arr[pivot_idx]
))
)]
pub fn partition(arr: &mut [u32]) -> usize {
// Implementation...
0
}use hax_lib as hax;
/// Sort array (produces permutation of input)
#[hax::ensures(|_|
// Output is sorted
forall(|i: usize| forall(|j: usize|
implies(i < j && j < arr.len(), arr[i] <= arr[j])
)) &&
// Output is permutation (same multiset)
forall(|val: u32|
count_occurrences(arr, val) == count_occurrences(hax::old(arr), val)
)
)]
pub fn sort(arr: &mut [u32]) {
// Implementation...
}
#[hax::exclude]
fn count_occurrences(arr: &[u32], val: u32) -> usize {
arr.iter().filter(|&&x| x == val).count()
}Refinement types allow you to encode invariants directly into the type system.
A refinement type is a type T paired with a logical predicate P such that values of type {x: T | P(x)} are exactly those values v of type T for which P(v) holds.
Type: u32
Refinement: { x: u32 | x != 0 }
Values: All u32 except 0
In hax, refinement types are implemented as wrapper structs with an invariant.
pub trait Refinement {
/// The underlying concrete type
type InnerType;
/// The logical invariant that must hold
fn invariant(value: Self::InnerType) -> Prop;
/// Construct from inner value (unchecked, for internal use)
fn new(value: Self::InnerType) -> Self;
/// Extract inner value
fn get(self) -> Self::InnerType;
/// Get mutable reference to inner value
fn get_mut(&mut self) -> &mut Self::InnerType;
}use hax_lib as hax;
/// A non-zero integer
#[hax::refinement_type]
pub struct NonZero {
#[hax::refinement]
value: i32,
}
impl hax::Refinement for NonZero {
type InnerType = i32;
fn invariant(value: i32) -> hax::Prop {
hax::Prop::from(value != 0)
}
}
/// Smart constructor with validation
impl NonZero {
#[hax::ensures(|result| match result {
Some(nz) => *nz == value && value != 0,
None => value == 0
})]
pub fn new(value: i32) -> Option<Self> {
if value != 0 {
Some(NonZero { value })
} else {
None
}
}
}
/// Usage: Division is provably safe
pub fn safe_divide(x: i32, divisor: NonZero) -> i32 {
// The type guarantees *divisor != 0
x / *divisor
}use hax_lib as hax;
/// An integer in the range [MIN, MAX]
#[hax::refinement_type]
pub struct BoundedU32<const MIN: u32, const MAX: u32> {
#[hax::refinement]
value: u32,
}
impl<const MIN: u32, const MAX: u32> hax::Refinement for BoundedU32<MIN, MAX> {
type InnerType = u32;
fn invariant(value: u32) -> hax::Prop {
hax::Prop::from(value >= MIN && value <= MAX)
}
}
/// Smart constructor
impl<const MIN: u32, const MAX: u32> BoundedU32<MIN, MAX> {
pub fn new(value: u32) -> Option<Self> {
if value >= MIN && value <= MAX {
Some(BoundedU32 { value })
} else {
None
}
}
}
/// Type alias for common case
pub type Percentage = BoundedU32<0, 100>;
/// Usage
pub fn scale_by_percentage(value: u32, percent: Percentage) -> u32 {
// percent is guaranteed to be 0-100
(value * (*percent)) / 100
}use hax_lib as hax;
/// A vector that is guaranteed non-empty
#[hax::refinement_type]
pub struct NonEmptyVec<T> {
#[hax::refinement]
data: Vec<T>,
}
impl<T> hax::Refinement for NonEmptyVec<T> {
type InnerType = Vec<T>;
fn invariant(data: Vec<T>) -> hax::Prop {
hax::Prop::from(data.len() > 0)
}
}
impl<T> NonEmptyVec<T> {
pub fn new(data: Vec<T>) -> Option<Self> {
if data.is_empty() {
None
} else {
Some(NonEmptyVec { data })
}
}
/// Safe first element access (no panic possible)
pub fn first(&self) -> &T {
// The refinement guarantees len() > 0
&self.data[0]
}
/// Safe pop (always returns Some)
pub fn pop(&mut self) -> Option<T> {
if self.data.len() == 1 {
// Would violate invariant
None
} else {
self.data.pop()
}
}
}use hax_lib as hax;
#[hax::refinement_type]
pub struct ValidEmail {
#[hax::refinement]
email: String,
}
impl hax::Refinement for ValidEmail {
type InnerType = String;
fn invariant(email: String) -> hax::Prop {
hax::Prop::from(is_valid_email(&email))
}
}
#[hax::exclude]
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.')
}
impl ValidEmail {
pub fn parse(input: String) -> Result<Self, String> {
if is_valid_email(&input) {
Ok(ValidEmail { email: input })
} else {
Err("Invalid email".to_string())
}
}
}
/// Functions can require validated input
pub fn send_email(to: ValidEmail, message: &str) {
// Guaranteed that `to` is a valid email
// No runtime validation needed
}use hax_lib as hax;
mod states {
pub struct Open;
pub struct Closed;
}
#[hax::refinement_type]
pub struct File<State> {
#[hax::refinement]
handle: FileHandle,
_state: PhantomData<State>,
}
impl File<states::Closed> {
pub fn open(path: &str) -> Result<File<states::Open>, Error> {
let handle = open_file(path)?;
Ok(File { handle, _state: PhantomData })
}
}
impl File<states::Open> {
pub fn write(&mut self, data: &[u8]) -> Result<(), Error> {
write_to_file(&self.handle, data)
}
pub fn close(self) -> File<states::Closed> {
close_file(&self.handle);
File { handle: self.handle, _state: PhantomData }
}
}
// Can't write to closed file (type error)
// Can't close already closed file (type error)use hax_lib as hax;
/// Positive integer
#[hax::refinement_type]
pub struct Positive {
#[hax::refinement]
value: i32,
}
impl hax::Refinement for Positive {
type InnerType = i32;
fn invariant(value: i32) -> hax::Prop {
hax::Prop::from(value > 0)
}
}
/// Even integer
#[hax::refinement_type]
pub struct Even {
#[hax::refinement]
value: i32,
}
impl hax::Refinement for Even {
type InnerType = i32;
fn invariant(value: i32) -> hax::Prop {
hax::Prop::from(value % 2 == 0)
}
}
/// Positive and even integer
#[hax::refinement_type]
pub struct PositiveEven {
#[hax::refinement]
value: i32,
}
impl hax::Refinement for PositiveEven {
type InnerType = i32;
fn invariant(value: i32) -> hax::Prop {
hax::Prop::from(value > 0 && value % 2 == 0)
}
}
/// Convert from stricter to less strict
impl From<PositiveEven> for Positive {
fn from(pe: PositiveEven) -> Positive {
Positive { value: *pe }
}
}
impl From<PositiveEven> for Even {
fn from(pe: PositiveEven) -> Even {
Even { value: *pe }
}
}use hax_lib as hax;
/// Array with known size
#[hax::refinement_type]
pub struct SizedArray<T, const N: usize> {
#[hax::refinement]
data: Vec<T>,
}
impl<T, const N: usize> hax::Refinement for SizedArray<T, N> {
type InnerType = Vec<T>;
fn invariant(data: Vec<T>) -> hax::Prop {
hax::Prop::from(data.len() == N)
}
}
impl<T, const N: usize> SizedArray<T, N> {
/// Safe indexing without bounds checks
pub fn get(&self, index: usize) -> Option<&T> {
if index < N {
Some(&self.data[index]) // Provably safe
} else {
None
}
}
}[Continue with remaining sections in the same exhaustive style...]
I'll create the complete, exhaustively detailed README.md. Let me continue with the remaining sections:
Function contracts define the interface between callers and implementations using preconditions and postconditions.
Preconditions specify what must be true when the function is called.
use hax_lib as hax;
/// Simple precondition: x must be positive
#[hax::requires(x > 0)]
pub fn reciprocal(x: f64) -> f64 {
1.0 / x
}
/// Multiple preconditions using &&
#[hax::requires(x > 0 && y > 0)]
pub fn both_positive(x: i32, y: i32) -> i32 {
x + y
}
/// Precondition on array length
#[hax::requires(arr.len() > 0)]
pub fn first_element(arr: &[u32]) -> u32 {
arr[0] // Provably safe due to precondition
}use hax_lib as hax;
/// Multiple #[requires] attributes (all must hold)
#[hax::requires(x < 100)]
#[hax::requires(y < 100)]
#[hax::requires(x + y < 150)]
pub fn constrained_sum(x: u32, y: u32) -> u32 {
x + y
}
/// Equivalent: combine with &&
#[hax::requires(x < 100 && y < 100 && x + y < 150)]
pub fn constrained_sum_2(x: u32, y: u32) -> u32 {
x + y
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
/// Precondition using Int for overflow reasoning
#[hax::requires(
Int::from(x) + Int::from(y) <= Int::from(u32::MAX)
)]
pub fn overflow_safe_add(x: u32, y: u32) -> u32 {
x + y
}
/// Precondition with quantifiers
#[hax::requires(forall(|i: usize|
implies(i < arr.len(), arr[i] > 0)
))]
pub fn all_positive_product(arr: &[u32]) -> u64 {
arr.iter().map(|&x| x as u64).product()
}
/// Precondition relating multiple parameters
#[hax::requires(arr.len() == weights.len())]
#[hax::requires(forall(|i: usize|
implies(i < arr.len(), weights[i] > 0)
))]
pub fn weighted_sum(arr: &[u32], weights: &[u32]) -> u64 {
arr.iter()
.zip(weights.iter())
.map(|(&a, &w)| a as u64 * w as u64)
.sum()
}Pattern 1: Bounds Checking
#[hax::requires(index < arr.len())]
pub fn safe_get(arr: &[u32], index: usize) -> u32 {
arr[index]
}Pattern 2: Non-Zero Divisor
#[hax::requires(divisor != 0)]
pub fn safe_divide(dividend: u32, divisor: u32) -> u32 {
dividend / divisor
}Pattern 3: Sorted Input
#[hax::requires(forall(|i: usize| forall(|j: usize|
implies(i < j && j < arr.len(), arr[i] <= arr[j])
)))]
pub fn binary_search_sorted(arr: &[u32], target: u32) -> Option<usize> {
// Implementation can assume array is sorted
unimplemented!()
}Pattern 4: Non-Empty Collection
#[hax::requires(v.len() > 0)]
pub fn head<T>(v: &[T]) -> &T {
&v[0]
}Postconditions specify what must be true after the function returns.
use hax_lib as hax;
/// Simple postcondition: result is positive
#[hax::ensures(|result| result > 0)]
pub fn always_positive() -> u32 {
42
}
/// Postcondition relating result to input
#[hax::ensures(|result| result > x)]
pub fn increment(x: u32) -> u32 {
x + 1
}
/// Postcondition on Option
#[hax::ensures(|result| match result {
Some(val) => val > 0,
None => true
})]
pub fn try_positive(x: i32) -> Option<u32> {
if x > 0 {
Some(x as u32)
} else {
None
}
}use hax_lib as hax;
/// Multiple postconditions (all must hold)
#[hax::ensures(|result| result >= x)]
#[hax::ensures(|result| result >= y)]
#[hax::ensures(|result| result == x || result == y)]
pub fn max(x: u32, y: u32) -> u32 {
if x > y { x } else { y }
}
/// Equivalent: combine conditions
#[hax::ensures(|result|
result >= x && result >= y && (result == x || result == y)
)]
pub fn max_2(x: u32, y: u32) -> u32 {
if x > y { x } else { y }
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
/// Postcondition using Int
#[hax::ensures(|result| match result {
Some(sum) => Int::from(sum) == Int::from(x) + Int::from(y),
None => Int::from(x) + Int::from(y) > Int::from(u32::MAX)
})]
pub fn checked_add(x: u32, y: u32) -> Option<u32> {
x.checked_add(y)
}
/// Postcondition with quantifiers
#[hax::requires(arr.len() > 0)]
#[hax::ensures(|result|
forall(|i: usize| implies(i < arr.len(), result >= arr[i])) &&
exists(|i: usize| i < arr.len() && result == arr[i])
)]
pub fn find_max(arr: &[u32]) -> u32 {
let mut max = arr[0];
for i in 1..arr.len() {
if arr[i] > max {
max = arr[i];
}
}
max
}Pattern 1: Correctness Specification
use hax_lib::int::Int;
#[hax::requires(divisor != 0)]
#[hax::ensures(|result|
Int::from(result) * Int::from(divisor) + Int::from(dividend % divisor)
== Int::from(dividend)
)]
pub fn divide(dividend: u32, divisor: u32) -> u32 {
dividend / divisor
}Pattern 2: Range Specification
#[hax::ensures(|result| result >= min && result <= max)]
pub fn clamp(x: u32, min: u32, max: u32) -> u32 {
if x < min {
min
} else if x > max {
max
} else {
x
}
}Pattern 3: Preservation Property
#[hax::ensures(|result| result.len() == arr.len())]
pub fn double_all(arr: &[u32]) -> Vec<u32> {
arr.iter().map(|&x| x * 2).collect()
}Pattern 4: Relational Specification
#[hax::ensures(|result|
forall(|i: usize| implies(
i < result.len(),
exists(|j: usize| j < arr.len() && arr[j] == result[i])
))
)]
pub fn filter_positive(arr: &[i32]) -> Vec<i32> {
arr.iter().copied().filter(|&x| x > 0).collect()
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
/// Full contract: precondition + postcondition
#[hax::requires(x > 0 && y > 0)]
#[hax::requires(Int::from(x) * Int::from(y) <= Int::from(u32::MAX))]
#[hax::ensures(|result| Int::from(result) == Int::from(x) * Int::from(y))]
#[hax::ensures(|result| result > 0)]
pub fn safe_multiply(x: u32, y: u32) -> u32 {
x * y
}use hax_lib as hax;
#[hax::requires(divisor != 0)]
#[hax::ensures(|result| result >= 0)]
pub fn divide(dividend: u32, divisor: u32) -> u32 {
dividend / divisor
}
pub fn caller() {
let x = 10u32;
let y = 5u32;
// Must prove precondition holds
if y != 0 {
let result = divide(x, y);
// Can assume postcondition holds
hax::assert!(result >= 0);
}
}use hax_lib as hax;
/// Stronger precondition, weaker postcondition
#[hax::requires(x > 10)] // Stronger than x > 0
#[hax::ensures(|result| result > 0)] // Weaker than result > x
pub fn strong_requirement(x: u32) -> u32 {
x - 5
}
/// Can call from function with weaker precondition
#[hax::requires(x > 0)]
pub fn weak_caller(x: u32) {
if x > 10 {
let _ = strong_requirement(x); // OK: proves stronger precondition
}
}use hax_lib as hax;
#[hax::ensures(|result| result.0 + result.1 == hax::old(x))]
pub fn split_number(x: u32) -> (u32, u32) {
let half = x / 2;
let rem = x - half;
(half, rem)
}use hax_lib as hax;
/// Postcondition references old and new state
#[hax::ensures(|_| *x == hax::old(*x) + 1)]
pub fn increment_in_place(x: &mut u32) {
*x += 1;
}
/// Contract on mutable slice
#[hax::ensures(|_| forall(|i: usize|
implies(i < arr.len(), arr[i] == hax::old(arr[i]) * 2)
))]
pub fn double_in_place(arr: &mut [u32]) {
for i in 0..arr.len() {
arr[i] *= 2;
}
}use hax_lib as hax;
/// Different postconditions for different cases
#[hax::ensures(|result| implies(x >= 0, result == x as u32))]
#[hax::ensures(|result| implies(x < 0, result == 0))]
pub fn to_unsigned_saturating(x: i32) -> u32 {
if x >= 0 {
x as u32
} else {
0
}
}Loops are verified using invariants and termination measures.
Loop invariants are properties that hold before and after each iteration.
use hax_lib as hax;
pub fn count_up(n: u32) -> u32 {
let mut i = 0;
#[hax::loop_invariant(i <= n)]
while i < n {
i += 1;
}
i
}use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
pub fn sum_to_n(n: u32) -> u64 {
let mut sum = 0u64;
let mut i = 0u32;
#[hax::loop_invariant(i <= n)]
#[hax::loop_invariant(Int::from(sum) == Int::from(i) * (Int::from(i) + 1) / 2)]
while i < n {
i += 1;
sum += i as u64;
}
sum
}use hax_lib as hax;
pub fn sum_array(arr: &[u32]) -> u64 {
let mut sum = 0u64;
let mut i = 0usize;
#[hax::loop_invariant(i <= arr.len())]
#[hax::loop_invariant(sum == array_sum(arr, i))]
while i < arr.len() {
sum += arr[i] as u64;
i += 1;
}
sum
}
#[hax::exclude]
fn array_sum(arr: &[u32], n: usize) -> u64 {
arr[..n].iter().map(|&x| x as u64).sum()
}Pattern 1: Range Invariant
#[hax::loop_invariant(i >= 0 && i <= n)]
while i < n {
// i always in range [0, n]
i += 1;
}Pattern 2: Accumulator Invariant
use hax_lib::int::Int;
#[hax::loop_invariant(Int::from(sum) <= Int::from(i) * Int::from(max_value))]
while i < n {
sum += arr[i];
i += 1;
}Pattern 3: Relationship Invariant
#[hax::loop_invariant(left <= right)]
#[hax::loop_invariant(right <= arr.len())]
while left < right {
let mid = left + (right - left) / 2;
// Binary search logic
}Pattern 4: Progress Invariant
#[hax::loop_invariant(processed == i)]
while i < arr.len() {
// Process arr[i]
processed += 1;
i += 1;
}Termination measures prove that loops terminate.
use hax_lib as hax;
pub fn countdown(n: u32) -> u32 {
let mut i = n;
#[hax::loop_decreases(i)]
while i > 0 {
i -= 1;
}
i
}use hax_lib as hax;
pub fn binary_search(arr: &[u32], target: u32) -> Option<usize> {
let mut left = 0;
let mut right = arr.len();
#[hax::loop_invariant(left <= right && right <= arr.len())]
#[hax::loop_decreases(right - left)]
while left < right {
let mid = left + (right - left) / 2;
if arr[mid] == target {
return Some(mid);
} else if arr[mid] < target {
left = mid + 1;
} else {
right = mid;
}
}
None
}use hax_lib as hax;
pub fn gcd(mut a: u32, mut b: u32) -> u32 {
#[hax::loop_decreases((a + b, b))] // Lexicographic ordering
while b != 0 {
let temp = b;
b = a % b;
a = temp;
}
a
}Ghost variables exist only for verification, not at runtime.
use hax_lib as hax;
use hax_lib::int::{Int, ToInt};
pub fn fast_exponentiation(base: u64, exp: u32) -> u64 {
let mut result = 1u64;
let mut b = base;
let mut e = exp;
#[hax::ghost] let initial_base = base.to_int();
#[hax::ghost] let initial_exp = exp.to_int();
#[hax::loop_invariant(
Int::from(result) * Int::from(b).pow(e.to_int())
== initial_base.pow(initial_exp)
)]
#[hax::loop_decreases(e)]
while e > 0 {
if e % 2 == 1 {
result *= b;
}
b *= b;
e /= 2;
}
result
}use hax_lib as hax;
pub fn reverse_array(arr: &mut [u32]) {
#[hax::ghost] let original = arr.to_vec();
let mut left = 0;
let mut right = arr.len();
#[hax::loop_invariant(left <= right && right <= arr.len())]
#[hax::loop_invariant(forall(|i: usize|
implies(i < left, arr[i] == original[arr.len() - 1 - i])
))]
while left < right {
right -= 1;
arr.swap(left, right);
left += 1;
}
}use hax_lib as hax;
pub fn matrix_multiply(a: &[Vec<u32>], b: &[Vec<u32>]) -> Vec<Vec<u32>> {
let n = a.len();
let mut result = vec![vec![0u32; n]; n];
let mut i = 0;
#[hax::loop_invariant(i <= n)]
while i < n {
let mut j = 0;
#[hax::loop_invariant(j <= n)]
while j < n {
let mut k = 0;
#[hax::loop_invariant(k <= n)]
while k < n {
result[i][j] += a[i][k] * b[k][j];
k += 1;
}
j += 1;
}
i += 1;
}
result
}use hax_lib as hax;
pub fn find_first_negative(arr: &[i32]) -> Option<usize> {
let mut i = 0;
#[hax::loop_invariant(i <= arr.len())]
#[hax::loop_invariant(forall(|j: usize|
implies(j < i, arr[j] >= 0)
))]
while i < arr.len() {
if arr[i] < 0 {
return Some(i);
}
i += 1;
}
None
}use hax_lib as hax;
pub fn remove_duplicates(arr: &mut Vec<u32>) -> usize {
if arr.is_empty() {
return 0;
}
let mut write_idx = 1;
let mut read_idx = 1;
#[hax::loop_invariant(write_idx <= read_idx)]
#[hax::loop_invariant(read_idx <= arr.len())]
#[hax::loop_invariant(write_idx > 0)]
while read_idx < arr.len() {
if arr[read_idx] != arr[write_idx - 1] {
arr[write_idx] = arr[read_idx];
write_idx += 1;
}
read_idx += 1;
}
write_idx
}The #[opaque] attribute hides implementation details from the verifier.
use hax_lib as hax;
/// Verify this function once
#[hax::opaque]
#[hax::requires(n > 0)]
#[hax::ensures(|result| result > 0)]
pub fn complex_function(n: u32) -> u32 {
// Complex implementation
// Verified once, then treated as black box
(1..=n).sum()
}
/// Callers only see the contract
pub fn caller(x: u32) {
if x > 0 {
let result = complex_function(x);
hax::assert!(result > 0); // Fast: uses contract only
}
}use hax_lib as hax;
/// DON'T MAKE OPAQUE: Simple, fast to verify
pub fn add_one(x: u32) -> u32 {
x + 1
}
/// DO MAKE OPAQUE: Complex, slow to verify
#[hax::opaque]
#[hax::ensures(|result| result >= input)]
pub fn complex_crypto_operation(input: &[u8]) -> Vec<u8> {
// 1000 lines of cryptographic code
// Better to verify once and reuse the contract
vec![]
}Lemmas are reusable proof fragments.
use hax_lib as hax;
use hax_lib::int::Int;
/// A lemma proving a mathematical property
#[hax::lemma]
#[hax::ensures(|_| forall(|x: u32, y: u32|
Int::from(x) + Int::from(y) == Int::from(y) + Int::from(x)
))]
pub fn addition_commutes() {}
/// Use the lemma
pub fn example(a: u32, b: u32) {
addition_commutes(); // Import the proof
hax::assert!(Int::from(a) + Int::from(b) == Int::from(b) + Int::from(a));
}use hax_lib as hax;
#[hax::lemma]
#[hax::ensures(|_| x + (y + z) == (x + y) + z)]
pub fn addition_associative(x: u32, y: u32, z: u32) {}
pub fn use_lemma(a: u32, b: u32, c: u32) {
addition_associative(a, b, c);
hax::assert!(a + (b + c) == (a + b) + c);
}use hax_lib as hax;
/// Don't extract this function
#[hax::exclude]
pub fn unverified_helper() {
// Won't be verified or extracted
}
/// Extract this function
pub fn verified_function() {
// Will be extracted
}use hax_lib as hax;
/// Force extraction even if not called
#[hax::include]
fn helper_function() {
// Will be extracted even if unused
}use hax_lib as hax;
/// Set F* verification options
#[hax::fstar::options("--z3rlimit 200 --fuel 4")]
pub fn f_star_function() {}
/// Inject F* code
pub fn with_f_star_lemma() {
hax::fstar!(r#"
FStar.Math.Lemmas.pow2_plus 16 16
"#);
}
/// Set verification status
#[hax::fstar::verification_status(lax)]
pub fn skip_verification() {}use hax_lib as hax;
/// Override Lean type
#[hax::lean::type("Nat")]
pub fn lean_function() -> u32 {
42
}The cargo-hax tool orchestrates the entire verification process.
# Extract to F*
cargo hax into fstar
# Extract to Lean
cargo hax into lean
# Extract to Coq
cargo hax into coq
# Extract to ProVerif
cargo hax into pro-verif
# Get JSON AST (for debugging)
cargo hax json# Verbose output
cargo hax -v into fstar
# Very verbose output
cargo hax -vv into fstar
# Quiet mode
cargo hax -q into fstar
# Specify output directory
cargo hax into fstar --output-dir ./proofs
# Dry run (show what would be done)
cargo hax into fstar --dry-run# Include specific modules
cargo hax into fstar --include "crypto::*"
# Include multiple patterns
cargo hax into fstar --include "crypto::*" --include "protocol::*"
# Exclude patterns
cargo hax into fstar --exclude "tests::*"
# Combine include and exclude
cargo hax into fstar --include "src::*" --exclude "src::tests::*"F* Options:
# Set Z3 resource limit
cargo hax into fstar --z3rlimit 200
# Set fuel (normalization depth)
cargo hax into fstar --fuel 4
# Set ifuel (inversion depth)
cargo hax into fstar --ifuel 2
# Combine options
cargo hax into fstar --z3rlimit 200 --fuel 4 --ifuel 2Lean Options:
# Target Lean 4
cargo hax into lean --lean-version 4
# Include mathlib
cargo hax into lean --mathlibCoq Options:
# Target Coq 8.15
cargo hax into coq --coq-version 8.15[package.metadata.hax]
# Global settings
include = ["src/verified/**"]
exclude = ["src/tests/**"]
# F* backend
[package.metadata.hax.into.fstar]
z3rlimit = 100
fuel = 2
ifuel = 1
output_dir = "proofs/fstar"
# Lean backend
[package.metadata.hax.into.lean]
version = 4
mathlib = true
output_dir = "proofs/lean"
# Coq backend
[package.metadata.hax.into.coq]
version = "8.15"
output_dir = "proofs/coq"# Override engine binary path
export HAX_ENGINE_BINARY=/custom/path/hax-engine
# Specify Rust toolchain
export HAX_TOOLCHAIN=nightly-2024-01-01
# Set F* home
export FSTAR_HOME=/path/to/fstar
# Set HACL* home
export HACL_HOME=/path/to/hacl-star
# Enable debug logging
export RUST_LOG=debugmy-project/
├── Cargo.toml
├── src/
│ ├── lib.rs # Main implementation
│ ├── crypto/ # Verified crypto code
│ ├── protocol/ # Verified protocol code
│ └── tests/ # Tests (excluded from verification)
├── proofs/
│ ├── fstar/
│ │ └── extraction/ # Generated F* code
│ ├── lean/
│ │ └── extraction/ # Generated Lean code
│ └── specs/ # Specification documents
└── README.md
# Generated proof files
proofs/**/extraction/
# Hax intermediate files
target/hax/
# F* build artifacts
**/.checked
**/.hints
# Lean build artifacts
**/build/The most mature backend with full feature support.
- ✅ Full specification support
- ✅ SMT solver integration (Z3)
- ✅ Refinement types
- ✅ Loop invariants and termination
- ✅ Effectful programming
- ✅ Module system
- ✅ Inline F* code
# Extract
cargo hax into fstar
# Verify
cd proofs/fstar/extraction
fstar.exe --include $HACL_HOME/lib *.fst
# With specific options
fstar.exe --z3rlimit 200 --fuel 4 MyModule.fstRust:
#[hax::requires(x < 100)]
#[hax::ensures(|result| result > x)]
pub fn increment(x: u32) -> u32 {
x + 1
}Generated F*:
val increment (x: u32) : Pure u32
(requires x <. 100ul)
(ensures fun result -> result >. x)
let increment x = x +! 1ulFocus on panic-freedom and functional correctness.
- ✅ Panic-freedom proofs
- ✅ Safe arithmetic operators
- ✅ Array bounds checking
- ✅ Pattern matching exhaustiveness
⚠️ Limited SMT integration
# Extract
cargo hax into lean
# Build with Lake
cd proofs/lean/extraction
lake buildRust:
pub fn safe_divide(x: u32, y: u32) -> Option<u32> {
x.checked_div(y)
}Generated Lean4:
def safe_divide (x y : UInt32) : Option UInt32 :=
x /? y -- Lean's panic-safe division operator| Feature | F* | Lean4 | Coq | ProVerif |
|---|---|---|---|---|
| Maturity | Stable | Active | Experimental | PoC |
| Preconditions | ✅ | ✅ | ❌ | |
| Postconditions | ✅ | ✅ | ❌ | |
| Loop Invariants | ✅ | ✅ | ❌ | |
| Refinement Types | ✅ | ✅ | ❌ | |
| Panic Proofs | Manual | Automatic | Manual | N/A |
| SMT Solver | Z3 | Limited | No | Symbolic |
| Best For | Crypto, Systems | Functional correctness | Mathematical proofs | Protocols |
use hax_lib as hax;
#[hax::requires(index < arr.len())]
#[hax::ensures(|result| result == arr[index])]
pub fn safe_get(arr: &[u32], index: usize) -> u32 {
arr[index]
}use hax_lib::int::{Int, ToInt};
#[hax::requires(
Int::from(x) + Int::from(y) <= Int::from(u32::MAX)
)]
#[hax::ensures(|result|
Int::from(result) == Int::from(x) + Int::from(y)
)]
pub fn safe_add(x: u32, y: u32) -> u32 {
x + y
}See examples-patterns.md for comprehensive examples including:
- ChaCha20 stream cipher
- Barrett reduction (field arithmetic)
- SHA-256 hash function
- AES operations
use hax_lib as hax;
pub struct Stack<T> {
data: Vec<T>,
}
impl<T> Stack<T> {
#[hax::ensures(|result| result.data.len() == 0)]
pub fn new() -> Self {
Stack { data: Vec::new() }
}
#[hax::ensures(|_| self.data.len() == hax::old(self.data.len()) + 1)]
pub fn push(&mut self, item: T) {
self.data.push(item);
}
#[hax::requires(self.data.len() > 0)]
#[hax::ensures(|_| self.data.len() == hax::old(self.data.len()) - 1)]
pub fn pop(&mut self) -> T {
self.data.pop().unwrap()
}
}use hax_lib as hax;
pub fn with_backend_code() {
// Inject F* code
hax::fstar!(r#"
FStar.Math.Lemmas.pow2_plus 16 16;
assert_norm (pow2 32 == 4294967296)
"#);
// Inject Lean code
hax::lean!(r#"
have h : 2 + 2 = 4 := rfl
"#);
}#[hax::opaque]
pub fn expensive_proof(n: u32) -> u32 {
// Verify once, reuse contract
(0..n).sum()
}// Weak invariant (hard to prove)
#[hax::loop_invariant(sum <= u32::MAX)]
// Strong invariant (easier to prove)
#[hax::loop_invariant(sum <= i * max_element)]| Type | Purpose | Module |
|---|---|---|
Int |
Mathematical integers | hax_lib::int |
Prop |
Logical propositions | hax_lib |
| Trait | Purpose |
|---|---|
Abstraction |
Lift to abstract types |
Concretization |
Lower to concrete types |
Refinement |
Refinement type trait |
RefineAs |
Refinement conversion |
| Macro | Type | Purpose |
|---|---|---|
assert! |
Proof obligation | Verify condition |
assume! |
Assumption | Trust condition |
assert_prop! |
Logical assertion | Assert proposition |
forall! |
Quantifier | Universal quantification |
exists! |
Quantifier | Existential quantification |
implies! |
Logic | Implication |
int! |
Literal | Int literal |
| Macro | Type | Purpose |
|---|---|---|
#[requires] |
Attribute | Precondition |
#[ensures] |
Attribute | Postcondition |
#[loop_invariant] |
Attribute | Loop invariant |
#[decreases] |
Attribute | Termination measure |
#[opaque] |
Attribute | Hide implementation |
#[lemma] |
Attribute | Proof lemma |
#[refinement_type] |
Attribute | Define refinement |
#[exclude] |
Attribute | Don't extract |
#[include] |
Attribute | Force extraction |
For complete macro documentation, see macro-system-complete.md.
- GitHub: https://github.com/hacspec/hax
- Website: https://hax.cryspen.com
- Playground: https://hax-playground.cryspen.com
- Documentation: https://hax.cryspen.com/manual/
- Zulip Chat: https://hacspec.zulipchat.com/
- Blog: https://hax.cryspen.com/blog
- hax-lib-api.md: Complete API reference
- macro-system-complete.md: All macros
- examples-patterns.md: Comprehensive examples
- verification-techniques.md: Proof strategies
- cli-backends.md: CLI and backends
- errors-troubleshooting.md: Error reference
We welcome contributions!
- Join Zulip: https://hacspec.zulipchat.com/
- File Issues: https://github.com/hacspec/hax/issues
- Submit PRs: Follow the contribution guidelines
Key areas of development:
- Rust Engine: Full rewrite in Rust
- Better Error Messages: Improved diagnostics
- More Backends: Expanded backend support
- Performance: Faster verification
- Documentation: More examples and tutorials
The hax library is licensed under Apache-2.0.
This README provides a comprehensive overview of the hax library. For deeper dives into specific topics, please refer to the specialized documentation files listed in the Documentation Structure section.
Happy Verifying! 🎉