Assura
Write what it should do. AI proves it does.
Assura is a contract-first AI-native language that transpiles to verified Rust.
Humans write behavioral contracts. AI writes implementations. The compiler
proves correctness mathematically via SMT solvers. Ships as native or WASM
binaries through rustc.
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
ensures { abs(result) <= abs(a) }
effects { pure }
}
Quick Start
# Preferred:
cargo install assura --locked
# Or from a monorepo clone:
# cargo install --path crates/assura-cli --locked
# Check a contract
assura check my_contract.assura
# Build (generates verified Rust)
assura build my_contract.assura
See CRATES-IO.md and the root README for co-publish details and prebuilt GitHub Release installers.
Documentation
- Getting started: Install, first check, build, run
- Tutorial: Broader language tour after the first contract
- What we prove: Verified / Unknown / Counterexample honesty map
- SMT portfolio note: Dual solvers, timeouts, Unknown policy
- Compared to other tools: Dafny, Verus, Liquid Haskell, tests
- Case studies: Heartbleed, libwebp, showcase demos
- For AI agents: JSON check, IR acceptance, MCP
- Preferred URLs: Canonical links (not assura.dev)
- Cheatsheet: Quick reference for syntax and features
- Cookbook: Common patterns and recipes
- Language Specification: Complete language reference
- Error code index: Axxxxx phase map for agents and humans
- Compiler Internals: How the compiler works
Getting Started with Assura
From install to a verified, runnable implementation.
This guide works on a clean machine (no monorepo required). It uses a
tiny result-bearing showcase contract. For synthesizable ensures shapes
(including result == x and multi-clause bounds), assura check
synthesizes an in-memory implementation body so you get Verified
without writing IR by hand. Residual shapes use
assura build --write-ir (offline) or --auto-implement (LLM).
For broader language coverage, see TUTORIAL.md and demos/README.md (showcase vs EXPECT FAIL taxonomy).
After your first successful check, read What we prove so Verified / Unknown / Counterexample (and vacuous success) are not confused. AI-oriented workflow: AI-AGENTS.md.
1. Install
Preferred (crates.io): requires Rust 1.85+ (edition 2024). Z3 is pulled in automatically for verification builds.
cargo install assura --locked
assura --help
Prebuilt shell installer (GitHub Releases; no Rust toolchain needed for
the binary itself): see the latest release assets
(assura-installer.sh) or the root README. There is no Homebrew
formula yet.
From a git checkout instead of crates.io:
cargo install --path crates/assura-cli --locked
1b. Try without a full local install
If you only want to see a check result before installing:
- Open the repo in GitHub Codespaces (
.devcontainer/devcontainer.jsoninstalls Rust +libz3-dev), then:
Same commands work on any machine that already has Rust 1.85+ and Z3.cargo install --path crates/assura-cli --locked assura check demos/showcase-echo.assura - Watch the demo GIF (no install):
https://github.com/assura-lang/assura/blob/main/assets/demo/assura-check.gif - Copy-paste the showcase from section 2 into a file after
cargo install assura --locked.
There is no hosted WASM playground yet (tracked separately). Prefer install + one local check over a partial browser demo that cannot run Z3.
2. Create a tiny project
mkdir hello-assura && cd hello-assura
Write the contract only (ShowcaseEcho.assura). No sidecar required for
identity:
cat > ShowcaseEcho.assura << 'EOF'
// SHOWCASE: result-bearing identity (auto-synthesized IR).
contract ShowcaseEcho {
input(x: Int)
output(result: Int)
ensures { result == x }
}
EOF
In the Assura monorepo you can use demos/showcase-echo.assura (and
optional demos/ShowcaseEcho.ir if you want an explicit sidecar).
3. Check (prefer all clauses Verified)
assura check ShowcaseEcho.assura
Expected: ShowcaseEcho: ensures ... verified and check passed (no errors).
Synthesizable ensures shapes (no hand IR):
| Family | Examples |
|---|---|
| Equality | result == x, result == x + 1, nested arith, -x |
| Builtins | free or method: abs/min/max/clamp/signum (e.g. x.abs()) |
| Bounds | result >= e, >, <=, <; And chains; multi-clause bounds prefer a lower-bound witness |
| Multi-ensures | Prefer top-level result == e over if/match/other plans; combine pure bound ensures (lower-bound witness) otherwise |
| Structure | fields, tuples, length, Bool logic/cmp, if/match/let, same-file pure calls |
Shapes the planner cannot synthesize still report Unknown (not a fake pass). Ladder when that happens:
- Simplify ensures toward the table above, or
assura build file.assura --write-ir(offline heuristic IR next to source), orassura build file.assura --auto-implement(offline first, then LLM for residuals).
Inequality synthesis picks one witness body (e.g. result >= x uses
result = x); multi-bound clauses share one witness (prefer lower bound).
That is not a full specification of every satisfying implementation.
Use assura check -v to see synthesized in-memory: ContractName. For
multi-ensures contracts, verbose also names the body driver ensures
(which clause selected the IR body) and any residual ensures that were
not the driver (still checked under that body; may CE or stay Unknown).
Agents using assura check --json get the same surface under
file_info.ir (colocated, synthesized, synth_notes).
Optional: co-located IR sidecar
When you want an explicit body (or synthesis cannot cover the shape),
add {ContractName}.ir next to the source (name matches the contract,
not the file stem):
cat > ShowcaseEcho.ir << 'EOF'
module ShowcaseEcho {
fn #0 : ($0: Int) -> Int ! pure
{
$result = load $0 : Int
}
}
EOF
4. Build (IR becomes the implementation)
assura build ShowcaseEcho.assura --output generated
Assura loads co-located IR for verification and injects it into the
generated Rust body (you should see a log line about injected IR bodies).
assura build --write-ir also writes that IR next to the source so a later
assura build reuses it. Field and abs/min/max/clamp/signum IR lower to real
Rust (.y, .abs(), .min(), .clamp(), .signum()) that cargo test
exercises via proptest.
The generated crate is a library with a property test, not a binary.
5. Run tests on the generated artifact
cd generated
cargo test
Expected: proptest test test_showcaseecho passes (identity on random i64
inputs, with debug_assert! on the ensures).
Call the generated API from your own code:
use generated::contract_showcaseecho::check;
fn main() {
assert_eq!(check(42), 42);
}
A minimal extra smoke test inside the generated crate (optional; do not
commit edits under generated/, re-run assura build after contract changes):
cd generated
cat >> src/lib.rs << 'EOF'
#[cfg(test)]
mod smoke {
#[test]
fn echo_forty_two() {
assert_eq!(super::contract_showcaseecho::check(42), 42);
}
}
EOF
cargo test smoke::echo_forty_two
What you just proved
| Step | Command | What green means |
|---|---|---|
| Check | assura check ... | SMT proved the ensures under the IR body (sidecar or synthesized) |
| Build | assura build ... | Rust body implements the same IR (not todo!()) |
| Test | cargo test | Runtime asserts + proptest agree with the postcondition |
Offline IR (no LLM) and runnable binary
Generate co-located IR from ensures heuristics (no API key), inject it into
Rust, and emit a binary you can cargo run:
assura build ShowcaseEcho.assura --write-ir --bin --output generated
cd generated
cargo run -- 42
# prints: 42
Auto-implement residual shapes
--auto-implement fills bodies for contracts that still lack IR:
- Co-located
.iron disk (if any) - Offline ensures heuristics (same as
--write-ir, no API key) - LLM only for remaining unanalyzable ensures (needs AI config)
assura build richer.assura --auto-implement --output generated
Strict verification (Unknown limitations fail the check):
assura check ShowcaseEcho.assura --strict
Directory mode for SHOWCASE demos only:
assura check demos --showcase-only
Next steps
- More demos: demos/README.md (prefer SHOWCASE files)
- Language tutorial: TUTORIAL.md
- Call-shaped helpers: same-file pure callees get non-identity IR siblings when their ensures are analyzable
Optional monorepo smoke
From a full clone (CI or local):
bash scripts/smoke-getting-started.sh
# or manually:
assura check demos/showcase-echo.assura
assura build demos/showcase-echo.assura --output /tmp/assura-gs-out
(cd /tmp/assura-gs-out && cargo test)
Assura Tutorial
A hands-on guide to writing, checking, and building Assura contracts.
Fastest path to a verified, runnable example: start with
GETTING-STARTED.md (install → check → build →
cargo test with co-located IR).
Installation
Preferred: crates.io
cargo install assura --locked
Requires Rust 1.85+ (edition 2024). The first build may download a Z3 prebuilt automatically.
Prebuilt binary
Download an installer from GitHub Releases (cargo-dist; multi-platform).
From source
# From a clone:
cargo install --path crates/assura-cli --locked
# Or from GitHub without cloning the tree:
cargo install --git https://github.com/assura-lang/assura assura --locked
Verify the installation
assura --help
Prerequisites for verification
The Z3 SMT solver is downloaded automatically during cargo build (via
the z3 crate’s gh-release feature). No manual Z3 installation is
needed.
CVC5 is an optional alternative solver for portfolio mode (tries both solvers). It is not required for normal use:
bash scripts/setup-cvc5.sh
# paste the printed export lines (CVC5_LIB_DIR, CVC5_INCLUDE_DIR)
Step 1: Create a Project
assura init my-project
cd my-project
This creates:
my-project/
assura.toml # Project configuration
contracts/
lib.assura # Starter contract
The generated assura.toml:
[package]
name = "my-project"
version = "0.1.0"
[build]
target = "native" # "native" or "wasm32-wasi"
output = "generated"
[verify]
smt-solver = "z3" # "z3", "cvc5", or "portfolio"
layer = 1 # 0 = structural only, 1 = SMT
timeout = 1000 # SMT timeout in ms
# [dependencies] # Optional: import contracts from other projects
# math-lib = { path = "../math-lib" }
Step 2: Write a Contract
Edit contracts/lib.assura:
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { b != 0 }
}
A contract declares:
- input(…): parameters the function accepts
- output(…): what it returns
- requires { … }: preconditions (caller’s responsibility)
- ensures { … }: postconditions (implementation’s responsibility)
The ensures { b != 0 } clause is trivially verified by Z3 because
the requires clause already guarantees b != 0. As you learn the
language, you’ll write more expressive postconditions.
Step 3: Check Your Contract
assura check contracts/lib.assura
Output:
Verification (1 clause(s)):
SafeDivision:
ensures ... verified
contracts/lib.assura: check passed (no errors)
Verbose mode
assura check contracts/lib.assura --verbose
Shows timing for each pipeline phase (lex, parse, resolve, typecheck, verify).
Verification layers
| Layer | Flag | What it checks |
|---|---|---|
| 0 | --layer 0 | Type checking, name resolution, linearity |
| 1 | --layer 1 | SMT verification via Z3: refinement types, arithmetic (default) |
What Verified / Unknown / Counterexample mean (and what is not proved): What we prove.
assura check contracts/lib.assura --layer 0
Step 4: Build to Rust
assura build contracts/lib.assura
This generates a Rust project in generated/:
generated/
Cargo.toml
src/
lib.rs
The generated Rust includes debug_assert! statements derived from
your requires and ensures clauses. Run the generated code:
cd generated
cargo test
Custom output directory
assura build contracts/lib.assura --output my-output
WASM target
assura build contracts/lib.assura --target wasm
This generates a project configured for wasm32-wasip1, including a
.cargo/config.toml with the target pre-set.
Step 5: Multi-File Projects
As your project grows, split contracts across multiple files and use imports to reference them.
Project layout
my-project/
assura.toml
contracts/
lib.assura # Root contract
math.assura # Imported by lib
Importing local modules
Use dot-separated paths. The path maps to the filesystem relative to the project root:
import contracts.math
contract App {
input(x: Int)
requires { x >= 0 }
}
import contracts.math loads contracts/math.assura.
External dependencies
Add a [dependencies] section to assura.toml to import contracts
from other projects:
[package]
name = "my-project"
version = "0.1.0"
[dependencies]
math-lib = { path = "../math-lib" }
Then import from the dependency using its name (hyphens become underscores in import paths):
import math_lib.core
contract App {
input(x: Int)
requires { x >= 0 }
}
Checking a project
Point assura check at the project directory (not a single file):
assura check .
This discovers all .assura files, resolves imports (including
dependencies), and type-checks every module.
Step 6: Understand Errors
When a contract fails verification, Assura shows a counterexample:
Verification (1 clause(s)):
SafeDivision:
ensures ... COUNTEREXAMPLE
| result -> 0
| a -> 1
| b -> 2
Use assura explain to learn about specific error codes:
assura explain A03001 # Type mismatch
assura explain A05001 # Linear type used twice
assura explain A07003 # Unknown effect
Error spans are precise even for expressions inside braced clauses (e.g. requires { x > 0 }), thanks to full trivia capture in the parser. A type error on true will point exactly at the sub-expression, not the requires keyword.
Contract Features
Refinement types
Narrow a base type with a predicate:
type Positive = { v: Int | v > 0 }
type Percentage = { v: Float | v >= 0.0 && v <= 100.0 }
Effects
Declare what side effects a contract may perform:
contract ReadFile {
effects {
io
fs
}
requires {
path != ""
}
}
Valid effect names: io, database, logging, mem, net, fs,
rng, time, alloc, diverge, random, and dotted sub-effects
like console.read, filesystem.write.
Quantifiers
Express properties over collections:
contract Sorted {
ensures {
forall i in data : i >= 0
}
}
Services with typestate
Model stateful protocols:
service Connection {
states { Disconnected, Connected }
operation connect {
requires {
host != ""
}
ensures {
connected == true
}
}
}
Ghost code and decreases clauses
Prove termination of recursive algorithms:
fn factorial(n: Nat) -> Nat
requires {
n >= 0
}
ensures {
result >= 1
}
decreases {
n
}
Formatting
Format .assura files (like rustfmt for Rust):
assura fmt contracts/lib.assura
Check formatting without modifying:
assura fmt contracts/lib.assura --check
Solver Selection
Choose which SMT solver to use:
# Z3 (default)
assura check file.assura --solver z3
# CVC5
assura check file.assura --solver cvc5
# Portfolio: tries Z3 first, falls back to CVC5 on timeout
assura check file.assura --solver portfolio
Or set it in assura.toml:
[verify]
smt-solver = "portfolio"
VS Code Extension
Install the extension for syntax highlighting and LSP integration:
cd editors/vscode
npm install && npm run compile
Features: syntax highlighting, inline diagnostics via the Assura LSP server.
AI Agent Setup
To configure an AI coding assistant to use Assura, run:
assura agent-instructions > .assura-context.md
This outputs a compact reference with Assura syntax, type mappings, CLI commands, and workflow steps that you can add to your agent’s system prompt or project instructions (AGENTS.md, .cursorrules, etc.).
See the scenario guides for detailed walkthroughs of AI-assisted development workflows.
Part 2: Learning by Example
This section walks through five contracts of increasing complexity. Each builds on concepts from the previous one.
Example 1: Input Validation
The simplest useful contract: validate that a username meets length requirements.
contract ValidateUsername {
input(name: String, min_len: Nat, max_len: Nat)
output(valid: Bool)
requires { min_len > 0 }
requires { max_len >= min_len }
requires { name.length() >= 0 }
ensures { max_len >= min_len }
}
Save this as validate.assura and run:
assura check validate.assura
Expected output:
ValidateUsername:
ensures ... verified
validate.assura: check passed (no errors)
What you learned: requires sets preconditions that callers must
satisfy. ensures sets postconditions that the implementation must
guarantee. Z3 proves ensures holds given the requires.
Example 2: Arithmetic with Multiple Clauses
Contracts can have multiple requires and ensures clauses. Each
is checked independently.
contract SafeAverage {
input(a: Int, b: Int, max: Int)
output(avg: Int)
requires { a >= 0 }
requires { b >= 0 }
requires { max > 0 }
requires { a <= max }
requires { b <= max }
ensures { max >= 0 }
ensures { a + b >= 0 }
}
This contract specifies an averaging function where both inputs are
non-negative and bounded by max. Z3 proves each ensures clause
independently against the combined requires.
Example 3: A Failing Contract (Reading Counterexamples)
This contract has a bug: the ensures clause does not follow from the preconditions.
contract BuggyClamp {
input(value: Int, low: Int, high: Int)
output(result: Int)
requires { low <= high }
ensures { result >= low }
}
Run assura check buggy_clamp.assura and you will see:
BuggyClamp:
ensures ... counterexample found
result = -1, low = 0
Why it fails: result is an unconstrained output variable. Z3 can
assign it any value. Since nothing forces result >= low, Z3 finds
result = -1, low = 0 as a counterexample.
The lesson: In Assura, result and output variables are free
(the language is specification-only, with no implementation bodies).
Write ensures clauses that follow logically from requires, or
that constrain relationships between inputs only.
Example 4: Effects and Safety Annotations
Assura tracks side effects. Declare which effects a function may perform:
contract ReadConfig {
input(path: String)
output(config: String)
effects { io, fs }
requires { path.length() > 0 }
ensures { path.length() > 0 }
}
Valid effect names include: io, fs, net, database, logging,
mem, rng, time, alloc, and dotted sub-effects like
filesystem.write, network.connect, database.read.
Example 5: Invariants and Quantifiers
Contracts can express properties that must hold for all elements:
contract SortedArray {
input(arr: List<Int>, n: Nat)
requires { n >= 0 }
// Every element is non-negative (real property of `arr`, not a tautology).
invariant { forall i in arr: i >= 0 }
ensures { n >= 0 }
}
Use --layer 2 for quantified invariant verification:
assura check sorted.assura --layer 2
Part 3: Real-World CVE Walkthrough
This section walks through demos/heartbleed.assura, which models
CVE-2014-0160 (the Heartbleed bug).
The Vulnerability
Heartbleed was a buffer over-read in OpenSSL’s TLS heartbeat extension.
The server reads payload_length bytes from the request but does not
check that payload_length is within the actual received data. An
attacker sends a small payload with a large payload_length, causing
the server to leak memory contents.
CVSS: 7.5 (High)
The Assura Contract
Open demos/heartbleed.assura:
contract tls_heartbeat_response {
input(
payload_length: Nat,
padding_length: Nat,
record_length: Nat
)
output(response: Bytes)
requires { payload_length > 0 }
requires { padding_length > 0 }
requires { 3 + payload_length + padding_length <= record_length }
requires { record_length > 0 }
ensures { record_length > payload_length }
ensures { record_length > 0 }
}
What Each Clause Does
requires { payload_length > 0 }: The payload must not be emptyrequires { 3 + payload_length + padding_length <= record_length }: The key fix: total size (3-byte header + payload + padding) must fit within the record. This is the check that OpenSSL was missing.ensures { record_length > payload_length }: The record is always larger than the payload (no over-read possible)
Running the Check
assura check demos/heartbleed.assura --verbose
Z3 proves that record_length > payload_length follows from
3 + payload_length + padding_length <= record_length (since
padding_length > 0, the record must be strictly larger).
What Would Happen Without the Fix
Remove the bounds-check requires clause and Z3 immediately finds a
counterexample: payload_length = 100, record_length = 5. This is
exactly the Heartbleed scenario.
Part 4: Common Mistakes and Debugging
“counterexample found” on ensures
Symptom: Your ensures clause gets a counterexample even though it “should” be true.
Cause: Output variables (result, variables in output()) are
free. Z3 can assign them any value. Your ensures must follow logically
from requires alone, or constrain only input relationships.
Fix: Either:
- Make your ensures reference only input variables
- Add requires clauses that constrain the relationship more tightly
“unknown” instead of “verified”
Symptom: Z3 returns “unknown” for a clause.
Possible causes:
- Timeout: The formula is too complex. Try
--timeout 30000(30s) - Non-linear arithmetic: Z3 struggles with multiplication of
variables. Simplify the formula or use
--solver cvc5 - Unmodelable features: Some language features are not yet encoded in SMT. The CLI shows these as warnings (exit 0)
Unknown effect names
Symptom: Error A07003 “unknown effect”
Fix: Use one of the known effect names: io, database,
logging, mem, net, fs, rng, time, alloc, diverge,
random. For sub-effects, use dotted names: console.read,
filesystem.write, network.connect, database.read, log.info.
“expected type X, got Y”
Symptom: Type error A03001
Fix: Check that your types match. Common mistakes:
- Using
Integerinstead ofInt - Using
Naturalinstead ofNat - Mixing
IntandNatwithout explicit bounds
Reading error output
Assura uses ariadne for rich error display:
error[A03001]: type mismatch
--> myfile.assura:5:12
|
5 | requires { x > "hello" }
| ^^^^^^^^^^^ expected Int, found String
The arrow (-->) points to the file and line. The underline shows
exactly which expression has the error. Use assura explain A03001
for a detailed description of any error code.
Verification layers
| Layer | What it checks | Timeout | Flag |
|---|---|---|---|
| 0 | Type checking only (no solver) | instant | --layer 0 |
| 1 | Standard SMT (requires/ensures) | 1s | --layer 1 (default) |
| 2 | Quantified invariants, termination, roundtrips | 10s | --layer 2 |
| 3 | BMC, k-induction, liveness properties | 30s | --layer 3 |
Higher layers are slower but check more properties. Start with the default (layer 1) and increase when needed.
Part 5: Integration Guides
CI Integration
Add Assura checks to your GitHub Actions workflow:
name: Assura
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Z3
run: sudo apt-get install -y libz3-dev
- name: Install Assura
run: cargo install --git https://github.com/assura-lang/assura assura
- name: Check contracts
run: assura check src/**/*.assura
LSP Integration
The Assura LSP server provides real-time diagnostics in your editor:
# Start the LSP server
assura lsp
Configure your editor’s LSP client to use assura lsp as the server
command. The server provides:
- Inline diagnostics (parse errors, type errors, verification results)
- Hover information (type of expressions, contract summaries)
- Go to definition
- Document symbols
MCP Server
For AI agent integration, Assura provides an MCP (Model Context Protocol) server:
# Start the MCP server
assura mcp
The MCP server exposes tools that AI agents can use to check contracts, get error explanations, and generate contract templates.
gRPC Server
For programmatic access from any language:
# Start the gRPC server
assura server --port 50051
The gRPC API supports streaming verification results and batch checking.
Quick Reference
Contract Structure
contract Name {
input(param1: Type1, param2: Type2)
output(result: Type)
requires { precondition }
ensures { postcondition }
invariant { property }
effects { effect1, effect2 }
decreases { measure }
}
Types
| Type | Description | Example |
|---|---|---|
Int | Arbitrary-precision integer | 42, -1 |
Nat | Non-negative integer | 0, 100 |
Float | Floating-point number | 3.14 |
Bool | Boolean | true, false |
String | Text string | "hello" |
Bytes | Byte sequence | binary data |
Unit | No value | () |
List<T> | Ordered collection | List<Int> |
Map<K,V> | Key-value mapping | Map<String, Int> |
Option<T> | Optional value | Option<Bool> |
Operators
| Operator | Meaning |
|---|---|
&& | Logical AND |
|| | Logical OR |
! | Logical NOT |
==, != | Equality |
<, >, <=, >= | Comparison |
+, -, *, /, % | Arithmetic |
forall x in T: P | Universal quantifier |
exists x in T: P | Existential quantifier |
old(expr) | Pre-state value |
CLI Commands
| Command | Description |
|---|---|
assura check file.assura | Check a contract |
assura check file.assura --verbose | Verbose output with timing |
assura check file.assura --layer 2 | Layer 2 verification |
assura check file.assura --layer 3 | Layer 3 (BMC/k-induction) |
assura build file.assura | Generate Rust code |
assura fmt file.assura | Format a contract |
assura explain AXXXXX | Explain an error code |
assura init project-name | Create a new project |
assura lsp | Start LSP server |
assura mcp | Start MCP server |
assura diff a.assura b.assura | Compare contracts |
Declaration Types
# Standard contract
contract Name { ... }
# External function (no body)
extern fn name(params) -> ReturnType { ... }
# Function with contract
fn name(params) -> ReturnType { ... }
# Liveness property
liveness Name {
prove: eventually(condition)
}
# Service with typestate
service Name {
states { State1, State2 }
transitions { State1 -> State2 }
}
# Feature max constant
feature_max NAME: Type = value
# Enum type
enum Name { Variant1, Variant2 }
Next Steps
- Read the scenario guides for real workflow walkthroughs
- Read the demo contracts in
demos/for real-world examples - See the language specification for the full grammar
- See the internals documentation for compiler architecture
Assura Quick Reference
Types
| Rust | Assura | Notes |
|---|---|---|
i8..i128, isize | Int | Arbitrary-precision signed |
u8..u128, usize | Nat | Non-negative integer |
f32, f64 | Float | IEEE 754 |
bool | Bool | |
String, &str | String | UTF-8 |
Vec<u8>, &[u8] | Bytes | Byte buffer |
() | Unit | |
!, Infallible | Never | |
Vec<T> | List<T> | |
HashMap<K,V> | Map<K,V> | |
HashSet<T> | Set<T> | |
Option<T> | T? | Nullable |
Result<T,E> | Result<T,E> | Kept as-is |
Box<T>, Arc<T>, Rc<T> | T | Wrapper erased |
&T, &mut T | T | Reference erased |
(A, B, C) | (A, B, C) | Tuples preserved |
Refinement Types
x: Int where x > 0 // inline refinement
x: { n: Int | n >= 0 } // set-builder syntax
data: NonEmpty<List<Int>> // named refinement
Declaration Forms
Contract (standalone specification)
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
effects { pure }
}
Bind (attach contract to existing Rust function)
bind "mylib::divide" as divide_checked {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result == a / b }
}
Function
fn helper(x: Int) -> Bool
requires { x >= 0 }
ensures { result == (x mod 2 == 0) }
Service (typestate)
service Connection {
states { Disconnected, Connected, Authenticated }
transitions {
connect: Disconnected -> Connected
login: Connected -> Authenticated
close: * -> Disconnected
}
}
Other declarations
type Percentage = { n: Float | n >= 0.0 && n <= 100.0 }
enum Status { Active, Inactive, Suspended }
extern fn system_time() -> Nat effects { time }
block incremental { ... }
Contract Clauses
| Clause | Purpose | Example |
|---|---|---|
requires | Precondition | requires { x > 0 } |
ensures | Postcondition | ensures { result >= 0 } |
invariant | Loop/type invariant | invariant { len >= 0 } |
decreases | Termination measure | decreases { n } |
effects | Side effect declaration | effects { io, database } |
where | Type constraint | where T: Comparable |
modifies | Frame condition | modifies { buffer, count } |
data_flow | Taint/info-flow rule | data_flow { input must_not_reach output } |
Expression Builtins
| Expression | Meaning |
|---|---|
old(x) | Value of x before function call |
result | Return value (in ensures) |
forall x: T, P(x) | Universal quantifier |
exists x: T, P(x) | Existential quantifier |
abs(x) | Absolute value |
length(xs) | Collection length |
consumed(r) | Linear resource consumed |
Operator Precedence (low to high)
| Level | Operators |
|---|---|
| 1 | || (logical or) |
| 3 | && (logical and) |
| 5 | ==, != (equality) |
| 7 | <, >, <=, >= (comparison) |
| 9 | +, - (additive) |
| 11 | *, /, %, mod (multiplicative) |
| – | !, - (unary prefix) |
| – | ., (), [] (postfix) |
Effect Names
Group effects (expand to sub-effects)
| Group | Sub-effects |
|---|---|
io | console.read, console.write, filesystem.read, filesystem.write, network.connect, network.send, network.receive, time.read, random |
database | database.read, database.write |
logging | log.debug, log.info, log.warn, log.error |
Short aliases
mem, net, fs, rng, time, alloc, diverge, random
Pure functions
effects { pure } // no side effects allowed
Custom sub-effects
Any group.sub where group is known is accepted: io.custom, database.migrate.
Error Code Ranges
| Range | Category | Example |
|---|---|---|
| A01xxx | Syntax | A01001 unexpected token |
| A02xxx | Name resolution | A02001 undefined symbol |
| A03xxx | Types | A03001 type mismatch |
| A05xxx | Linearity | A05001 double use |
| A06xxx | Typestate | A06001 invalid transition |
| A07xxx | Effects | A07001 undeclared effect |
| A08xxx | Info-flow | A08001 taint leak |
Look up any code: assura explain A03001
CLI Commands
assura check file.assura # verify (parse + resolve + types + SMT)
assura check file.assura --watch # re-verify on save
assura build file.assura # verify + generate Rust code
assura init my-project # scaffold new project
assura fmt file.assura # format source
assura fmt file.assura --check # check formatting (CI)
assura explain A03001 # explain error code
assura infer src/lib.rs # generate bind skeletons from Rust
assura test-gen file.assura # generate proptest code
assura audit . # scan Rust project for violations
assura coverage . # show contract coverage
assura doctor # check dependencies
assura agent-instructions # AI agent quick reference
assura completions zsh # generate shell completions
Common Patterns
// Safe division
requires { divisor != 0 }
// Bounds check
requires { index >= 0 && index < length(data) }
// Non-empty input
requires { length(items) > 0 }
// Monotonicity
ensures { result >= old(counter) }
// Size preservation
ensures { length(result) == length(input) }
Assura Contract Cookbook
Ready-to-copy contract patterns organized by category. Each pattern is self-contained. For syntax basics, see the tutorial.
Arithmetic Safety
Safe Division
Prevents: division by zero, incorrect quotient
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
ensures { abs(result) <= abs(a) }
effects { pure }
}
Integer Overflow Guard
Prevents: silent integer overflow on addition
contract SafeAdd {
input(a: Int, b: Int, max: Int)
output(result: Int)
requires { a >= 0 }
requires { b >= 0 }
requires { a + b <= max }
ensures { result == a + b }
ensures { result >= 0 }
effects { pure }
}
Percentage Bounds
Prevents: percentage values outside 0..100
type Percentage = { v: Float | v >= 0.0 && v <= 100.0 };
contract ApplyDiscount {
input(price: Float, discount: Percentage)
output(result: Float)
requires { price >= 0.0 }
ensures { result >= 0.0 }
ensures { result <= price }
effects { pure }
}
Bounds Checking
Safe Array Index
Prevents: out-of-bounds array access
contract SafeIndex {
input(arr: List<Int>, index: Nat)
output(result: Int)
requires { index < arr.length() }
effects { pure }
}
Bounded Slice
Prevents: slice overrun past buffer end
contract SafeSlice {
input(buf: Bytes, offset: Nat, len: Nat)
output(data: Bytes)
requires { offset + len <= buf.length() }
ensures { data.length() == len }
effects { pure }
}
Buffer Capacity
Prevents: writes past allocated buffer capacity
contract BoundedBuffer {
input(capacity: Nat, count: Nat, item_size: Nat)
requires { capacity > 0 }
requires { count * item_size <= capacity }
invariant { count * item_size <= capacity }
effects { pure }
}
String and Bytes
Non-Empty String
Prevents: empty string passed where content is required
contract NonEmptyInput {
input(name: String)
requires { name.length() > 0 }
effects { pure }
}
Bounded String Length
Prevents: oversized strings causing truncation or overflow
contract BoundedString {
input(value: String, max_len: Nat)
requires { value.length() > 0 }
requires { value.length() <= max_len }
requires { max_len <= 65535 }
effects { pure }
}
Option/Result Safety
Safe Unwrap via Precondition
Prevents: unwrap on None/Err
contract SafeUnwrap {
input(value: Int?, default_val: Int)
output(result: Int)
ensures { result == if value != null then value else default_val }
effects { pure }
}
Error Propagation
Prevents: unhandled error cases
fn parse_port(s: String) -> Int?
requires { s.length() > 0 }
ensures { result != null implies result >= 1 }
ensures { result != null implies result <= 65535 }
effects { pure }
Collection Properties
Non-Empty Collection
Prevents: operations on empty collections (head, reduce, min, max)
contract NonEmptyList {
input(items: List<Int>)
output(result: Int)
requires { items.length() > 0 }
effects { pure }
}
Sorted Output
Prevents: sort functions that return unsorted data
contract SortContract {
input(arr: List<Int>)
output(result: List<Int>)
requires { arr.length() > 0 }
ensures { result.length() == arr.length() }
ensures { forall i in 0..result.length() - 1: result[i] <= result[i + 1] }
effects { pure }
}
Element Uniqueness
Prevents: duplicate entries in collections that require distinct elements
contract UniqueElements {
input(items: List<Int>, new_item: Int)
output(result: List<Int>)
requires { forall i in items: i != new_item }
ensures { result.length() == items.length() + 1 }
effects { pure }
}
Monotonicity and Ordering
Monotonic Counter
Prevents: counter decrement, stale sequence numbers
contract IncrementCounter {
input(current: Nat, amount: Nat)
output(result: Nat)
requires { amount > 0 }
ensures { result > current }
ensures { result == current + amount }
effects { pure }
}
Timestamp Ordering
Prevents: out-of-order event timestamps
contract AppendEvent {
input(last_ts: Nat, new_ts: Nat)
requires { new_ts > last_ts }
ensures { new_ts > last_ts }
effects { pure }
}
Resource Lifecycle
Connection Open/Close
Prevents: use-after-close, double-close, resource leaks
service Connection {
states: Closed -> Open -> Closed
operation Open {
input(host: String)
requires { host.length() > 0 }
requires { self.state == Closed }
ensures { self.state == Open }
effects { net }
}
operation Send {
input(data: Bytes)
requires { self.state == Open }
requires { data.length() > 0 }
ensures { self.state == Open }
effects { net }
}
operation Close {
requires { self.state == Open }
ensures { self.state == Closed }
effects { net }
}
}
Acquire/Release Lock
Prevents: double-acquire, use without lock, forgotten release
service Mutex {
states: Unlocked -> Locked -> Unlocked
operation Acquire {
requires { self.state == Unlocked }
ensures { self.state == Locked }
effects { mem }
}
operation Release {
requires { self.state == Locked }
ensures { self.state == Unlocked }
effects { mem }
}
}
Effects and Purity
Pure Computation
Prevents: accidental side effects in business logic
contract PureTransform {
input(items: List<Int>)
output(result: List<Int>)
requires { items.length() > 0 }
ensures { result.length() == items.length() }
effects { pure }
}
IO Isolation
Prevents: database access from code that should only do network IO
fn fetch_remote(url: String) -> Bytes
requires { url.length() > 0 }
effects { net }
fn save_to_db(data: Bytes) -> Bool
requires { data.length() > 0 }
effects { database.write }
fn api_handler(url: String) -> Bool
requires { url.length() > 0 }
effects { net, database.write }
Taint Tracking
Untrusted Input Validation
Prevents: unsanitized user input reaching sensitive operations
fn read_user_input() -> String @taint:untrusted
effects { io }
fn validate_input(
raw: String @taint:untrusted,
max_len: Nat
) -> String @taint:validated
requires { max_len > 0 }
effects { pure }
{
validate {
raw.length() > 0 && raw.length() <= max_len
} raw
or ""
}
fn execute_query(query: String @taint:validated) -> Int
effects { database.read }
Quantifiers
All Elements Positive
Prevents: negative values slipping into a non-negative collection
contract AllPositive {
input(items: List<Int>)
requires { items.length() > 0 }
ensures { forall i in items: i >= 0 }
effects { pure }
}
Element Exists
Prevents: search returning not-found when element is guaranteed present
contract FindElement {
input(arr: List<Int>, n: Nat, target: Int)
output(result: Int)
requires { n > 0 }
requires { exists i in 0..n: arr[i] == target }
ensures { result == target }
effects { pure }
}
Bind Declarations
Retrofit Existing Rust Function
Prevents: calling an existing Rust function without contract enforcement
bind "my_crate::math::divide" as safe_divide {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b == a }
}
Bind with Effects
Prevents: calling an FFI function without declaring its side effects
bind "libc::malloc" as safe_malloc {
input(size: Nat)
output(result: Bytes)
requires { size > 0 }
ensures { result.length() == size }
effects { mem }
}
Key Derivation and Crypto
Secure Key Length
Prevents: weak cryptographic keys
contract SecureKeyDerivation {
input(password_len: Nat, salt_len: Nat, iterations: Nat)
requires { password_len >= 8 }
requires { salt_len >= 16 }
requires { iterations >= 100000 }
effects { pure }
}
Case studies
Short public write-ups of demos you can run today. Full sources live under
demos/. Showcase (must-pass) vs EXPECT FAIL taxonomy:
demos/README.md.
Heartbleed-class bounds (CVE-2014-0160)
Story: OpenSSL Heartbleed read attacker-controlled payload_length
without ensuring the record actually contained that many bytes.
Demo: demos/heartbleed.assura
Run:
assura check demos/heartbleed.assura
What the contract encodes: preconditions on record layout and padding so response size cannot exceed the buffer (bounds safety as SMT-checkable ensures on inputs).
What is proved: when the contract is in the supported fragment and check reports Verified for those clauses, the modeled over-read is impossible under the stated requires. See What we prove.
libwebp Huffman / heap overflow class (CVE-2023-4863)
Story: a CVSS 9.8 heap buffer overflow in libwebp (WebP) affected browsers and Electron apps.
Demo: demos/libwebp-huffman.assura
Run:
assura check demos/libwebp-huffman.assura
# optional:
assura check demos/libwebp-huffman.assura --verbose --stats
What the contract encodes: memory region, taint, table, and related feature surface aimed at the class of bug (see file header comments).
What is proved: structural + SMT obligations that fire for the features actually modeled on that file. Treat Unknown limitation markers as incomplete encoding, not green proof.
Showcase identity (happy path)
Story: smallest result-bearing path for onboarding.
Demo: demos/showcase-echo.assura
(or the copy-paste file in GETTING-STARTED.md)
Run:
assura check demos/showcase-echo.assura
assura check synthesizes analyzable IR for simple result == x shapes
so you can see Verified without hand-written IR.
Next
- More patterns: Cookbook
- AI implement loop: GETTING-STARTED.md, design note DESIGN-AI-VERIFICATION-LOOP.md
Assura Scenario Walkthroughs
Five practical scenarios showing how to use Assura in real workflows. Each includes exact commands, example code, and expected output.
Table of Contents
- Greenfield Development with AI + Assura
- Retrofitting Contracts onto Existing Rust Code
- Security Audit with
assura audit - CI Integration (GitHub Actions)
- Team Onboarding and Progressive Adoption
Scenario 1: Greenfield Development with AI + Assura
You are starting a new project from scratch. You want an AI coding agent to write both the contracts and the Rust implementation, with Assura verifying correctness at every step.
1.1 Project setup
assura init safe-parser
cd safe-parser
This creates:
safe-parser/
assura.toml
contracts/
lib.assura
The generated assura.toml:
[package]
name = "safe-parser"
version = "0.1.0"
[build]
target = "native" # "native" or "wasm32-wasi"
output = "generated"
[verify]
smt-solver = "z3" # "z3", "cvc5", or "portfolio"
layer = 1 # 0 = structural only, 1 = SMT
timeout = 1000 # SMT timeout in ms
[profile]
type = "minimal" # minimal, parser, database, etc.
1.2 Instructing the AI agent
Add this to your project’s AGENTS.md (or system prompt) so the AI
understands the contract-first workflow:
## Development Workflow
1. Write an Assura contract FIRST that specifies the function's behavior.
2. Run `assura check contracts/lib.assura` to verify the contract is
well-formed and the clauses are satisfiable.
3. Write the Rust implementation that satisfies the contract.
4. Run `assura build contracts/lib.assura` to generate verified Rust
stubs with debug_assert! guards from the contract.
5. If `assura check` reports a counterexample, fix the contract or
implementation before proceeding.
## Assura Type Mapping
Use these types in contracts (not Rust types):
- i32/i64/isize -> Int
- u32/u64/usize -> Nat
- f64 -> Float
- bool -> Bool
- String/&str -> String
- Vec<u8> -> Bytes
- Vec<T> -> List<T>
- Option<T> -> T?
## Contract Structure
contract Name {
input(param: Type, ...)
output(result: ReturnType)
requires { precondition }
ensures { postcondition }
effects { pure | io | fs | net | ... }
}
1.3 Worked example: safe string parser with bounded output
Instead of the starter SafeDivision contract, write a contract for a string parser that extracts a bounded substring.
Edit contracts/lib.assura:
// Safe bounded substring extraction.
// Guarantees: no out-of-bounds access, output length matches request.
contract BoundedSubstring {
input(s: String, start: Nat, len: Nat)
output(result: String)
requires { start + len <= s.length() }
requires { len > 0 }
ensures { result.length() == len }
effects { pure }
}
// Token parser: splits input on a delimiter, returns bounded results.
// Guarantees: token count is at most max_tokens, no token exceeds max_len.
contract TokenParser {
input(input: String, delimiter: String, max_tokens: Nat, max_len: Nat)
output(result: List<String>)
requires { delimiter.length() > 0 }
requires { max_tokens > 0 }
requires { max_len > 0 }
ensures { result.length() <= max_tokens }
ensures { forall t in result: t.length() <= max_len }
effects { pure }
}
1.4 Check the contract
assura check contracts/lib.assura --verbose
Expected output (exact text depends on contract complexity):
Pipeline timing for contracts/lib.assura:
lex: 85 tokens (0.12ms)
parse: 2 declaration(s), 0 import(s) (0.08ms)
resolve: 4 symbol(s) (0.03ms)
typecheck: 8 binding(s) (0.15ms)
Verification (4 clause(s)):
BoundedSubstring:
requires ✓ verified
ensures ✓ verified
TokenParser:
requires ✓ verified
ensures ✓ verified
contracts/lib.assura: check passed (no errors)
1.5 Iterating on counterexamples
Suppose you write a buggy contract:
contract BadDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b >= 0 }
ensures { result * b == a }
effects { pure }
}
The requires clause allows b == 0, which makes the ensures clause
unsatisfiable for a != 0. Running:
assura check contracts/lib.assura
Produces:
Error[A05100]: verification failed for BadDivision: ensures:
counterexample: a = 1, b = 0
contracts/lib.assura: 1 error(s) found
The counterexample shows concrete values that violate the contract.
Fix it by changing b >= 0 to b > 0 (or b != 0 to also allow
negative divisors).
1.6 Live feedback with watch mode
Run the checker in watch mode while editing:
assura check contracts/lib.assura --watch
Output on startup:
[watch] Checking contracts/lib.assura...
contracts/lib.assura: check passed (no errors)
[watch] Watching contracts/lib.assura for changes. Press Ctrl+C to stop.
Every time you save the file, the checker re-runs automatically. If the content has not changed (editor auto-save, formatter), it skips the re-check to avoid redundant work.
On change:
[watch] File changed, re-checking contracts/lib.assura...
Verification (4 clause(s)):
BoundedSubstring:
requires ✓ verified
ensures ✓ verified
...
contracts/lib.assura: check passed (no errors)
[watch] Watching for changes. Press Ctrl+C to stop.
1.7 Build to Rust
Once contracts are verified, generate the Rust project:
assura build contracts/lib.assura --output generated
Expected output:
wrote generated/Cargo.toml
wrote generated/src/lib.rs
OK contracts/lib.assura -> generated/ (generated Rust compiles)
The generated generated/src/lib.rs contains Rust functions with
debug_assert! guards derived from your contract clauses. You can now
add your implementation inside these function bodies.
To target WASM instead of native:
assura build contracts/lib.assura --target wasm --output generated
This additionally writes generated/.cargo/config.toml with
target = "wasm32-wasip1" and runs cargo build --target wasm32-wasip1.
1.8 Verification statistics
Use --stats to see detailed timing and clause counts:
assura check contracts/lib.assura --stats
=== Verification Statistics ===
Clauses: 4
Verified: 4
Counterexamples: 0
Timeouts: 0
Unknown: 0
Lex time: 0.12ms (85 tokens)
Parse time: 0.08ms
Resolve time: 0.03ms
Type-check time: 0.15ms
Verify time: 12.45ms
Total time: 12.83ms
Scenario 2: Retrofitting Contracts onto Existing Rust Code
You have an existing Rust codebase and want to add Assura contracts incrementally, without rewriting anything.
2.1 Bootstrap contract skeletons with assura infer
Suppose you have a file src/parser.rs:
#![allow(unused)]
fn main() {
pub fn parse_header(data: &[u8], offset: usize) -> Option<Header> {
if offset + HEADER_SIZE > data.len() {
return None;
}
let magic = u32::from_le_bytes(data[offset..offset+4].try_into().unwrap());
let version = data[offset + 4];
let length = u16::from_le_bytes(data[offset+5..offset+7].try_into().unwrap());
Some(Header { magic, version, length: length as usize })
}
pub fn validate_checksum(data: &[u8], expected: u32) -> bool {
let mut sum: u32 = 0;
for byte in data {
sum = sum.wrapping_add(*byte as u32);
}
sum == expected
}
pub fn extract_payload(data: &[u8], offset: usize, length: usize) -> Vec<u8> {
data[offset..offset+length].to_vec()
}
}
Generate skeleton contracts:
assura infer src/parser.rs
Output (printed to stdout):
// Generated by: assura infer src/parser.rs
// Review and refine these contracts before use.
bind "crate::parser::parse_header" as parse_header {
input(data: Bytes, offset: Nat)
output(result: Header?)
// TODO: add requires clauses (preconditions)
// TODO: add ensures clauses (postconditions)
}
bind "crate::parser::validate_checksum" as validate_checksum {
input(data: Bytes, expected: Nat)
output(result: Bool)
// TODO: add requires clauses (preconditions)
// TODO: add ensures clauses (postconditions)
}
bind "crate::parser::extract_payload" as extract_payload {
input(data: Bytes, offset: Nat, length: Nat)
output(result: Bytes)
// TODO: add requires clauses (preconditions)
// TODO: add ensures clauses (postconditions)
}
// 3 function(s) analyzed from src/parser.rs
Write to a file instead:
assura infer src/parser.rs --output contracts/parser.assura
Filter to a single function:
assura infer src/parser.rs --function parse_header
2.2 Refining skeleton contracts
The skeletons have the right types but no invariants. Add real contracts:
bind "crate::parser::parse_header" as parse_header {
input(data: Bytes, offset: Nat)
output(result: Header?)
requires { offset + 7 <= data.length() }
ensures {
result.is_some() implies
result.unwrap().length <= data.length() - offset
}
}
bind "crate::parser::extract_payload" as extract_payload {
input(data: Bytes, offset: Nat, length: Nat)
output(result: Bytes)
requires { offset + length <= data.length() }
ensures { result.length() == length }
}
2.3 The bind declaration in detail
bind attaches a contract to an existing Rust function without modifying
the Rust source. The syntax:
bind "rust::module::path::function_name" as local_name {
input(param1: AssuraType, param2: AssuraType)
output(result: AssuraReturnType)
requires { precondition_expression }
ensures { postcondition_expression }
effects { effect_list }
}
- The quoted string is the full Rust path to the function.
as local_namegives the contract a name for Assura’s symbol table.inputlists parameters with Assura types (not Rust types).outputdeclares the return type. Useresultto refer to it inensuresclauses.requiresstates what must be true before the function is called.ensuresstates what must be true after the function returns.effectsdeclares side effects:pure,io,fs,net,mem,database,logging,rng,time,alloc.
2.4 Finding a real bug via counterexample
The extract_payload function above has a latent bug: if offset + length
overflows usize, the slice operation panics. Without the contract’s
requires clause, callers could pass values that trigger this.
Run verification:
assura check contracts/parser.assura
If the contract’s requires clause is missing:
Error[A05100]: verification failed for extract_payload: ensures:
counterexample: data = [], offset = 1, length = 0
contracts/parser.assura: 1 error(s) found
The counterexample reveals that with an empty data array and a nonzero
offset, the function would access out-of-bounds memory. The contract
forces callers to prove offset + length <= data.length() before calling.
2.5 Incremental adoption strategy
You do not need to contract every function at once. A practical progression:
- Start with
assura inferon your most critical modules (parsers, crypto, network handlers). - Refine the generated skeletons with real preconditions and postconditions.
- Run
assura checkon those contracts. Fix any counterexamples. - Add contracts to more modules over time.
- Wire
assura checkinto CI (see Scenario 4) so regressions are caught.
Focus on functions that:
- Parse untrusted input
- Perform arithmetic on sizes or offsets
- Handle cryptographic material
- Manage state transitions
2.6 AI prompt for retrofitting
Give an AI coding agent this prompt to automate the refinement step:
I have Assura skeleton contracts generated by `assura infer`. For each
bind declaration, analyze the Rust source function and:
1. Add requires clauses for every precondition the function assumes
(non-null, in-bounds, non-zero divisor, valid state).
2. Add ensures clauses for every postcondition the function guarantees
(output length, return value bounds, state changes).
3. Add effects declarations (pure, io, fs, net, mem, etc.).
4. Do NOT write vacuous contracts (always-true conditions).
5. Use Assura types, not Rust types: Int (not i64), Nat (not usize),
Bytes (not &[u8]), String (not &str), List<T> (not Vec<T>).
6. After writing each contract, run `assura check <file>` and fix
any counterexamples before moving to the next function.
Scenario 3: Security Audit with assura audit
You want to scan an existing Rust project for potential vulnerabilities without writing any contracts manually.
3.1 Running a basic audit
From the root of a Cargo workspace:
assura audit .
Output:
Scanning . ... found 47 public functions in 12 files
Generating contracts ... 47 skeleton contracts
Verifying ...
AUDIT SUMMARY: 47 functions, 38 verified, 3 findings, 0 errors
FINDINGS:
[WARNING] parse_record: ensures (counterexample)
Counterexample found
| offset = 4294967295
| length = 1
[WARNING] resize_buffer: ensures (counterexample)
Counterexample found
| new_size = 0
[INFO] decrypt_block: ensures (timeout)
Z3 timed out (needs deeper contract or longer timeout)
The audit generates skeleton contracts with heuristic preconditions (bounds checks on index-like parameters, non-empty checks on collections), runs the full pipeline (parse, resolve, type check, SMT verify), and reports findings.
3.2 Targeting risky code
Scan only functions matching a pattern:
assura audit . --focus "parser::*"
Scan only unsafe functions:
assura audit . --unsafe-only
Limit the number of functions analyzed:
assura audit . --max-functions 20
Use shallow depth (types only, no heuristic contracts):
assura audit . --depth shallow
3.3 Understanding audit findings
Each finding has a severity and a clause type:
| Severity | Clause | Meaning |
|---|---|---|
| WARNING | counterexample | Z3 found concrete inputs that violate the inferred contract. Potential bug. |
| INFO | timeout | Z3 could not prove or disprove within the time limit. Needs manual review. |
| INFO | unknown | Z3 returned “unknown” (usually nonlinear arithmetic). Needs manual review. |
A counterexample finding means the SMT solver found specific input values where the heuristic contract is violated. This does not always mean there is a bug; the heuristic contract may be too strict. But it always deserves investigation.
3.4 Using CVE-pattern templates
For deeper analysis, use the CVE-pattern templates to generate contracts
targeting specific vulnerability classes. The templates are in
templates/cve-patterns.md:
- Buffer overflow (CWE-120, CWE-787): bounds checks on index operations
- Integer overflow (CWE-190, CWE-191): arithmetic wrapping prevention
- Use-after-free (CWE-416): linear resource consumption
- Injection (CWE-89, CWE-79): taint tracking on untrusted input
- Null dereference (CWE-476): Option validation
Feed the template and your Rust function signatures to an AI agent to generate targeted contracts.
3.5 Triage workflow
- Run
assura audit . --format json > audit.jsonfor a machine-readable report. - Sort findings by severity: counterexamples first, then timeouts.
- For each counterexample, check whether the concrete values are reachable in practice. If yes, it is a real bug. If no, the heuristic contract needs refinement.
- For timeouts, decide whether to increase
--timeout, write a more specific manual contract, or accept the risk. - Track findings in your issue tracker. Re-run the audit after fixes to confirm resolution.
3.6 JSON output for tooling integration
assura audit . --format json
Output structure:
{
"functions_scanned": 47,
"files_scanned": 12,
"verified": 38,
"findings": 3,
"errors": 0,
"results": [
{
"function": "parse_record: ensures",
"clause": "counterexample",
"severity": "warning",
"message": "Counterexample found",
"counterexample": "offset = 4294967295\nlength = 1"
},
{
"function": "resize_buffer: ensures",
"clause": "counterexample",
"severity": "warning",
"message": "Counterexample found",
"counterexample": "new_size = 0"
},
{
"function": "decrypt_block: ensures",
"clause": "timeout",
"severity": "info",
"message": "Z3 timed out (needs deeper contract or longer timeout)",
"counterexample": null
}
]
}
Use jq to extract actionable items:
assura audit . --format json | jq '.results[] | select(.severity == "warning")'
Scenario 4: CI Integration (GitHub Actions)
You want every PR to be verified by Assura before merging.
4.1 Complete workflow file
Create .github/workflows/assura.yml:
name: Assura Verification
on:
pull_request:
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Z3
run: sudo apt-get install -y libz3-dev
- name: Cache Assura build
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/assura
~/.cargo/registry
~/.cargo/git
key: assura-${{ hashFiles('assura-version') }}
- name: Install Assura
run: |
if ! command -v assura &> /dev/null; then
cargo install assura --locked
fi
- name: Format check
run: |
for f in contracts/*.assura; do
assura fmt "$f" --check || {
echo "::error file=$f::File is not formatted. Run: assura fmt $f"
exit 1
}
done
- name: Verify contracts (Layer 1, SMT)
run: |
for f in contracts/*.assura; do
echo "Checking $f..."
assura check "$f" --layer 1 --json > "/tmp/$(basename $f).json" 2>&1
if [ $? -ne 0 ]; then
echo "::error file=$f::Contract verification failed"
cat "/tmp/$(basename $f).json"
exit 1
fi
done
- name: Build generated Rust
run: |
for f in contracts/*.assura; do
assura build "$f" --output generated --no-check
done
cd generated && cargo check
- name: Upload verification results
if: always()
uses: actions/upload-artifact@v4
with:
name: assura-results
path: /tmp/*.json
4.2 Installing Z3 in CI
Z3 is required for Layer 1 (SMT) verification. On Ubuntu runners:
- name: Install Z3
run: sudo apt-get install -y libz3-dev
On macOS runners:
- name: Install Z3
run: brew install z3
To pin a specific Z3 version (recommended for reproducibility):
- name: Install Z3 4.13.0
run: |
wget https://github.com/Z3Prover/z3/releases/download/z3-4.13.0/z3-4.13.0-x64-glibc-2.35.zip
unzip z3-4.13.0-x64-glibc-2.35.zip
sudo cp z3-4.13.0-x64-glibc-2.35/bin/libz3.so /usr/lib/
sudo cp z3-4.13.0-x64-glibc-2.35/include/*.h /usr/include/
4.3 Using JSON output for annotations
Parse JSON results to create GitHub annotations:
- name: Verify contracts with annotations
run: |
EXIT=0
for f in contracts/*.assura; do
OUTPUT=$(assura check "$f" --json 2>&1)
RC=$?
if [ $RC -ne 0 ]; then
EXIT=1
# Extract error messages from JSON diagnostics
echo "$OUTPUT" | jq -r '
.diagnostics[]
| select(.severity == "error")
| "::error file=\(.file // "unknown"),line=1::\(.code): \(.message)"
'
fi
done
exit $EXIT
4.4 Failing PRs on contract violations
The assura check command exits with code 1 when verification fails.
This means any step using assura check will fail the CI job
automatically. No special configuration is needed.
Exit codes:
0: all checks passed1: verification errors (counterexamples, type errors, parse errors)2: file not found, invalid arguments, or other fatal errors
4.5 Verification stats tracking
Capture stats over time to track verification coverage:
- name: Collect verification stats
run: |
for f in contracts/*.assura; do
assura check "$f" --stats 2>&1 | tail -n 12
done > stats.txt
- name: Upload stats
uses: actions/upload-artifact@v4
with:
name: verification-stats
path: stats.txt
4.6 Audit as a separate CI job
Run assura audit on PRs that modify Rust source (not just contracts):
audit:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.changed_files, '.rs')
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install Z3
run: sudo apt-get install -y libz3-dev
- name: Install Assura
run: cargo install assura --locked
- name: Run audit
run: |
assura audit . --format json --max-functions 100 > audit.json
FINDINGS=$(jq '.findings' audit.json)
if [ "$FINDINGS" -gt 0 ]; then
echo "::warning::Assura audit found $FINDINGS potential issues"
jq '.results[] | select(.severity == "warning")' audit.json
fi
Scenario 5: Team Onboarding and Progressive Adoption
You are introducing Assura to a team that has never used formal verification. This scenario covers the learning progression, common mistakes, and conventions.
5.1 Learning progression
Week 1: Simple contracts. Start with pure functions that have obvious preconditions:
// Level 1: basic precondition
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
effects { pure }
}
Run assura check and assura explain A05100 to understand
counterexamples.
Week 2: Refinement types and multiple clauses. Introduce refined types and combine multiple ensures clauses:
type PositiveInt = { n: Int | n > 0 };
contract Clamp {
input(value: Int, low: Int, high: Int)
output(result: Int)
requires { low <= high }
ensures { result >= low }
ensures { result <= high }
ensures { (value >= low and value <= high) implies result == value }
effects { pure }
}
Week 3: Effect tracking. Add effects to contracts for functions with side effects:
contract ReadConfig {
input(path: String)
output(result: Map<String, String>)
requires { path.length() > 0 }
ensures { result.length() >= 0 }
effects { fs }
}
Week 4: Bind declarations. Start attaching contracts to existing
Rust code using bind:
bind "crate::db::get_user" as get_user {
input(id: Nat)
output(result: User?)
requires { id > 0 }
ensures { result.is_some() implies result.unwrap().id == id }
effects { database.read }
}
Week 5+: Advanced features. Explore quantifiers (forall, exists),
typestate, taint tracking, and memory regions. Use the demo files in
demos/ as reference implementations.
5.2 “Tests pass but contract finds bug” example
This is the moment that sells Assura to skeptics. Consider this function and its tests:
#![allow(unused)]
fn main() {
// src/math.rs
pub fn percentage(part: u64, total: u64) -> u64 {
(part * 100) / total
}
// tests/math_test.rs
#[test]
fn test_percentage() {
assert_eq!(percentage(50, 100), 50);
assert_eq!(percentage(1, 4), 25);
assert_eq!(percentage(0, 100), 0);
}
}
All tests pass. But the contract:
bind "crate::math::percentage" as percentage {
input(part: Nat, total: Nat)
output(result: Nat)
requires { total > 0 }
ensures { result <= 100 }
}
Running assura check:
Error[A05100]: verification failed for percentage: ensures:
counterexample: part = 101, total = 100
contracts/math.assura: 1 error(s) found
The counterexample reveals: when part > total, the result exceeds 100.
The tests never tested this case. The contract caught a real logic gap
that 100% test coverage on happy paths would miss.
Fix options:
- Add
requires { part <= total }to forbid invalid inputs. - Change the implementation to
std::cmp::min((part * 100) / total, 100). - Add
requires { part <= total }ANDensures { result <= 100 }so both the caller and the implementation are constrained.
5.3 When to write contracts vs tests
| Use contracts when | Use tests when |
|---|---|
| The function has preconditions callers must satisfy | You need to test integration between multiple components |
| You want to prove a property holds for ALL inputs | You need to verify specific edge cases with exact values |
| The function handles untrusted or user-supplied input | You need to test performance or timing |
| The function performs arithmetic that might overflow | You need to test external service interactions |
| You want to enforce an API contract across a team | You need snapshot/golden-file testing |
Contracts and tests are complementary. Contracts prove universal properties (“for all valid inputs, the output is bounded”). Tests verify specific scenarios (“given this exact input, the output is this exact value”). Use both.
5.4 Common mistakes and how to fix them
Mistake 1: Vacuous contracts. A contract that is always true catches nothing:
# BAD: always true, catches nothing
contract Useless {
input(x: Int)
ensures { x == x }
}
Fix: write contracts that constrain behavior. If the ensures clause is a tautology, delete it and write one that describes actual guarantees.
Mistake 2: Wrong Assura types. Using Rust types in contracts:
# BAD: Rust types in contract
contract Wrong {
input(x: i64, y: usize)
output(result: Vec<u8>)
}
Fix: use Assura types. i64 is Int, usize is Nat, Vec<u8> is
Bytes. See the type mapping table in Scenario 2.
Mistake 3: Missing effect declarations. Forgetting to declare side effects:
# BAD: reads from filesystem but declares pure
contract ReadFile {
input(path: String)
output(result: Bytes)
effects { pure }
}
The type checker rejects this with error A07003 if the bound function
performs I/O. Fix: use effects { fs } or effects { io }.
Mistake 4: Overly strict ensures. Writing postconditions that are true for the implementation but impossible to verify:
# BAD: Z3 cannot reason about string formatting
contract FormatDate {
input(year: Nat, month: Nat, day: Nat)
output(result: String)
ensures { result == year.to_string() + "-" + month.to_string() + "-" + day.to_string() }
}
Z3 struggles with string concatenation and formatting. Fix: write verifiable properties instead:
contract FormatDate {
input(year: Nat, month: Nat, day: Nat)
output(result: String)
requires { month >= 1 and month <= 12 }
requires { day >= 1 and day <= 31 }
ensures { result.length() >= 8 }
ensures { result.length() <= 10 }
}
Mistake 5: Not running assura explain. When you get an error code,
look it up:
assura explain A05100
Output:
A05100: Verification counterexample
The SMT solver found a concrete assignment of values to the contract's
input variables that satisfies the requires clauses but violates at
least one ensures clause.
Example:
contract Foo {
input(x: Int)
ensures { x > 0 } // <-- no requires constraining x
}
// counterexample: x = -1
How to fix:
Add a requires clause that rules out the counterexample values,
or weaken the ensures clause if the property is too strong.
5.5 Team conventions
Establish these conventions early:
-
One contract file per Rust module. If your Rust source is
src/parser.rs, the contract goes incontracts/parser.assura. -
Contract names match function names. Use
bind "crate::foo::bar" as bar, notas bar_contractoras BarContract. -
Run
assura fmtbefore committing. Likerustfmtfor Rust,assura fmtnormalizes whitespace and indentation. Useassura fmt --checkin CI to enforce formatting. -
Start with
requires, thenensures, theneffects. Consistent clause ordering makes contracts scannable:bind "..." as name { input(...) output(...) requires { ... } ensures { ... } effects { ... } } -
Commit contracts alongside code changes. If a PR changes a function’s behavior, the contract must be updated in the same PR.
-
Use
assura check --jsonin code review tools. Pipe JSON output into your review bot or CI annotations so reviewers see verification results inline. -
Do not ignore counterexamples. Every counterexample is either a real bug or a contract that needs refinement. Both are valuable. Never suppress a counterexample without understanding it.
Assura for AI agents
Assura is built so agents can propose implementations and the compiler can accept or reject them with structured evidence.
Install
cargo install assura --locked
assura --help
Docs site: https://assura-lang.github.io/assura/
Core loop
- Human (or agent) writes or edits a
.assuracontract. - Agent produces an implementation path (synthesized IR, co-located
.ir, or Rust undercheck-rust). - Agent runs
assura check/assura check-rust/assura build. - Agent reads machine-readable results and iterates on counterexamples, not on vibes.
assura check path/to/file.assura --json
assura check-rust src/ --json
Acceptance policy (LLM IR / auto-implement)
Do not require every clause status == "verified". Many clauses return
Unknown because a feature is not yet encoded in SMT. For agent IR:
- Reject if any Counterexample appears.
- Accept when there is no counterexample (Unknown is allowed for unmodeled features). Generated Rust may still carry runtime assertions from requires/ensures codegen.
See AGENTS.md pipeline notes for verify_ir and multi-contract source
pitfalls (first contract only unless single-contract source is synthesized).
Vacuous success
JSON success can still be vacuous (no contracts, or no SMT proof
obligations). Inspect file_info.vacuous / vacuous_reason (and human
summaries that say “no SMT proof obligations”). Do not report that as
full coverage.
MCP
The assura-mcp crate exposes tools for agent hosts (rmcp). From a
workspace build:
cargo run -p assura-mcp -- --help
Exact tool names evolve; list tools from your MCP client after connecting.
Prefer assura check --json when MCP is unavailable.
Suggested agent checklist
- Prefer showcase demos over
*-audit.assura(many audits are EXPECT FAIL). - On failure, paste the clause description and model into the next edit.
- Use
--layer 0for fast structural iteration when SMT is noisy. - Read What we prove before claiming “proved secure”.
Related
Compared to other tools
Assura is a contract-first language aimed at AI-assisted development: humans write behavioral contracts; the compiler proves implementations (or returns a counterexample) with SMT (Z3/CVC5) and emits Rust.
This page answers the first question PL and Rust audiences ask: is this just Dafny / Verus / Liquid Haskell / better unit tests?
Snapshot
| Assura | Dafny | Verus | Liquid Haskell | Unit / property tests | |
|---|---|---|---|---|---|
| Primary surface | Dedicated .assura contracts | Dafny language | Annotations on Rust | Liquid types / refinements on Haskell | Tests in host language |
| Implementation author | Often AI (IR / auto-implement / check-rust) | Human (or AI as ordinary code) | Human-written Rust | Human-written Haskell | Human or AI |
| Proof backend | Z3 / CVC5 via Assura pipeline | Boogie / Z3 | VIR / Z3 | Liquid Fixpoint / SMT | None (sampling) |
| Default emit | Rust source (rustc / WASM) | C#, Go, JS, Java, Python, … | Stays Rust | Stays Haskell | N/A |
| AI agent loop | First-class (MCP, check-rust, auto-implement) | Possible but not the product shape | Possible | Possible | Common, no proof |
| What “success” means | No counterexample for modeled clauses; layers 0–2 | Verified method / module | Verified function | Type-checked refinements | Tests green |
When Assura is a better fit
- You want specs separate from host-language syntax so agents and humans share a stable contract surface.
- You care about an AI write → SMT check → fix loop with structured results (counterexample vs unknown vs verified).
- You want Rust as the ship format without requiring the implementation to be authored as verified Rust-in-place first.
When another tool is a better fit
| Need | Prefer |
|---|---|
| Verify existing Rust in place with fine-grained borrow-aware proofs | Verus |
| Mature multi-target verified language with large libraries | Dafny |
| Refinement types inside Haskell | Liquid Haskell |
| Fast feedback without SMT, or non-modeled effects | Property tests / fuzzing (still useful with Assura) |
Honesty constraints
Assura does not claim:
- That every clause is always decided (see What we prove).
- That it replaces human review for product requirements.
- That it is a drop-in Verus substitute for verifying arbitrary Rust crates.
For competitive research notes (internal depth), see INVESTIGATION.md. For a short public pitch, start with the docs site introduction.
What we prove
Assura uses layered checking. Not every green assura check means
“mathematically proved forever for every feature.” This page is the
honest map for security and formal-methods readers.
Result kinds (SMT)
| Result | Meaning | Typical user action |
|---|---|---|
| Verified | Solver showed the obligation holds under the model (e.g. validity of ensures under requires) | None; keep the contract |
| Counterexample | Solver found values that break a clause | Fix the contract or the implementation / IR |
| Timeout | Solver hit the time budget | Simplify; raise timeout; try portfolio (Z3+CVC5) |
| Unknown | Inconclusive for other reasons | Read the reason string |
Known SMT limitation (warning, not hard fail)
When the reason includes the marker not yet encoded in SMT, the CLI
treats the result as a warning (exit 0 by default). That means the
compiler intentionally skipped modeling a feature, not that the clause
was proved.
Other Unknown reasons (non-linear arithmetic, genuine solver unknown) are treated more severely. See FAQ: “Unknown” verification result.
Layers
| Layer | What runs | Time scale | Proves? |
|---|---|---|---|
| 0 Structural | Parse, resolve, type, domain checkers | ms | Types, names, many security structure rules (taint, lock order shape, …) |
| 1 Decidable / SMT | Clause encoding to Z3/CVC5 | sub-second to seconds | Modeled boolean obligations (requires/ensures/invariants in the supported fragment) |
| 2+ Heavy / advanced | Extra passes (liveness-style, BMC-related, …) | seconds+ | Additional obligations when enabled |
Default config values are short (often 1s at the options layer). Layer 1
clause solvers floor at 10 seconds so multi-clause demos are not
starved; set [verify] timeout = 30000 (or higher) when you need more.
See FAQ: Z3 timeout on a contract.
What is not automatically proved
- Features that still return Unknown with the known-limitation marker
- Host code outside Assura contracts / IR / check-rust body modeling
- Absolute absence of all security bugs (only the properties you state and that the solver models)
- Correctness of the SMT solvers themselves or of
rustc
Vacuous success
An empty file or a contract with no SMT proof obligations can still
report check success with no type errors. Human and JSON output call this
out (vacuous / “no SMT proof obligations”). Agents must not treat that
as coverage of ensures/invariants.
AI-generated implementations
For auto-implement / IR injection, acceptance is typically no Counterexample, not “every clause is Verified”. Unknown from unmodeled features is common; runtime assertions from codegen still apply.
Related
- FAQ — timeouts, counterexamples, Unknown
- Compared to other tools
- Preferred URLs
SMT and portfolio design notes
Technical note for readers who care about solver encoding and result policy, not AI marketing. Companion to What we prove and Compiler Internals.
Dual solvers, one policy brain
Assura can run Z3 (default) and CVC5 (portfolio / optional
native). Encoding and clause gates live in shared policy modules under
assura-smt (portfolio_policy, clause_gate_policy,
encode_timeout_policy, …). Backends build solver terms; they should not
redefine merge order or limitation strings.
Portfolio merge priority
When both solvers report a result for the same clause, merge prefers:
- Verified
- Counterexample
- Unknown
- Timeout
On equal priority, Z3 wins (richer models / unsat cores in practice).
See merge_portfolio_results / portfolio_result_priority in
crates/assura-smt/src/policy/portfolio_policy.rs.
Shared timeout floor
Config defaults for verify can be short (historically ~1s at the options layer). Per-clause solving floors at 10_000 ms so multi-clause demos are not starved:
| Backend | Mechanism |
|---|---|
| Z3 | Soft/hard timeout from clause_timeout_ms(requested) |
| CVC5 shell | --tlimit string from clause_timeout_tlimit |
| CVC5 native | solver option tlimit with the same resolved ms |
Raising [verify] timeout (or CLI equivalent) above the floor passes
through. Source: crates/assura-smt/src/policy/encode_timeout_policy.rs
(issues #1383 / #1384).
Unknown is not one thing
| Kind | Marker / reason | CLI default | Interpretation |
|---|---|---|---|
| Known limitation | reason contains not yet encoded in SMT | Warning, exit 0 | Compiler skipped modeling; not proved |
| Other Unknown | non-linear, solver unknown, etc. | Error (exit 1) | Inconclusive; treat as failure for gate scripts |
| Timeout | dedicated result variant | Error | Budget exhausted |
Helpers: VerificationResult::unknown_not_encoded,
KNOWN_SMT_LIMITATION_MARKER, is_known_smt_limitation. Do not invent
alternate marker wording in new code paths.
IR bodies and unconstrained results
Without an implementation body (IR sidecar, synthesis, or check-rust encode), result (and other outputs) may be free variables. Ensures that only constrain unconstrained outputs can produce Counterexample even when the requires look fine. Mitigations:
- Co-located
{ContractName}.ir - In-memory synthesis for common
result == …shapes assura check-rustbody encoding where modeled- Writing ensures as consequences of requires on inputs only (demo style)
Vacuous files (no SMT obligations) can still “pass” type-check; JSON
exposes vacuous / “no SMT proof obligations”. Agents must not treat
that as proof coverage.
AI-generated IR acceptance
For auto-implement / LLM IR injection, acceptance is typically no Counterexample, not “every clause Verified.” Unknown from unmodeled features is common; codegen still emits runtime assertions for requires/ensures where applicable.
What this is not
- A claim that Z3/CVC5 themselves are proved correct
- A claim that every Assura feature is fully encoded
- A Verus-style proof of arbitrary existing Rust
Further reading
- WHAT-WE-PROVE.md — user-facing result kinds and layers
- COMPARE.md — Assura vs Dafny / Verus / tests
- INTERNALS.md — crate and module map
- SPECIFICATION.md — language and verification layers
- Launch / outreach drafts under launch/
Preferred public URLs
Use these canonical URLs when linking to Assura (blog posts, Show HN,
social, agent prompts). Do not use https://assura.dev for this
project.
| What | URL |
|---|---|
| Source | https://github.com/assura-lang/assura |
| Documentation (homepage) | https://assura-lang.github.io/assura/ |
| CLI on crates.io | https://crates.io/crates/assura |
Library embed (assura-pipeline) | https://crates.io/crates/assura-pipeline |
| Releases / installers | https://github.com/assura-lang/assura/releases |
| Demo GIF | https://github.com/assura-lang/assura/blob/main/assets/demo/assura-check.gif |
| Org | https://github.com/assura-lang |
Install paths today: crates.io (cargo install assura --locked) or
cargo-dist shell installer on Releases. No Homebrew formula is
published (installers = ["shell"] only).
Brand notes
- Assura here is the contract-first language under the
assura-langGitHub organization. - assura.dev is a different product (structure validation for developer onboarding). It is not affiliated with this language.
- Social handles for the language should use a distinctive form such as
assuralang/assura-langto avoid collisions with insurance and other brands that use the word “Assura”.
Domain policy
Until a custom domain is acquired and pointed at the docs site, the public homepage is the GitHub Pages mdBook URL above. If a domain is added later, redirect it to the same content and update this page, the repo homepage setting, and the org website field in one pass.
Troubleshooting and FAQ
Installation
Install the Assura CLI
Preferred (crates.io):
cargo install assura --locked
Requires a Rust toolchain (edition 2024 / rustc 1.85+). Also available as prebuilt binaries from GitHub Releases (cargo-dist).
From a monorepo clone or git:
cargo install --path crates/assura-cli --locked
# or:
cargo install --git https://github.com/assura-lang/assura assura --locked
More detail: README Quick Start,
Tutorial installation, and
CRATES-IO.md (what co-publishes, and embedding
assura-pipeline from crates.io).
After install, confirm the binary:
assura --version
assura doctor
Z3 is not found
Symptom: assura check prints “Z3 is required for SMT verification”
or verification results are all Timeout.
Fix:
# macOS
brew install z3
# Ubuntu/Debian
sudo apt-get install -y libz3-dev
# Verify
z3 --version
assura doctor
If Z3 is installed but not found, check that the z3 binary is on
your $PATH. On Linux, you may also need libz3.so in the library
path:
export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
Rust toolchain version
Assura requires Rust edition 2024 (rustc 1.85+). Check with:
rustc --version
rustup update stable
Verification
Solver timeout on a contract
Symptom: Verification result says Timeout instead of Verified
or Counterexample.
Causes and fixes:
-
Complex quantifiers. Quantifiers (
forall,exists) with nested data structures are expensive. Simplify the quantifier body or add trigger annotations. -
Large numeric ranges. Constraints like
forall x: Int, 0 <= x && x < 1000000enumerate a huge space. Use tighter bounds or refinement types. -
Increase the timeout.
VerifyOptions/assura.tomldefault to 1000 ms, but Layer 1 clause solvers floor at 10 seconds so short defaults do not starve multi-clause demos. Set a higher value (e.g. 30000 or 60000) when you need more than the floor:[verify] timeout = 30000The same budget applies to Z3 and CVC5 (shell and native), and to portfolio mode: Z3 uses the solver
timeoutoption; CVC5 usestlimit/--tlimitwith the same floor/resolve helper. Layer 2 advanced passes use the configured value without that floor. -
Use Layer 0 first. Structural checks (Layer 0) are instant and catch type errors, undefined names, and effect violations without invoking Z3:
assura check file.assura --layer 0 -
Try portfolio mode. CVC5 may handle the query faster:
assura check file.assura --solver portfolio
Understanding counterexamples
Symptom: Z3 returns a Counterexample but the values look strange.
A counterexample is a concrete set of inputs that violates a contract clause. For example:
Counterexample for SafeDivision ensures clause:
a = -2147483648
b = -1
This means signed integer division of INT_MIN / -1 overflows. The
fix is to add a precondition:
requires { !(a == min_int() && b == -1) }
Tips for reading counterexamples:
- Z3 picks adversarial edge cases:
0,-1,MAX_INT,MIN_INT, empty collections. - If the counterexample involves very large numbers, your contract may be missing overflow guards.
- If the values look random, the contract clause may be too weak to constrain the search space.
“Unknown” verification result
Full honesty map: What we prove.
Unknown means the solver could not decide the obligation. Reasons
include timeouts, incomplete models, and known SMT limitations
(reason contains not yet encoded in SMT). The CLI treats that known
limitation marker as a warning (exit 0), not a hard verification
failure. Other Unknown reasons are more severe.
This is different from Timeout (budget exceeded) and from Counterexample (definite failure with a model).
Common causes of inconclusive results:
- Non-linear arithmetic (multiplication of two variables)
- Recursive functions without decreases clauses
- Mixing bitvector and integer theories
Fix: simplify the contract or add auxiliary lemmas.
Verification passes but generated code fails cargo check
The contract is verified but the generated Rust code does not compile. This usually means:
-
Missing type import. The generated code references a type that is not in scope. Add it to the contract’s context.
-
Generic type mismatch. The contract uses
Intbut the Rust function expects a specific integer type likei32. The codegen mapsInttoi64by default. -
Generated project structure. Check the
generated/directory. It should be a valid Cargo workspace. Runcargo checkinside it to see the exact error.
Type Errors
A03001: type mismatch
The most common error. An expression has type X but the context
expects type Y. Also used for empty tuple types such as (,),
(Int,,Bool), nested List<(,)> / Map<String, (,)>, type aliases
(type Bad = (,) or type Bad = List<(,)>), refined bases
(type Bad = { x: (,) | true }), enum payloads (enum E { Bad((,)) }),
and empty tuple params or returns on fn / service (use () for
Unit, or (T,) for a 1-tuple). Pair tuples and generics work in enum
payloads and params: enum E { Box(List<Int>), Pair((Int, Bool)) },
fn f(t: (Int, Bool)). Match arms must use the right field count:
Pair(x, y) is valid; Pair(x) is A03001.
Error A03001: type mismatch
expected Bool, found Int
--> contracts/lib.assura:5:15
Fix: Check that the requires/ensures clause body evaluates to
Bool. Arithmetic expressions like x + 1 are Int, not Bool.
Use comparisons: x + 1 > 0.
A03005: unknown field or method
Error A03005: unknown field `len` on type List<Int>
In Assura contracts, use length(xs) instead of xs.len(). The
contract language uses mathematical functions, not Rust methods.
A03006: clause body not Bool
Error A03006: requires clause must be Bool, found Int
requires / ensures / invariant clause bodies must be boolean
predicates. Use comparisons (x > 0) or logical operators, not bare
arithmetic (x + 1). (Arity mismatches on function calls use A03002.)
Effect Errors
A07001: undeclared effect
Error A07001: undeclared effect `io` in pure function
The function body uses an effect that is not in the effects clause.
Either add the effect:
effects { io }
Or mark the function as pure only if it truly has no side effects.
A07003: unknown effect name
Error A07003: unknown effect name `memory`
Use one of the built-in effect names:
Groups: io, database, logging
Leaf effects: console.read, console.write, filesystem.read,
filesystem.write, network.connect, network.send,
network.receive, time.read, random, database.read,
database.write, log.debug, log.info, log.warn, log.error
Short aliases: mem, net, fs, rng, time, alloc,
diverge
Custom sub-effects like io.custom are accepted if the group (io)
is known.
Name Resolution Errors
A02001: undefined symbol
Error A02001: undefined symbol `result`
--> contracts/lib.assura:7:15
result is only available inside ensures clauses. In requires
clauses, you can only reference input parameters.
Similarly, old(x) is only valid in ensures clauses.
CLI
assura check vs assura build
checkruns the full verification pipeline (parse, resolve, type-check, SMT) and reports results. Does not generate code.builddoes everythingcheckdoes, plus generates a Rust project ingenerated/and runscargo checkon it.
Use check during development. Use build when you are ready to
integrate the generated code.
assura infer output is incomplete
assura infer generates skeleton bind declarations from Rust source
files. It extracts public function signatures but cannot infer
meaningful preconditions and postconditions.
The output is a starting point. You must fill in the requires and
ensures clauses based on the function’s intended behavior.
assura audit is slow
assura audit generates contracts for every public function and
verifies them all. For large projects:
# Focus on specific modules
assura audit . --focus "parser::*"
# Limit function count
assura audit . --max-functions 20
# Reduce timeout per function
assura audit . --timeout 2000
# Only audit unsafe functions
assura audit . --unsafe-only
Watch mode does not detect changes
assura check file.assura --watch uses filesystem notifications. If
changes are not detected:
- Save the file explicitly (some editors delay writes).
- Check that the file path matches exactly (watch tracks the specific file, not the directory).
- On Linux, check
inotifylimits:cat /proc/sys/fs/inotify/max_user_watches # Increase if needed echo 524288 | sudo tee /proc/sys/fs/inotify/max_user_watches
Editor Integration
VS Code: LSP not starting
- Install the Assura extension from the
editors/vscode/directory. - Set the server path in settings:
{ "assura.serverPath": "/path/to/assura-lsp" } - Build the LSP server:
cargo build --bin assura-lsp - Restart VS Code.
The LSP provides diagnostics, go-to-definition, hover, completions, formatting, find references, and rename.
VS Code: no syntax highlighting
The TextMate grammar in editors/vscode/syntaxes/assura.tmLanguage.json
provides basic highlighting. If colors are missing:
- Reload the extension: Ctrl+Shift+P > “Developer: Reload Window”
- Check that
.assurafiles are recognized: look for “Assura” in the bottom-right language selector.
Project Configuration
assura.toml options
[package]
name = "my-project"
version = "0.1.0"
[build]
output = "generated"
target = "native" # or "wasm"
[verify]
timeout = 5000
layer = 255 # 0=structural, 1=SMT, 255=all
solver = "z3" # "z3", "cvc5", or "portfolio"
[profile]
parallel = true
cache = true
Creating a new project
assura init my-project
cd my-project
assura check contracts/lib.assura
This creates assura.toml and a starter contract in contracts/.
Migration Guide: Dafny, Verus, and SPARK to Assura
This guide helps users of existing formal verification tools translate their contracts and workflows to Assura. Each section covers syntax mapping, key differences, common pitfalls, and a complete worked example.
Dafny to Assura
Syntax Mapping
| Dafny | Assura | Notes |
|---|---|---|
method Foo(x: int) returns (r: int) | contract Foo { input(x: Int) output(result: Int) } | Dafny combines spec+impl; Assura separates them |
requires x > 0 | requires { x > 0 } | Same keyword, Assura uses braces |
ensures r > 0 | ensures { result > 0 } | Assura uses result instead of named return |
decreases x | decreases { x } | Same semantics |
invariant i >= 0 | invariant { i >= 0 } | Same semantics |
forall x :: P(x) | forall x in collection : P(x) | Assura binds over collections |
exists x :: P(x) | exists x in collection : P(x) | Assura binds over collections |
old(x) | old(x) | Same semantics |
function F(x: int): int | contract F { ... effects { pure } } | Dafny functions are ghost; Assura uses pure effect |
ghost var g: int | ghost { g: Int } | Similar concept |
class C { ... } | type C { ... } or service C { ... } | Assura uses types for data, services for stateful objects |
modifies this | effects { mem } | Assura uses the effect system for mutation tracking |
Key Differences
-
Separation of concerns: Dafny verifies implementation inline (the method body IS the implementation). Assura separates the contract (what) from the implementation (how). You write
.assurafiles for contracts; AI or developers write Rust for implementations. -
Target language: Dafny compiles to C#, Go, Java, JavaScript, or Python. Assura generates Rust source code.
-
Effect tracking: Dafny uses
reads/modifiesclauses. Assura uses a hierarchical effect system (io,database,net, etc.) that tracks fine-grained side effects. -
Linear types: Dafny has no linear type system. Assura supports usage grades (
:_1,:_n,:_omega) for resource management.
Common Pitfalls
- No method bodies: Assura contracts do not contain implementation
code. If you have
method Foo() { ... }in Dafny, split it into a contract (.assura) and a Rust implementation. - Return values: Dafny uses named returns (
returns (r: int)). Assura usesoutput(result: Type)and the keywordresultin ensures clauses. - Quantifier syntax: Dafny uses
forall x :: P(x). Assura requires a binding domain:forall x in range : P(x).
Worked Example
Dafny:
method BinarySearch(a: array<int>, key: int) returns (index: int)
requires forall i, j :: 0 <= i < j < a.Length ==> a[i] <= a[j]
ensures 0 <= index < a.Length ==> a[index] == key
ensures index == -1 ==> forall i :: 0 <= i < a.Length ==> a[i] != key
{
var lo, hi := 0, a.Length;
while lo < hi
invariant 0 <= lo <= hi <= a.Length
invariant forall i :: 0 <= i < lo ==> a[i] < key
invariant forall i :: hi <= i < a.Length ==> a[i] > key
{
var mid := (lo + hi) / 2;
if a[mid] < key { lo := mid + 1; }
else if a[mid] > key { hi := mid; }
else { return mid; }
}
return -1;
}
Assura:
contract BinarySearch {
input(a: List<Int>, key: Int)
output(result: Int)
requires { forall i in a : forall j in a : i < j ==> a[i] <= a[j] }
ensures { result >= 0 ==> a[result] == key }
ensures { result == 0 - 1 ==> forall i in a : a[i] != key }
effects { pure }
}
Verus to Assura
Syntax Mapping
| Verus | Assura | Notes |
|---|---|---|
proof fn lemma(x: int) | contract lemma { ... } | Verus proof fns are ghost; Assura contracts are specs |
exec fn compute(x: u64) -> u64 | bind "crate::compute" as compute { ... } | Verus annotates Rust; Assura binds to existing Rust |
spec fn abstract_view(x: T) -> V | contract abstract_view { ... effects { pure } } | Verus spec fns map to pure contracts |
requires(x > 0) | requires { x > 0 } | Same keyword, different syntax |
| `ensures( | r | r > 0)` |
decreases(x) | decreases { x } | Same semantics |
tracked x: T | ghost { x: T } | Verus “tracked” is similar to Assura ghost |
assert(P) | requires { P } or invariant { P } | Context-dependent |
#[verifier::external_body] | (default behavior) | Assura always separates spec from impl |
Key Differences
-
Integration model: Verus annotates Rust source code directly with
proof,exec, andspeckeywords. Assura uses separate.assurafiles that reference Rust code viabinddeclarations. -
Same language: Verus specs are written in Rust syntax. Assura has its own contract language with domain-specific features (typestate, effects, information flow).
-
Ownership model: Verus leverages Rust’s ownership and borrowing directly. Assura has its own linear type system that maps to Rust ownership in codegen.
-
SMT interaction: Verus uses a custom SMT encoding with triggers and quantifier patterns. Assura uses Z3 with configurable verification layers (Layer 0: structural, Layer 1: basic SMT, Layer 2: full).
Common Pitfalls
- No inline verification: Verus proves properties inline in Rust.
Assura contracts are external specs. You cannot write
proof { ... }blocks in Assura; useghost { ... }clauses instead. - Type mapping: Verus uses Rust types (
u64,Vec<T>). Assura uses abstract types (Nat,List<T>) that map to Rust types in codegen. - Tracked vs ghost: Verus
trackedvariables participate in ownership. Assuraghostvariables are fully erased at runtime.
Worked Example
Verus:
#![allow(unused)]
fn main() {
use vstd::prelude::*;
verus! {
spec fn triangle(n: nat) -> nat
decreases n
{
if n == 0 { 0 }
else { n + triangle((n - 1) as nat) }
}
proof fn triangle_is_monotonic(i: nat, j: nat)
requires i <= j
ensures triangle(i) <= triangle(j)
decreases j - i
{
if i < j {
triangle_is_monotonic(i, (j - 1) as nat);
}
}
}
}
Assura:
contract triangle {
input(n: Nat)
output(result: Nat)
ensures { result >= 0 }
decreases { n }
effects { pure }
}
contract triangle_is_monotonic {
input(i: Nat, j: Nat)
output(result: Unit)
requires { i <= j }
ensures { true }
decreases { j - i }
effects { pure }
}
SPARK Ada to Assura
Syntax Mapping
| SPARK Ada | Assura | Notes |
|---|---|---|
Pre => X > 0 | requires { X > 0 } | Same semantics |
Post => Result > 0 | ensures { result > 0 } | SPARK uses Result, Assura uses result |
Global => (Input => X) | effects { pure } or specific effects | SPARK Global maps to Assura effects |
Depends => (Result => X) | data_flow { result depends_on x } | Direct mapping |
Contract_Cases | Multiple requires/ensures pairs | Assura uses clause repetition |
type T is new Integer range 1 .. 100 | refinement type { v: Int | v >= 1 && v <= 100 } | SPARK subtypes map to refinement types |
pragma Assert(P) | invariant { P } | Context-dependent |
with SPARK_Mode => On | (always on in .assura files) | Assura files are always verified |
Key Differences
-
Language family: SPARK annotates Ada code with contracts. Assura generates Rust code. The underlying language families are very different (Ada is a systems language with tasking; Rust has ownership and borrowing).
-
Information flow: SPARK has
GlobalandDependsfor data flow tracking. Assura has botheffects(for side effects) anddata_flow(for information flow / taint tracking). Assura’s system is more granular. -
Concurrency: SPARK uses Ada tasking with Ravenscar profile. Assura uses effect-based concurrency tracking.
-
Tooling: SPARK uses GNATprove (based on Why3/Alt-Ergo/CVC5). Assura uses Z3 as the primary solver with CVC5 as a fallback.
Common Pitfalls
- No subtype ranges: SPARK’s
range 1 .. 100does not have a direct Assura equivalent. Use refinement types:type Bounded = { v: Int | v >= 1 && v <= 100 }. - Global annotations: SPARK’s
Global => (In_Out => State)maps to Assura’s effect system. Useeffects { mem }for in-memory state modification. - Package structure: SPARK uses Ada packages (spec + body). Assura
uses modules (
.assurafiles). Each SPARK package spec maps to one Assura module.
Worked Example
SPARK Ada:
package Stack
with SPARK_Mode => On
is
Max_Size : constant := 100;
type Element is new Integer;
procedure Push (E : in Element)
with Pre => not Is_Full,
Post => Size = Size'Old + 1 and Top = E,
Global => (In_Out => State);
function Is_Full return Boolean
with Post => Is_Full'Result = (Size = Max_Size),
Global => (Input => State);
function Size return Natural
with Global => (Input => State);
function Top return Element
with Pre => Size > 0,
Global => (Input => State);
end Stack;
Assura:
module stack
type Element = Int
contract push {
input(e: Element)
output(result: Unit)
requires { size < 100 }
ensures { old(size) + 1 == size }
effects { mem }
}
contract is_full {
input()
output(result: Bool)
ensures { result == (size == 100) }
effects { pure }
}
contract size {
input()
output(result: Nat)
ensures { result >= 0 }
ensures { result <= 100 }
effects { pure }
}
contract top {
input()
output(result: Element)
requires { size > 0 }
effects { pure }
}
Quick Reference: Equivalent Concepts
| Concept | Dafny | Verus | SPARK | Assura |
|---|---|---|---|---|
| Precondition | requires | requires() | Pre => | requires { } |
| Postcondition | ensures | ensures(|r| ) | Post => | ensures { } |
| Loop invariant | invariant | invariant() | Loop_Invariant | invariant { } |
| Termination | decreases | decreases() | Subprogram_Variant | decreases { } |
| Old value | old(x) | old(x) | X'Old | old(x) |
| Return value | named return | closure param | Result | result |
| Ghost code | ghost var | tracked/ghost | Ghost | ghost { } |
| Pure function | function | spec fn | no side effect | effects { pure } |
| Data flow | reads/modifies | n/a | Depends | data_flow { } |
| Side effects | modifies | n/a | Global | effects { io, mem, ... } |
| Refinement type | subset type | n/a | subtype range | { v: T | P } |
| Linear type | n/a | ownership | n/a | :_1, :_n |
| Typestate | n/a | n/a | n/a | states { } |
Assura Language Specification v0.1
Implementer’s reference. Every section is designed so an engineer can build the corresponding compiler component without ambiguity.
Verification Scope
Assura verifies safety and liveness properties of single-node programs under all Rust memory orderings (SeqCst, AcqRel, Relaxed).
Safety (Layer 0-2): preconditions, postconditions, invariants, frame conditions, structural invariants. Decidable or bounded SMT.
Liveness (Layer 2-3): bounded model checking with k-induction. Properties like “eventually a leader is elected” are verified up to a configurable step bound K, with optional unbounded proof via k-induction.
Concurrency (Layer 1-2): per-thread views for weak memory ordering, prophecy variables for linearizability proofs. These extend the verification beyond what Dafny, F*, or SPARK offer.
Table of Contents
- 1. Contract Language Grammar
- 2. Type System
- 3. Effect System
- 4. Implementation IR Format
- 5. Verification Architecture
- 6. Rust Codegen Mapping
- 7. Error Code Catalog
- 8. Module System
- 9. Standard Library
- 10. CLI Interface
- 11. AI Agent API
- 12. Decidability Boundaries
- 13. Type Interaction Test Cases
- 14. Verification Categories
- 14.CORE: Verification Infrastructure
- 14.MEM: Memory Safety
- 14.TYPE: Types and Contracts
- 14.SEC: Trust and Security
- 14.CONC: Concurrency
- 14.STOR: Storage and Durability
- 14.FMT: Data Formats and Parsing
- 14.NUM: Numerical and Precision
- 14.PLAT: Platform and Configuration
- 14.PERF: Performance
- 14.TEST: Testing and Verification Workflow
- 14.MISC: Specialized
1. Contract Language Grammar
The contract language is what humans write. It is declarative,
readable, and complete. The grammar below is in EBNF notation.
Terminals are in 'quotes' or UPPER_CASE. Non-terminals are in
PascalCase.
1.2 Project Profile
A project declares which verification categories are active. Features in excluded categories are ignored by the parser and skipped by the verifier.
project stb_image_rs {
profile: [core, mem, sec, fmt, num, type, test]
}
CORE is always included and cannot be excluded.
Preset Profiles
| Preset | Categories | Example Projects |
|---|---|---|
| minimal | CORE, MEM, TYPE | Simple libraries, CLI tools |
| parser | + SEC, FMT | stb_image, picohttpparser, protobuf |
| database | + STOR, CONC | SQLite, RocksDB |
| embedded | + STOR, PLAT | littlefs, RTOS drivers |
| crypto | + SEC, CONC, NUM | WireGuard, libsodium |
| tls | + SEC, CONC, FMT, NUM, PLAT | mbedTLS, s2n-tls |
| systems | All categories | jemalloc, kernel modules |
Custom profiles list categories explicitly. Adding a category mid-project is always safe (additive).
1.1 Lexical Grammar
(* Identifiers *)
Ident = Letter (Letter | Digit | '_')* ;
TypeIdent = UpperLetter (Letter | Digit | '_')* ;
FieldIdent = LowerLetter (Letter | Digit | '_')* ;
EffectIdent = LowerLetter (Letter | Digit | '.' | '_')* ;
(* Literals *)
IntLit = ['-'] Digit+ ['_' Digit+]* ;
FloatLit = ['-'] Digit+ '.' Digit+ ;
StringLit = '"' { StringChar } '"' ;
BoolLit = 'true' | 'false' ;
(* Comments *)
LineComment = '//' { any except newline } ;
BlockComment = '/*' { any } '*/' ;
(* Keywords *)
Keyword = 'service' | 'contract' | 'type' | 'enum' | 'states'
| 'transition' | 'operation' | 'query' | 'input'
| 'output' | 'errors' | 'requires' | 'ensures'
| 'invariant' | 'effects' | 'must-not' | 'rule'
| 'data-flow' | 'extern' | 'bind' | 'import'
| 'module' | 'pub' | 'where' | 'forall' | 'exists'
| 'in' | 'not' | 'and' | 'or' | 'if' | 'then'
| 'else' | 'old' | 'result' | 'self'
| 'concurrency' | 'privacy' | 'retention' | 'audit'
| 'evolution' | 'transaction' | 'serialization'
| 'api_compat' | 'performance' | 'protocol'
| 'observe' | 'compliance' | 'ordering'
| 'idempotent' ;
1.2 Top-Level Declarations
SourceFile = ModuleDecl? { ImportDecl } { TopDecl } ;
ModuleDecl = 'module' QualifiedName ';' ;
ImportDecl = 'import' QualifiedName ['as' Ident]
['{' ImportList '}'] ';' ;
ImportList = Ident { ',' Ident } ;
QualifiedName = Ident { '.' Ident } ;
TopDecl = ServiceDecl
| ContractDecl
| TypeDecl
| EnumDecl
| ExternDecl
| BindDecl
| ComplianceDecl
| ProtocolDecl ;
1.3 Service Declarations
ServiceDecl = 'service' TypeIdent '{'
{ ServiceItem }
'}' ;
ServiceItem = TypeDecl
| EnumDecl
| StateDecl
| TransitionDecl
| OperationDecl
| QueryDecl
| InvariantDecl
| ConcurrencyDecl
| PrivacyDecl
| RetentionDecl
| AuditDecl
| EvolutionDecl
| TransactionDecl
| SerializationDecl
| ApiCompatDecl
| PerformanceDecl
| ProtocolDecl
| ObserveDecl
| ComplianceDecl
| OrderingDecl
| IdempotencyDecl ;
1.4 Type Declarations
TypeDecl = 'type' TypeIdent [TypeParams] ['=' TypeExpr]
['{' { FieldDecl } '}']
[WhereClause] ;
TypeParams = '<' TypeParam { ',' TypeParam } '>' ;
TypeParam = TypeIdent [':' KindExpr] ;
FieldDecl = [Visibility] FieldIdent ':' TypeExpr
[FieldConstraint] ';' ;
Visibility = 'pub' ;
FieldConstraint = 'where' Predicate ;
(* Types *)
TypeExpr = BaseType
| RefinedType
| FunctionType
| GenericType
| TupleType
| OptionType
| ListType
| MapType
| SetType ;
BaseType = TypeIdent [TypeArgs]
| BuiltinType ;
BuiltinType = 'Int' | 'Nat' | 'Float' | 'Bool' | 'String'
| 'Bytes' | 'Unit' | 'Never' ;
RefinedType = '{' Ident ':' TypeExpr '|' Predicate '}' ;
FunctionType = '(' [ParamList] ')' '->' EffectRow TypeExpr ;
GenericType = TypeIdent '<' TypeExpr { ',' TypeExpr } '>' ;
TupleType = '(' TypeExpr ',' TypeExpr { ',' TypeExpr } ')' ;
OptionType = TypeExpr '?' ;
ListType = 'List' '<' TypeExpr '>' ;
MapType = 'Map' '<' TypeExpr ',' TypeExpr '>' ;
SetType = 'Set' '<' TypeExpr '>' ;
TypeArgs = '<' TypeExpr { ',' TypeExpr } '>' ;
1.5 Enum and State Declarations
EnumDecl = 'enum' TypeIdent [TypeParams] '{'
EnumVariant { ',' EnumVariant } [',']
'}' ;
EnumVariant = TypeIdent ['(' TypeExpr { ',' TypeExpr } ')'] ;
StateDecl = 'states' ':' StateIdent { '->' StateIdent } ;
StateIdent = TypeIdent ;
TransitionDecl = 'transition' StateIdent
['requires' ':' Predicate]
';' ;
1.6 Operations and Queries
OperationDecl = 'operation' TypeIdent '{'
{ OperationItem }
'}' ;
QueryDecl = 'query' TypeIdent '{'
{ OperationItem }
'}' ;
OperationItem = InputDecl
| OutputDecl
| ErrorsDecl
| RequiresClause
| EnsuresClause
| EffectsClause
| MustNotClause
| RuleClause
| DataFlowClause ;
InputDecl = 'input' ':' '{' { FieldDecl } '}' ;
| 'input' '(' ParamList ')' ;
OutputDecl = 'output' ':' '{' { FieldDecl } '}' ;
| 'output' '(' ParamList ')' ;
ErrorsDecl = 'errors' ':' '[' TypeExpr { ',' TypeExpr } ']' ;
ParamList = Param { ',' Param } ;
Param = Ident ':' TypeExpr ;
1.7 Contract Clauses
ContractDecl = 'contract' TypeIdent [TypeParams] '{'
{ ContractItem }
'}' ;
ContractItem = InputDecl | OutputDecl | RequiresClause
| EnsuresClause | EffectsClause | InvariantDecl ;
RequiresClause = 'requires' ['{'] Predicate ['}'] ;
| 'requires' ':' Predicate ;
EnsuresClause = 'ensures' ['{'] Predicate ['}'] ;
| 'ensures' ':' Predicate ;
InvariantDecl = 'invariant' ['{'] Predicate ['}'] ;
| 'invariant' ':' Predicate ;
EffectsClause = 'effects' ':' EffectList ;
| 'effects' ['{'] EffectList ['}'] ;
MustNotClause = 'must-not' ':' EffectList ;
EffectList = EffectIdent { ',' EffectIdent } ;
RuleClause = 'rule' ':' Predicate ;
DataFlowClause = 'data-flow' ':' Expr 'must-not-appear-in'
Ident { ',' Ident } ;
1.8 Predicates and Expressions
Predicate = PredAtom
| Predicate 'and' Predicate
| Predicate 'or' Predicate
| 'not' Predicate
| Predicate '=>' Predicate (* implication *)
| Quantifier
| '(' Predicate ')'
| 'if' Predicate 'then' Predicate
['else' Predicate] ;
PredAtom = Expr RelOp Expr
| Expr 'in' Expr
| Expr 'not' 'in' Expr
| Expr 'is' TypeIdent
| Expr (* boolean expr *) ;
Quantifier = 'forall' Ident 'in' Expr ':' Predicate
| 'exists' Ident 'in' Expr ':' Predicate ;
RelOp = '==' | '!=' | '<' | '<=' | '>' | '>=' ;
Expr = Literal
| Ident
| 'self'
| 'result'
| 'old' '(' Expr ')'
| Expr '.' Ident
| Expr '.' Ident '(' [ArgList] ')'
| Expr BinOp Expr
| UnaryOp Expr
| Expr '[' Expr ']'
| '|' Expr '|' (* absolute value *)
| '(' Expr ')'
| 'if' Expr 'then' Expr 'else' Expr
| ListExpr
| SetExpr
| MapExpr ;
BinOp = '+' | '-' | '*' | '/' | '%' | '++' ;
UnaryOp = '-' | 'not' ;
ArgList = Expr { ',' Expr } ;
Literal = IntLit | FloatLit | StringLit | BoolLit ;
ListExpr = '[' [Expr { ',' Expr }] ']' ;
SetExpr = '{' Expr { ',' Expr } '}' ;
MapExpr = '{' [MapEntry { ',' MapEntry }] '}' ;
MapEntry = Expr ':' Expr ;
1.9 Extended Contract Layers (8-27)
(* Layer 8: Concurrency *)
ConcurrencyDecl = 'concurrency' ':' TypeIdent 'is'
ConcurrencyMode ;
ConcurrencyMode = 'exclusive' | 'shared-read' | 'actor-isolated' ;
(* Layer 9: Numerical Precision -- expressed via type aliases *)
(* e.g., type Money = FixedDecimal<2, USD> *)
(* Layer 10: Temporal Ordering *)
OrderingDecl = 'ordering' ':' TypeIdent 'must' 'be' 'processed'
'in' OrderingMode ;
OrderingMode = 'chronological' 'order'
| 'reverse' 'chronological' 'order'
| 'dependency' 'order' ;
(* Layer 11: Idempotency *)
IdempotencyDecl = 'idempotent' ':' TypeIdent 'consumes'
TypeIdent '(' LinearityMode ')' ;
LinearityMode = 'linear' | 'affine' ;
(* Layer 12: Privacy *)
PrivacyDecl = 'privacy' ':' Expr 'purpose' '{' PurposeList '}' ;
PurposeList = Ident { ',' Ident } ;
RetentionDecl = 'retention' ':' Ident 'retain' Duration
'then' RetentionAction ;
Duration = IntLit '_' TimeUnit ;
TimeUnit = 'days' | 'months' | 'years' ;
RetentionAction = 'delete' | 'archive' | 'anonymize' ;
(* Layer 13: Schema Evolution *)
EvolutionDecl = 'evolution' ':' TypeIdent 'v' IntLit
EvolutionAction ;
EvolutionAction = 'adds' 'field' FieldDecl
| 'removes' 'field' Ident
| 'renames' 'field' Ident 'to' Ident
| 'changes' 'field' Ident 'from' TypeExpr 'to'
TypeExpr ['default' Expr] ;
(* Layer 14: Crash Safety *)
TransactionDecl = 'transaction' ':' TypeIdent '{'
{ CrashHandler }
'}' ;
CrashHandler = 'on_crash' ':' CrashAction ;
CrashAction = 'rollback' Expr
| 'mark' Expr 'as' TypeIdent
| 'compensate' Expr ;
(* Layer 15: Audit *)
AuditDecl = 'audit' ':' AuditRule ;
AuditRule = 'every' 'mutation' 'to' TypeIdent
'requires' TypeIdent [AuditConstraint]
| Ident 'requires' TypeIdent [AuditConstraint] ;
AuditConstraint = 'with' Predicate ;
(* Layer 16: Serialization *)
SerializationDecl = 'serialization' ':' TypeIdent
SerializationGuarantee ;
SerializationGuarantee = 'roundtrip' 'guarantee'
| 'backward' 'compatible'
| 'forward' 'compatible' ;
(* Layer 17: API Evolution *)
ApiCompatDecl = 'api_compat' ':' 'v' IntLit 'of' TypeIdent
ApiCompatRule ;
ApiCompatRule = 'may' 'add' ApiTarget
| 'may' 'not' 'remove' ApiTarget
| 'may' 'not' 'add' 'required' ApiTarget ;
ApiTarget = 'response' 'fields'
| 'request' 'fields'
| 'error' 'variants' ;
(* Layer 18: Performance *)
PerformanceDecl = 'performance' ':' TypeIdent 'is'
ComplexityBound 'in' Ident ;
ComplexityBound = 'O' '(' ComplexityExpr ')' ;
ComplexityExpr = '1' | 'n' | 'n' '^' IntLit
| 'log' 'n' | 'n' 'log' 'n' ;
(* Layer 19: Multi-Service Protocol *)
ProtocolDecl = 'protocol' TypeIdent '{'
{ ProtocolStep }
'}' ;
ProtocolStep = TypeIdent '->' TypeIdent ':' TypeIdent
['|' TypeIdent] ;
(* Layer 20: Observability *)
ObserveDecl = 'observe' ':' ObserveRule ;
ObserveRule = 'every' 'operation' 'emits' ObserveTarget
['with' Ident]
| TypeIdent 'emits' ObserveTarget ;
ObserveTarget = 'trace_span' | 'metric' Ident | 'log' Ident ;
(* Layer 21: Regulatory Compliance *)
ComplianceDecl = 'compliance' TypeIdent '{'
{ ComplianceRule }
'}' ;
ComplianceRule = 'rule' ':' Predicate ;
1.10 Extern and Bind Declarations
ExternDecl = 'extern' 'fn' Ident '(' [ParamList] ')'
'->' TypeExpr
{ ExternClause } ;
ExternClause = RequiresClause | EnsuresClause | EffectsClause ;
BindDecl = 'bind' StringLit 'as' Ident '{'
{ BindItem }
'}' ;
BindItem = InputDecl | OutputDecl | RequiresClause
| EnsuresClause | EffectsClause ;
1.11 Where Clauses and Kind Expressions
WhereClause = 'where' WhereItem { ',' WhereItem } ;
WhereItem = TypeParam ':' KindExpr
| Predicate ;
KindExpr = 'Type'
| 'Nat'
| 'Effect'
| 'Label'
| KindExpr '->' KindExpr ;
1.12 Effect Rows (used in function types)
EffectRow = '<' [EffectElem { ',' EffectElem }
['|' EffectVar]] '>'
| 'pure'
| EffectVar ;
EffectElem = EffectIdent ['<' TypeArgs '>'] ;
EffectVar = LowerLetter Ident? ;
2. Type System
Assura’s type system combines six features into a unified framework. Each feature is proven in isolation by prior work; the contribution is their composition. The core calculus is based on Quantitative Type Theory (QTT, Idris 2) extended with refinement predicates (Liquid Haskell), effect rows (Koka), information flow labels (Jif/FlowCaml), and typestate (Plaid).
2.1 Universes and Kinds
Kind ::= Type -- the kind of value types
| Nat -- natural number indices
| Effect -- effect rows
| Label -- security labels
| Kind -> Kind -- higher-order kinds
All types live in Type. Natural number indices (Nat) are used for
dependent typing (vector lengths, matrix dimensions). Effect is the
kind of effect rows. Label is the kind of security labels.
2.2 Core Judgment Forms
The central typing judgment is:
Gamma; Delta; Sigma |- e : T ! E
Where:
Gamma= unrestricted context (types, constants, type aliases)Delta= graded linear context (variables with usage grades)Sigma= typestate context (variables with current state)e= expressionT= type (possibly refined)E= effect row
Graded Linear Context (Delta)
Each entry in Delta has the form x :_{r} T where r is a grade
from the semiring (N+, 0, 1, +, *) extended with omega (unlimited):
Grade ::= 0 -- erased (compile-time only)
| 1 -- linear (use exactly once)
| n -- exact count (use exactly n times)
| omega -- unrestricted (use any number of times)
Grades compose:
- Parallel use:
r + s(both branches of a pair use x) - Sequential use:
r * s(f uses x r times, applied to g that uses x s times) - Subgrading:
0 <= 1 <= omega(a linear variable can be treated as unrestricted is NOT allowed; the reverse is)
Typestate Context (Sigma)
Each entry in Sigma has the form x @ S where S is the current
state of x from a declared state machine:
Sigma = { x1 @ S1, x2 @ S2, ... }
Operations on x update its state in Sigma. The type checker rejects
any operation that is not valid for the current state.
2.3 Refinement Types
A refined type {v : T | P} pairs a base type T with a predicate
P over the value v. The predicate is a term in the decidable
fragment of first-order logic (QF_UFLIA: quantifier-free uninterpreted
functions + linear integer arithmetic).
Subtyping Rule
Gamma |- {v:S | P} <: {v:T | Q}
if S <: T
and Gamma, v:T, P |- Q (checked by SMT: P => Q is valid)
Refinement subtyping reduces to SMT validity checking. The solver
receives P => Q and must prove it valid (return unsat for P and not Q).
Built-in Refinement Aliases
type Nat = {v: Int | v >= 0}
type Pos = {v: Int | v > 0}
type NonEmpty<T> = {v: List<T> | len(v) > 0}
type Btwn<Lo, Hi> = {v: Int | Lo <= v and v < Hi}
type Percentage = {v: Float | 0.0 <= v and v <= 1.0}
Measures
Measures bridge data structures into the logic. They are structurally recursive functions lifted into the refinement language:
measure len : List<T> -> Nat
len([]) = 0
len(_ :: t) = 1 + len(t)
measure elems : List<T> -> Set<T>
elems([]) = {}
elems(x :: xs) = {x} ++ elems(xs)
Measures appear as uninterpreted functions in SMT with axioms generated from their definitions.
2.4 Dependent Types
Types may depend on values. The dependent function type (Pi type) is:
(x : A) -> B where B may mention x
Assura supports a restricted form of dependent types: types may depend
on values of kind Nat, Bool, and finite enums. Full value-level
dependency (arbitrary terms in types) is deferred to v2.
Examples
type Vec<T, n: Nat> = ...
fn head(v: Vec<T, n>) -> T
where n > 0
fn append(a: Vec<T, n>, b: Vec<T, m>) -> Vec<T, n + m>
fn replicate(x: T, n: Nat) -> Vec<T, n>
Index Erasure
All Nat-kinded indices are erased at runtime. They exist only for
type checking. The generated Rust code has no runtime representation
of type indices.
2.5 Linear and Graded Types
Following Granule and Idris 2’s QTT, every binding carries a usage grade:
fn use_once(conn :_1 DbConnection) -> Result
-- conn must be used exactly once
fn use_twice(val :_2 Token) -> (Token, Token)
-- val is used exactly twice
fn read_only(cfg :_omega Config) -> Settings
-- cfg can be used any number of times
fn compile_only(phantom :_0 TypeInfo) -> Unit
-- phantom is erased at runtime
Context Splitting
When type-checking a pair (e1, e2), the linear context Delta is
split: Delta = Delta1 + Delta2 where each variable’s grade is
partitioned between the two sub-expressions.
Delta1 + Delta2 = Delta
if for each x: r1 + r2 = r where Delta(x) = r, Delta1(x) = r1, Delta2(x) = r2
Linear Protocol: Resource Safety
Linear types enforce resource protocols:
type File<State> where State in {Closed, Open}
fn open(path: String) -> File<Open> :_1
effects: filesystem.read
fn read(f: File<Open> :_1) -> (File<Open> :_1, Bytes)
effects: filesystem.read
fn close(f: File<Open> :_1) -> Unit
effects: filesystem.write
-- COMPILE ERROR: f used twice (grade violation)
-- fn bad(f: File<Open> :_1) = (read(f), read(f))
-- COMPILE ERROR: f not used (linear variable dropped)
-- fn leak(f: File<Open> :_1) = ()
2.6 Typestate
Typestate tracks the protocol state of objects through the type system. State changes are reflected in the type.
State Machine Declaration
type Order<State>
where State in {Created, Paid, Shipped, Delivered, Cancelled}
Transition Typing
Gamma; Delta; Sigma, x @ S1 |- transition(x) : T ! E -| Sigma', x @ S2
The judgment produces an updated typestate context where x moves
from state S1 to state S2.
Transition Rules
fn pay(o: Order<Created> :_1, payment: Payment)
-> Order<Paid> :_1
requires { payment.amount > 0 }
effects: payment.charge
fn ship(o: Order<Paid> :_1, tracking: NonEmpty<String>)
-> Order<Shipped> :_1
effects: logistics.dispatch
-- COMPILE ERROR: cannot ship from Created state
-- fn bad(o: Order<Created>) = ship(o, "TRACK123")
Typestate + Linearity Interaction
Typestate variables MUST be linear (grade 1). This prevents aliasing: if two references point to the same object, the state becomes ambiguous. Linearity guarantees each stateful object has exactly one owner.
2.7 Information Flow Types
Every type carries a security label from a lattice (L, <=). Data
can only flow from lower to higher labels. The default lattice is:
Public < Internal < Confidential < Restricted
Label Annotation
type SSN = String @Restricted
type Email = String @Internal
type PublicName = String @Public
type LogMessage = String @Public
Flow Rules
Gamma |- e : T @L1, L1 <= L2
---------------------------------
Gamma |- e : T @L2 (subsumption: can raise label)
Gamma |- e1 : T1 @L1, Gamma |- e2 : T2 @L2
----------------------------------------------
Gamma |- (e1, e2) : (T1, T2) @(L1 join L2) (join: pair takes max)
Declassification
Explicit declassification is required to lower a label:
fn mask_ssn(ssn: SSN) -> String @Public
declassify { "***-**-" ++ ssn.last(4) }
Declassification points are tracked and auditable. The compiler emits
a warning for every declassify so security reviews can inspect them.
Purpose Labels (GDPR/Privacy)
Beyond the security lattice, data carries purpose labels:
privacy: employee.ssn purpose {payroll, tax_reporting, w2_generation}
The compiler checks that ssn is only used in functions whose
declared purpose includes one of {payroll, tax_reporting, w2_generation}. Using it in a function with purpose {analytics}
is a compile error.
2.8 Totality Checking
Every function must be total unless annotated partial:
- Coverage: Pattern matches must be exhaustive
- Termination: Recursive calls must decrease on a well-founded measure
Termination Measures
fn factorial(n: Nat) -> Nat
decreases n
{
if n == 0 then 1
else n * factorial(n - 1)
}
The compiler checks that n - 1 < n on each recursive call using
the specified decreases measure.
Partial Escape Hatch
partial fn server_loop() -> Never
effects: io
{
-- intentionally non-terminating
}
partial functions cannot be called from total functions without an
explicit trust annotation.
2.9 Type Interaction Rules
The six type features interact. These rules govern the interactions:
Rule 1: Refinement + Linearity
A refined linear type {v: T @_1 | P} is checked by splitting: the
linear context tracks usage, the refinement predicate is verified by
SMT. They do not interfere.
Rule 2: Typestate + Effects
A state transition must declare its effects. The effect row is checked independently from the state transition:
fn ship(o: Order<Paid> :_1) -> Order<Shipped> :_1
effects: database.write, logistics.dispatch
Both the state transition (Paid -> Shipped) AND the effect compliance (only database.write and logistics.dispatch) are checked.
Rule 3: Information Flow + Effects
Effects are labeled with the minimum security level of data they may handle:
effects: log.info @Public -- log may only contain public data
effects: database.write @Restricted -- database may contain restricted data
A function that handles Restricted data can call database.write
but NOT log.info unless the data is declassified first.
Rule 4: Refinement + Dependent Types
Refinement predicates may reference dependent indices:
fn safe_index(v: Vec<T, n>, i: {x: Nat | x < n}) -> T
Here n is a dependent index and x < n is a refinement predicate.
The SMT solver handles the arithmetic; the type checker handles the
index propagation.
Rule 5: Linearity + Information Flow
Linear types and information flow labels are orthogonal. A value can be both linear and labeled:
fn process(key: CryptoKey @Restricted :_1) -> EncryptedData
The key must be used exactly once (linear) and cannot flow to public sinks (restricted).
Rule 6: Totality + Effects
Total functions must be pure or use only terminating effects. A
function with effects: io (which may block indefinitely) cannot be
total. The compiler partitions effects into terminating (pure,
database.read) and potentially non-terminating (io, network).
3. Effect System
3.1 Effect Algebra
Effects use row polymorphism (Koka model). An effect row is an unordered set of effect labels with an optional row variable tail:
EffectRow = <l1, l2, ..., ln> -- closed row (exactly these effects)
| <l1, l2, ..., ln | e> -- open row (these effects plus whatever e contains)
| pure -- empty row (no effects)
Effect Labels (Built-in)
pure -- no side effects
console.read -- read from stdin
console.write -- write to stdout/stderr
filesystem.read -- read files
filesystem.write -- write files
network.connect -- open network connections
network.send -- send data over network
network.receive -- receive data from network
database.read -- read from database
database.write -- write to database
payment.charge -- charge a payment method
payment.refund -- refund a payment
log.debug -- debug logging
log.info -- info logging
log.warn -- warning logging
log.error -- error logging
time.read -- read current time
random -- generate random values
state<T> -- mutable state of type T
exception<E> -- may throw exception of type E
diverge -- may not terminate
Custom Effects
effect audit.write<T> -- write audit entries of type T
effect email.send -- send emails
effect sms.send -- send SMS messages
effect cache.read -- read from cache
effect cache.write -- write to cache
3.2 Effect Polymorphism
Functions that are generic over effects use a row variable:
fn map<T, U>(list: List<T>, f: (T) -> <e> U) -> <e> List<U>
map inherits whatever effects f has. If f is pure, map is
pure. If f does IO, map does IO.
3.3 Effect Subtyping
A function with fewer effects is a subtype of a function with more:
<e1> <: <e1, e2> -- fewer effects is more specific
pure <: <e> -- pure is subtype of any effect
This means a pure function can be passed where an effectful function is expected, but not the reverse.
3.4 Effect Handlers
Effect handlers eliminate effects from the row. A handler for
exception<E> transforms <exception<E> | e> into <e>:
handle result = risky_operation()
with exception<ParseError> -> default_value
-- result has type T with effect row <e> (exception removed)
3.5 Effect Checking Rules
(* A function's body must only use effects declared in its signature *)
Gamma; Delta |- body : T ! E_actual
E_actual <=_row E_declared
-----------------------------------------
Gamma; Delta |- fn f() -> <E_declared> T { body } OK
(* Effect row inclusion *)
E1 <=_row E2 iff every label in E1 is also in E2
or E2 has an open tail variable
3.6 Effect Hierarchy
Effects form a hierarchy for convenience:
io = {console.read, console.write, filesystem.read, filesystem.write,
network.connect, network.send, network.receive, time.read, random}
database = {database.read, database.write}
logging = {log.debug, log.info, log.warn, log.error}
Using effects: io is shorthand for allowing all IO sub-effects.
4. Implementation IR Format
The Implementation IR is what AI generates. It is NOT human-readable. It is a flat, fully-annotated, canonically-serialized format optimized for Transformer attention patterns and maximum compiler checking.
4.1 Design Principles
- No variable names. Slots are numbered (
$0,$1,$2). Eliminates naming hallucination. - Flat structure. No nesting deeper than 2 levels. Each operation is a single instruction. Optimized for Transformer positional attention.
- Complete annotations. Every expression has an explicit type. No type inference. Every function has explicit effect and linearity annotations.
- Canonical serialization. One way to write everything. No stylistic variation. Enables exact diff and caching.
- Stable addressing. Instructions are addressed by index, not by name. Enables stable error references.
4.2 IR Grammar
IRModule = 'module' ModuleId '{' { IRDecl } '}' ;
IRDecl = IRFunction
| IRType
| IRImpl ;
IRFunction = 'fn' FnId ':' IRFnType '{'
{ IRInstr }
'}' ;
IRFnType = '(' { SlotDecl } ')' '->' IRType '!' EffectRow
'@' Grade
['pre' ':' IRPred]
['post' ':' IRPred] ;
SlotDecl = SlotId ':' IRType '@' Grade ['@' Label] ;
SlotId = '$' Nat ;
IRInstr = SlotId '=' IRExpr ':' IRType ;
IRExpr = 'const' Literal
| 'load' SlotId
| 'call' FnId '(' { SlotId } ')'
| 'field' SlotId '.' FieldIndex
| 'construct' TypeId '{' { FieldIndex '=' SlotId } '}'
| 'match' SlotId '{' { MatchArm } '}'
| 'if' SlotId 'then' BlockId 'else' BlockId
| 'arith' ArithOp SlotId SlotId
| 'cmp' CmpOp SlotId SlotId
| 'cast' SlotId 'as' IRType
| 'transition' SlotId 'to' StateId
| 'declassify' SlotId 'to' Label
| 'handle' EffectId BlockId HandlerBlock ;
MatchArm = Pattern '=>' BlockId ;
BlockId = '#' Nat ;
FieldIndex = '.' Nat ;
ArithOp = 'add' | 'sub' | 'mul' | 'div' | 'mod' ;
CmpOp = 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' ;
IRPred = 'true'
| 'false'
| 'cmp' CmpOp SlotId SlotId
| 'and' IRPred IRPred
| 'or' IRPred IRPred
| 'not' IRPred
| 'forall' SlotId 'in' SlotId ':' IRPred
| 'measure' MeasureId '(' SlotId ')' CmpOp Literal
| 'call' FnId '(' { SlotId } ')' ;
4.3 IR Example
The contract:
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
effects { pure }
}
Generates IR:
module safe_division {
fn #0 : ($0: Int @omega, $1: Int @omega) -> Int ! pure
pre: cmp ne $1 (const 0)
post: cmp eq (arith add (arith mul $result $1) (arith mod $0 $1)) $0
{
$2 = arith div $0 $1 : Int
$result = load $2 : Int
}
}
4.4 Canonical Serialization Format
The IR has two serialization formats:
- Text format (
.assura-ir): Human-inspectable, used for debugging. The grammar above. - Binary format (
.assura-irb): Compact, used for caching and transmission. MessagePack encoding with a fixed schema.
Both formats are canonicalized:
- Slots are numbered sequentially from
$0 - Instructions are in topological order (definition before use)
- No comments, no whitespace variation
- Identical IR always produces identical bytes
4.5 IR Metadata
Every IR module includes metadata for tracing:
{
"source_contract": "src/contracts/division.assura",
"source_hash": "sha256:abc123...",
"generator": "claude-4/2026-06",
"generation_timestamp": "2026-06-11T20:00:00Z",
"assura_version": "0.4.0",
"verification_status": "unverified"
}
5. Verification Architecture
5.1 Verification Layers
The compiler verifies in layers, fastest first. Each layer runs only if all previous layers pass.
Layer 0: Syntactic / Structural (No SMT, < 10ms)
Checks performed purely by the parser and type checker algorithms:
| Check | Method | Time |
|---|---|---|
| Parse validity | Recursive descent parser | < 1ms |
| Name resolution | Scope analysis | < 1ms |
| Linearity / ownership | Context splitting algorithm | < 5ms |
| Basic typestate | Finite state machine DFA | < 5ms |
| Exhaustive patterns | Coverage checker | < 2ms |
| Effect set containment | Set inclusion | < 1ms |
| Scope/lifetime | Lexical region analysis | < 2ms |
No SMT solver is invoked. These checks are decidable and deterministic.
Layer 1: Lightweight SMT (Decidable, < 200ms)
Checks using quantifier-free decidable SMT theories:
| Check | SMT Theory | Time |
|---|---|---|
| Refinement subtyping | QF_UFLIA (int) or QF_UFLRA (float) | < 50ms |
| Null safety | QF_UFLIA (v != null) | < 10ms |
| Bounds checking | QF_UFLIA (0 <= i < len) | < 50ms |
| Information flow (finite lattice) | QF_DT (enum sort) | < 10ms |
| Grade arithmetic | QF_LIA (semiring) | < 10ms |
| Typestate with data guards | QF_DT + QF_LIA | < 20ms |
| Numerical unit checking | QF_DT (unit compatibility) | < 10ms |
| API compatibility rules | QF_DT (variance) | < 10ms |
All queries in this layer are decidable. The solver always terminates.
Layer 2: Heavy SMT (Undecidable, < 10s, budgeted)
Checks using quantified or nonlinear SMT theories with timeouts:
| Check | SMT Theory | Budget | Fallback |
|---|---|---|---|
| Quantified invariants | AUFLIA | 5s | Property-based test |
| Functional correctness | AUFLIA + UF | 10s | Lemma hint required |
| Termination (complex) | LIA + fuel | 5s | decreases annotation required |
| Serialization roundtrip | AUFLIA + DT | 5s | Runtime check |
| Complexity bounds (AARA) | QF_LIA + LP | 5s | Annotation required |
| Multi-service protocol | HORN | 5s | Bounded model check |
| Crash safety compensation | DT + LIA | 5s | Runtime check |
Each query has a timeout. If the solver times out, the compiler does NOT reject the code. Instead:
- It emits a warning:
W0801: Unable to verify invariant within 5s - It generates a property-based test for the unverified property
- The property is re-checked at Layer 2 if the user runs
assura verify --deep
5.2 Z3 Encoding Strategy
Refinement Types -> SMT
Each refinement subtyping check {v:S | P} <: {v:T | Q} generates:
; Declare the value variable
(declare-const v Int)
; Assert the antecedent (what we know)
(assert P_encoded)
; Assert the negation of the consequent (what we need to prove)
(assert (not Q_encoded))
; Check satisfiability
(check-sat)
; unsat => P implies Q => subtyping holds
; sat => counterexample exists => type error
Measures -> Uninterpreted Functions
measure len : List<T> -> Nat
len([]) = 0
len(_ :: t) = 1 + len(t)
Encodes as:
(declare-fun len (List) Int)
(assert (= (len nil) 0))
(assert (forall ((x T) (xs List))
(= (len (cons x xs)) (+ 1 (len xs)))))
(assert (forall ((xs List)) (>= (len xs) 0)))
Typestate -> Enumeration Datatypes
(declare-datatypes () ((OrderState Created Paid Shipped Delivered Cancelled Error)))
(define-fun valid_transition ((from OrderState) (action Int)) OrderState
(ite (and (= from Created) (= action 0)) Paid ; pay
(ite (and (= from Paid) (= action 1)) Shipped ; ship
(ite (and (= from Shipped) (= action 2)) Delivered ; deliver
Error))))
; Verify sequence: pay then ship
(declare-const s0 OrderState)
(declare-const s1 OrderState)
(assert (= s0 (valid_transition Created 0)))
(assert (= s1 (valid_transition s0 1)))
(assert (= s1 Error))
(check-sat) ; unsat => sequence is valid
Information Flow -> Lattice Constraints
(declare-datatypes () ((Label Public Internal Confidential Restricted)))
(define-fun flows ((from Label) (to Label)) Bool
(or (= from to)
(and (= from Public) (not (= to Public))) ; Public flows anywhere
(and (= from Internal) (or (= to Confidential) (= to Restricted)))
(and (= from Confidential) (= to Restricted))))
; Check: can Restricted data flow to Public log?
(assert (flows Restricted Public))
(check-sat) ; sat would mean it's allowed; expect unsat
Business Invariants -> Quantified Assertions
invariant { forall u in users.values(): u.email.is_valid() }
Encodes as:
(declare-fun users (Int) User)
(declare-fun user_count () Int)
(declare-fun is_valid_email (String) Bool)
(assert (not
(forall ((i Int))
(=> (and (>= i 0) (< i user_count))
(is_valid_email (email (users i)))))))
(check-sat)
; sat => counterexample: specific i where email is invalid
; unsat => invariant holds
5.3 Counterexample Extraction
When Z3 returns sat (property violated), the compiler extracts a
model (concrete values for each variable) and formats it as a
structured counterexample:
{
"error_code": "E0412",
"category": "invariant_violation",
"constraint": "order.total == sum(item.price * item.qty)",
"counterexample": {
"items": [{"price": 10, "qty": 2}, {"price": 5, "qty": 3}],
"expected_total": 35,
"actual_total": 0
},
"model_values": {
"v": 0,
"items.len": 2,
"items[0].price": 10,
"items[0].qty": 2
}
}
The counterexample includes concrete inputs that violate the property, making it actionable for AI to fix.
6. Rust Codegen Mapping
The Assura compiler generates Rust source code. This section defines how each Assura construct maps to Rust.
6.1 Types
| Assura Type | Rust Type | Notes |
|---|---|---|
Int | i64 | |
Nat | u64 | Runtime bounds check on construction |
Float | f64 | |
Bool | bool | |
String | String | |
Bytes | Vec<u8> | |
Unit | () | |
Never | ! (never type) | |
List<T> | Vec<T> | |
Map<K, V> | BTreeMap<K, V> | Deterministic ordering |
Set<T> | BTreeSet<T> | Deterministic ordering |
T? | Option<T> | |
(T, U) | (T, U) |
6.2 Refinement Types
Refinement types erase to their base type at runtime. The predicate is checked at construction in debug mode:
type Pos = {v: Int | v > 0}
Generates:
#![allow(unused)]
fn main() {
/// Assura refinement type: {v: Int | v > 0}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Pos(i64);
impl Pos {
pub fn new(v: i64) -> Result<Self, AssuraRefinementError> {
#[cfg(debug_assertions)]
if !(v > 0) {
return Err(AssuraRefinementError {
type_name: "Pos",
predicate: "v > 0",
actual_value: format!("{}", v),
});
}
Ok(Self(v))
}
pub fn value(&self) -> i64 { self.0 }
}
}
In release mode, the check is elided. The Assura compiler has already proven the predicate holds at every construction site.
6.3 Typestate
Typestate maps to Rust phantom types + the typestate pattern:
type Order<State> where State in {Created, Paid, Shipped}
Generates:
#![allow(unused)]
fn main() {
pub mod order_states {
pub struct Created;
pub struct Paid;
pub struct Shipped;
}
pub struct Order<State> {
// ... fields ...
_state: std::marker::PhantomData<State>,
}
impl Order<order_states::Created> {
pub fn pay(self, payment: Payment) -> Order<order_states::Paid> {
Order {
// ... fields ...
_state: std::marker::PhantomData,
}
}
}
impl Order<order_states::Paid> {
pub fn ship(self, tracking: String) -> Order<order_states::Shipped> {
// ...
}
}
// pay() on Order<Shipped> does not exist -> compile error in Rust too
}
The self parameter consumes the old state (Rust ownership = linear).
The return type has the new state. Rust’s type checker enforces the
state machine as a second safety net.
6.4 Effects
Effects map to Rust traits that bound what capabilities a function receives:
fn save(order: Order) -> OrderId
effects: database.write, log.info
Generates:
#![allow(unused)]
fn main() {
pub fn save<Db: DatabaseWrite, Log: LogInfo>(
db: &mut Db,
log: &Log,
order: Order,
) -> OrderId {
// implementation
}
}
Capability traits are passed explicitly. A function that declares
effects: pure receives no capability parameters and cannot perform
any side effects.
6.5 Linear Types
Linear types map to Rust ownership. A linear parameter is passed by value (moved), ensuring single use:
fn close(f: File<Open> :_1) -> Unit
Generates:
#![allow(unused)]
fn main() {
pub fn close(f: File<Open>) {
// f is moved in, cannot be used after this call
drop(f);
}
}
Rust’s move semantics naturally enforce linearity. The Assura compiler
additionally checks exact usage counts (Rust allows unused variables
with #[allow(unused)]; Assura does not).
6.6 Information Flow Labels
Labels erase at runtime. They exist only in the Assura type checker. The generated Rust code has no label representation:
type SSN = String @Restricted
Generates:
#![allow(unused)]
fn main() {
pub type SSN = String; // label erased
}
The Assura compiler has already verified that no Restricted data
flows to Public sinks. The Rust code does not need to re-check.
6.7 Contracts (requires/ensures)
In debug mode, contracts become runtime assertions. In release mode, they are elided:
fn divide(a: Int, b: Int) -> Int
requires { b != 0 }
ensures { result * b + (a % b) == a }
Generates:
#![allow(unused)]
fn main() {
pub fn divide(a: i64, b: i64) -> i64 {
debug_assert!(b != 0, "Assura requires: b != 0");
let result = a / b;
debug_assert!(
result * b + (a % b) == a,
"Assura ensures: result * b + (a % b) == a"
);
result
}
}
6.8 Measures
Measures generate pure Rust functions used only in debug assertions:
measure len : List<T> -> Nat
Generates:
#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
fn assura_measure_len<T>(list: &[T]) -> u64 {
list.len() as u64
}
}
6.9 Dependent Indices
Dependent indices erase completely. Vec<T, n> becomes Vec<T>:
fn append(a: Vec<T, n>, b: Vec<T, m>) -> Vec<T, n + m>
Generates:
#![allow(unused)]
fn main() {
pub fn append<T>(mut a: Vec<T>, b: Vec<T>) -> Vec<T> {
a.extend(b);
a
}
}
The index arithmetic (n + m) was verified at compile time. The Rust
code is a plain Vec with no runtime size tracking.
6.10 Extern and Bind
extern functions generate a trait that the hand-written Rust code
must implement:
extern fn render_template(name: String, data: Map<String, Value>) -> Html
effects: filesystem.read
Generates:
#![allow(unused)]
fn main() {
pub trait RenderTemplateExtern {
fn render_template(
&self,
name: String,
data: BTreeMap<String, Value>,
) -> Html;
}
}
bind functions generate wrapper functions with debug assertions
around the bound Rust function:
#![allow(unused)]
fn main() {
pub fn render_page_checked(template: String, user: User) -> Html {
debug_assert!(!template.is_empty(), "Assura requires: template.length > 0");
let result = app::renderer::render_page(template, user.clone());
debug_assert!(
result.contains(&user.name),
"Assura ensures: result.contains(user.name)"
);
debug_assert!(
!result.contains(&user.password),
"Assura ensures: not result.contains(user.password)"
);
result
}
}
7. Error Code Catalog
Error codes are stable identifiers for AI fix-pattern databases. The
format is ANNSSS where A = Assura, NN = category (2 digits),
SSS = specific error (3 digits).
7.1 Category Index
| Code Range | Category | Verification Layer |
|---|---|---|
| A01xxx | Syntax errors | 0 (parser) |
| A02xxx | Name resolution | 0 (scope) |
| A03xxx | Type mismatch | 0 (type checker) |
| A04xxx | Refinement violation | 1 (SMT) |
| A05xxx | Linearity / ownership | 0 (algorithmic) |
| A06xxx | Typestate violation | 0 (DFA) |
| A07xxx | Effect violation | 0 (set inclusion) |
| A08xxx | Information flow | 1 (SMT lattice) |
| A09xxx | Totality / termination | 0-2 |
| A10xxx | Pattern exhaustiveness | 0 (coverage) |
| A11xxx | Business invariant | 1-2 (SMT) |
| A12xxx | Concurrency | 0 (algorithmic) |
| A13xxx | Numerical precision | 0-1 |
| A14xxx | Temporal ordering | 0 (typestate) |
| A15xxx | Idempotency | 0 (linearity) |
| A16xxx | Privacy / purpose | 0-1 |
| A17xxx | Schema evolution | 0 (structural) |
| A18xxx | Crash safety | 1 (SMT) |
| A19xxx | Audit trail | 0 (effect pairing) |
| A20xxx | Serialization | 2 (SMT) |
| A21xxx | API evolution | 0 (variance) |
| A22xxx | Complexity bounds | 2 (AARA/LP) |
| A23xxx | Protocol violation | 1-2 (session types) |
| A24xxx | Observability | 0 (effect pairing) |
| A25xxx | Regulatory compliance | 1-2 (SMT) |
| A26xxx | i18n completeness | 0 (structural) |
| A27xxx | Module / import | 0 (scope) |
7.2 Specific Error Codes
Syntax (A01xxx)
| Code | Message | Cause |
|---|---|---|
| A01001 | Unexpected token | Parser error |
| A01002 | Unterminated string literal | Missing closing quote |
| A01003 | Invalid numeric literal | Malformed number |
| A01004 | Reserved keyword used as identifier | Naming conflict |
| A01005 | Mismatched braces | Unbalanced {} |
Name Resolution (A02xxx)
| Code | Message | Cause |
|---|---|---|
| A02001 | Undefined identifier X | Name not in scope |
| A02002 | Undefined type X | Type not declared |
| A02003 | Duplicate definition of X | Name collision |
| A02004 | Ambiguous import X | Multiple modules export same name |
| A02005 | Circular import | Module A imports B imports A |
Type Mismatch (A03xxx)
| Code | Message | Cause |
|---|---|---|
| A03001 | Expected T1, found T2 | Incompatible types |
| A03002 | Type parameter count mismatch | Wrong number of generics |
| A03003 | Cannot unify T1 with T2 | Failed unification |
| A03004 | Missing field F in struct | Incomplete construction |
| A03005 | Unknown field F in type T | Field does not exist |
| A03006 | Dependent index mismatch | Vec<T, 3> vs Vec<T, 5> |
Refinement Violation (A04xxx)
| Code | Message | Cause |
|---|---|---|
| A04001 | Precondition may not hold | requires clause violated |
| A04002 | Postcondition may not hold | ensures clause violated |
| A04003 | Refinement subtype check failed | {v: T | P} not subtype |
| A04004 | Division by zero possible | Divisor may be 0 |
| A04005 | Index out of bounds possible | Index may exceed length |
| A04006 | Arithmetic overflow possible | Result may exceed bounds |
| A04007 | Refinement timeout | SMT solver timed out |
Linearity (A05xxx)
| Code | Message | Cause |
|---|---|---|
| A05001 | Linear variable X used twice | Grade 1, used 2+ times |
| A05002 | Linear variable X not used | Grade 1, never consumed |
| A05003 | Grade mismatch: expected N, used M | Exact count violated |
| A05004 | Cannot copy linear value | Tried to duplicate |
| A05005 | Linear value dropped without consuming | Resource leak |
Typestate (A06xxx)
| Code | Message | Cause |
|---|---|---|
| A06001 | Invalid transition: S1 -> S2 | Not in state machine |
| A06002 | Operation requires state S, found S' | Wrong current state |
| A06003 | Object not in final state at end of scope | Protocol incomplete |
| A06004 | Ambiguous state after branch | Different states in if/else |
| A06005 | Missing transition guard | Required predicate missing |
Effect Violation (A07xxx)
| Code | Message | Cause |
|---|---|---|
| A07001 | Undeclared effect E | Effect not in function signature |
| A07002 | Pure function performs effect E | Side effect in pure context |
| A07003 | Effect E in must-not list | Explicitly forbidden effect |
| A07004 | Effect handler missing for E | Unhandled effect |
| A07005 | Effect hierarchy violation | Sub-effect used but parent not declared |
Information Flow (A08xxx)
| Code | Message | Cause |
|---|---|---|
| A08001 | Data flow violation: L1 to L2 | High to low flow |
| A08002 | PII leaked to logs | Restricted data in Public sink |
| A08003 | Implicit flow via branch | Secret in branch condition |
| A08004 | Purpose violation | Data used for undeclared purpose |
| A08005 | Missing declassification | Label downgrade without declassify |
Totality (A09xxx)
| Code | Message | Cause |
|---|---|---|
| A09001 | Non-exhaustive pattern match | Missing cases |
| A09002 | Recursion may not terminate | No decreasing measure |
| A09003 | Decreasing measure not well-founded | Measure does not decrease |
| A09004 | Partial function called from total context | Missing trust |
Business Invariant (A11xxx)
| Code | Message | Cause |
|---|---|---|
| A11001 | Invariant violated | SMT found counterexample |
| A11002 | Invariant not preserved by operation | Mutation breaks invariant |
| A11003 | Invariant verification timeout | SMT solver timed out |
| A11004 | Rule clause violated | Business rule not satisfied |
Concurrency (A12xxx)
| Code | Message | Cause |
|---|---|---|
| A12001 | Exclusive resource accessed concurrently | Data race possible |
| A12002 | Actor isolation violated | Cross-actor mutable access |
| A12003 | Shared-read resource modified | Write in shared-read context |
Numerical Precision (A13xxx)
| Code | Message | Cause |
|---|---|---|
| A13001 | Unit mismatch: U1 vs U2 | e.g., USD + EUR |
| A13002 | Dimensionally invalid operation | e.g., Money * Money |
| A13003 | Float used where fixed-point required | Precision loss |
| A13004 | Integer overflow possible | Arithmetic exceeds bounds |
Privacy (A16xxx)
| Code | Message | Cause |
|---|---|---|
| A16001 | Purpose violation | Data used outside declared purposes |
| A16002 | Retention policy missing | No retention declared for PII |
| A16003 | Anonymization required | Retention period expired |
Schema Evolution (A17xxx)
| Code | Message | Cause |
|---|---|---|
| A17001 | Breaking field removal | Required field removed |
| A17002 | Missing default for new field | Non-optional field added |
| A17003 | Type change without migration | Incompatible field type change |
API Evolution (A21xxx)
| Code | Message | Cause |
|---|---|---|
| A21001 | Breaking response field removal | Client may depend on field |
| A21002 | New required request field | Existing clients will fail |
| A21003 | Error variant removed | Client handlers break |
Complexity Bounds (A22xxx)
| Code | Message | Cause |
|---|---|---|
| A22001 | Exceeds declared complexity | O(n^2) found, O(n) declared |
| A22002 | Complexity analysis timeout | AARA solver timed out |
| A22003 | Unbounded allocation detected | No allocation bound proved |
7.3 Error Output Format
Every error is emitted as structured JSON:
{
"error_code": "A05001",
"severity": "error",
"category": "linearity",
"message": "Linear variable `conn` used twice",
"primary_location": {
"file": "src/db.assura-impl",
"line": 23,
"col": 5,
"instruction_index": 14
},
"secondary_locations": [
{
"file": "src/db.assura-impl",
"line": 27,
"col": 5,
"instruction_index": 18,
"label": "second use here"
}
],
"contract_reference": {
"file": "contracts/db.assura",
"line": 8,
"clause": "conn :_1 DbConnection"
},
"suggested_fixes": [
{
"description": "Clone conn before second use",
"confidence": 0.4,
"note": "Cloning a linear resource defeats its purpose"
},
{
"description": "Split operations into separate functions",
"confidence": 0.85,
"replacement_hint": "extract second use into a new function"
}
],
"related_errors": [],
"documentation_url": "https://assura-lang.github.io/assura/error-codes.html"
}
8. Module System
8.1 Module Declaration
Every .assura file is a module. The module name matches the file
path:
contracts/
payment/
order.assura -> module payment.order
refund.assura -> module payment.refund
user.assura -> module user
8.2 Import Rules
import payment.order -- import entire module
import payment.order { Order, Item } -- import specific names
import payment.order as po -- aliased import
Visibility
By default, all declarations are module-private. Use pub to export:
pub type Order { ... } -- exported
type InternalHelper { ... } -- private to module
pub contract PlaceOrder { ... } -- exported
8.3 Contract Composition
Contracts can extend other contracts:
contract BaseEntity {
ensures { self.id != "" }
ensures { self.created_at <= self.updated_at }
}
contract Order extends BaseEntity {
-- inherits BaseEntity's ensures clauses
ensures { self.total >= 0 }
}
8.4 Service Composition
Services can depend on other services:
service OrderService {
depends: PaymentService, InventoryService
operation PlaceOrder {
-- can reference PaymentService.charge and InventoryService.reserve
}
}
8.5 Contract Libraries
Reusable contract patterns are published as packages:
import assura.std.crud { CrudService }
import assura.std.auth { Authenticated, Authorized }
service UserService extends CrudService<User> {
-- inherits Create, Read, Update, Delete operations
-- with standard contracts for each
}
9. Standard Library
9.1 Core Types
module assura.core
-- Primitive types are built-in: Int, Nat, Float, Bool, String,
-- Bytes, Unit, Never
-- Standard refinement aliases
pub type Pos = {v: Int | v > 0}
pub type NonNeg = {v: Int | v >= 0}
pub type NonZero = {v: Int | v != 0}
pub type Percentage = {v: Float | 0.0 <= v and v <= 1.0}
-- Constrained strings
pub type NonEmpty = {v: String | len(v) > 0}
pub type Email = {v: String | is_email(v)}
pub type Url = {v: String | is_url(v)}
pub type Uuid = {v: String | is_uuid(v)}
-- Time
pub type Timestamp = {v: Int | v > 0} -- Unix epoch millis
pub type Duration = {v: Int | v >= 0} -- milliseconds
pub type Date = { year: Int, month: Btwn<1,13>, day: Btwn<1,32> }
9.2 Collection Contracts
module assura.collections
pub measure len<T> : List<T> -> Nat
pub measure elems<T> : List<T> -> Set<T>
pub measure keys<K,V> : Map<K,V> -> Set<K>
pub measure values<K,V> : Map<K,V> -> List<V>
pub measure size<T> : Set<T> -> Nat
pub contract ListOps<T> {
operation head {
input(list: NonEmpty<List<T>>)
output(result: T)
ensures { result in elems(list) }
effects { pure }
}
operation append {
input(a: List<T>, b: List<T>)
output(result: List<T>)
ensures { len(result) == len(a) + len(b) }
ensures { elems(result) == elems(a) ++ elems(b) }
effects { pure }
}
operation filter {
input(list: List<T>, pred: (T) -> Bool)
output(result: List<T>)
ensures { len(result) <= len(list) }
ensures { forall x in result: x in elems(list) }
ensures { forall x in result: pred(x) == true }
effects { pure }
}
operation sort {
input(list: List<T>)
output(result: List<T>)
requires { T has Ord }
ensures { len(result) == len(list) }
ensures { elems(result) == elems(list) }
ensures { forall i in 0..len(result)-1:
result[i] <= result[i+1] }
effects { pure }
}
}
9.3 Numerical Types
module assura.numeric
pub type FixedDecimal<Scale: Nat, Unit: Label>
pub type USD
pub type EUR
pub type GBP
pub type Percent
pub type Money<C> = FixedDecimal<2, C>
pub contract NumericOps {
operation add<S, U> {
input(a: FixedDecimal<S, U>, b: FixedDecimal<S, U>)
output(result: FixedDecimal<S, U>)
ensures { result == a + b }
effects { pure }
}
operation scale<S, U> {
input(amount: FixedDecimal<S, U>, factor: Float)
output(result: FixedDecimal<S, U>)
effects { pure }
}
}
9.4 Common Contract Patterns
module assura.std.crud
pub service CrudService<Entity> {
type Id = Uuid
states: Draft -> Active -> Archived -> Deleted
operation Create {
input(data: Entity)
output(id: Id)
ensures { self.store.contains(id) }
ensures { self.store[id] == data }
effects { database.write }
}
operation Read {
input(id: Id)
output(entity: Entity)
requires { self.store.contains(id) }
effects { database.read }
}
operation Update {
input(id: Id, data: Entity)
requires { self.store.contains(id) }
ensures { self.store[id] == data }
effects { database.write }
}
operation Delete {
input(id: Id)
requires { self.store.contains(id) }
ensures { not self.store.contains(id) }
effects { database.write }
}
invariant { forall id in self.store.keys():
self.store[id].is_valid() }
}
9.5 Auth Contracts
module assura.std.auth
pub type Role = enum { Admin, User, ReadOnly, Service }
pub type Principal = { id: Uuid, roles: Set<Role> }
pub contract Authenticated {
requires { self.principal != null }
requires { self.principal.is_authenticated() }
}
pub contract Authorized<R: Role> {
extends Authenticated
requires { R in self.principal.roles }
}
10. CLI Interface
10.1 Commands
assura <command> [options] [files...]
COMMANDS:
check Type-check contracts and implementations (layers 0-1)
verify Full verification including SMT (layers 0-2)
build Check + generate Rust code + compile
run Build + execute
init Create a new Assura project
fmt Format contract files
lsp Start the language server
ir Inspect generated IR
explain Show detailed explanation of an error code
OPTIONS (global):
--json Output diagnostics as JSON (default for AI)
--human Output diagnostics as human-readable text
--color Force color output
--no-color Disable color output
-q, --quiet Only output errors
-v, --verbose Show verification details
--threads <N> Number of parallel verification threads
10.2 Command Details
assura check
assura check [options] [files...]
Runs layers 0 and 1 (structural checks + decidable SMT).
Fast feedback loop for AI iteration.
OPTIONS:
--layer <N> Run only up to layer N (0, 1, or 2)
--contract <file> Check only this contract
--watch Watch for file changes and re-check
--timeout <ms> SMT solver timeout per query (default: 1000)
assura verify
assura verify [options] [files...]
Runs all layers including layer 2 (heavy SMT).
Used for pre-commit verification.
OPTIONS:
--deep Enable maximum verification depth
--timeout <ms> SMT solver timeout per query (default: 10000)
--fuel <N> Recursion unfolding depth (default: 5)
--solver <name> SMT solver: z3 (default), cvc5
--stats Show verification statistics
--dump-smt <dir> Write SMT queries to directory for debugging
assura build
assura build [options]
Verify + generate Rust code + compile with rustc.
OPTIONS:
--release Optimize generated Rust (release mode)
--target <triple> Rust target triple (e.g., wasm32-wasi)
--out <dir> Output directory (default: target/)
--skip-verify Skip layer 2 (only layers 0-1), for dev speed
--keep-generated Don't delete generated .rs files after build
assura init
assura init [name]
Create a new Assura project.
Generated structure:
<name>/
assura.toml # project configuration
contracts/
main.assura # initial contract file
src/
main.rs # hand-written Rust entry point
Cargo.toml # workspace manifest
assura explain
assura explain <error_code>
Show detailed explanation of an error code with examples.
$ assura explain A05001
Error A05001: Linear variable used twice
Category: Linearity (Layer 0)
A variable with linear grade (exactly-once usage) was used more
than once. Linear types ensure resources like database connections,
file handles, and tokens are consumed exactly once.
Example:
fn bad(conn :_1 DbConnection) -> (Result, Result) {
(query(conn, "SELECT 1"), query(conn, "SELECT 2"))
} ^^^^^ ^^^^^
First use here Second use here (ERROR)
Fix: Split into separate functions, each receiving its own connection.
10.3 Configuration File
assura.toml at the project root:
[project]
name = "my-service"
version = "0.1.0"
assura-version = "0.1"
[contracts]
path = "contracts/"
[build]
target = "native" # or "wasm32-wasi"
generated-dir = "generated/"
keep-generated = false
[verify]
default-layer = 1 # default for `assura check`
smt-solver = "z3"
smt-timeout-ms = 5000
fuel = 5
[effects]
[effects.custom]
"audit.write" = { label = "Internal" }
"email.send" = { label = "Public" }
[security]
labels = ["Public", "Internal", "Confidential", "Restricted"]
default-label = "Internal"
11. AI Agent API
The AI agent API is how AI systems interact with the Assura compiler programmatically. It supports both gRPC and JSON-over-HTTP.
11.1 API Operations
Check
POST /v1/check
Request:
{
"contracts": [
{
"path": "contracts/order.assura",
"content": "service OrderService { ... }"
}
],
"implementations": [
{
"path": "src/order.assura-ir",
"content": "module order { fn #0 ... }"
}
],
"options": {
"max_layer": 2,
"smt_timeout_ms": 5000,
"fuel": 5
}
}
Response:
{
"status": "error",
"diagnostics": [ ... ],
"statistics": {
"layer_0_time_ms": 12,
"layer_1_time_ms": 87,
"layer_2_time_ms": 2340,
"smt_queries": 15,
"smt_sat": 1,
"smt_unsat": 14,
"smt_timeout": 0
}
}
Build
POST /v1/build
Request:
{
"contracts": [ ... ],
"implementations": [ ... ],
"target": "wasm32-wasi",
"release": true
}
Response:
{
"status": "success",
"artifacts": [
{
"path": "target/order.wasm",
"size_bytes": 45230,
"content_base64": "AGFzbQEA..."
}
]
}
Explain
GET /v1/explain/A05001
Response:
{
"error_code": "A05001",
"category": "linearity",
"layer": 0,
"title": "Linear variable used twice",
"description": "...",
"examples": [ ... ],
"fix_patterns": [ ... ]
}
Health
GET /v1/health
Response:
{
"status": "healthy",
"version": "0.1.0",
"smt_solver": "z3",
"smt_solver_version": "4.13.0"
}
11.2 gRPC Service Definition
syntax = "proto3";
package assura.v1;
service AssuraCompiler {
rpc Check(CheckRequest) returns (CheckResponse);
rpc Build(BuildRequest) returns (BuildResponse);
rpc Explain(ExplainRequest) returns (ExplainResponse);
rpc Health(HealthRequest) returns (HealthResponse);
rpc CheckStream(stream CheckRequest) returns (stream Diagnostic);
}
message CheckRequest {
repeated SourceFile contracts = 1;
repeated SourceFile implementations = 2;
CheckOptions options = 3;
}
message SourceFile {
string path = 1;
string content = 2;
string hash = 3;
}
message CheckOptions {
int32 max_layer = 1;
int32 smt_timeout_ms = 2;
int32 fuel = 3;
string solver = 4;
}
message CheckResponse {
Status status = 1;
repeated Diagnostic diagnostics = 2;
VerificationStats statistics = 3;
}
enum Status {
STATUS_UNKNOWN = 0;
STATUS_OK = 1;
STATUS_ERROR = 2;
STATUS_WARNING = 3;
}
message Diagnostic {
string error_code = 1;
Severity severity = 2;
string category = 3;
string message = 4;
Location primary_location = 5;
repeated Location secondary_locations = 6;
ContractReference contract_reference = 7;
Counterexample counterexample = 8;
repeated SuggestedFix suggested_fixes = 9;
}
enum Severity {
SEVERITY_UNKNOWN = 0;
SEVERITY_ERROR = 1;
SEVERITY_WARNING = 2;
SEVERITY_INFO = 3;
SEVERITY_HINT = 4;
}
message Location {
string file = 1;
int32 line = 2;
int32 col = 3;
int32 end_line = 4;
int32 end_col = 5;
string label = 6;
string source_text = 7;
}
message ContractReference {
string file = 1;
int32 line = 2;
string clause = 3;
}
message Counterexample {
string constraint = 1;
map<string, string> inputs = 2;
string expected = 3;
string actual = 4;
}
message SuggestedFix {
string description = 1;
float confidence = 2;
string replacement = 3;
}
message VerificationStats {
int32 layer_0_ms = 1;
int32 layer_1_ms = 2;
int32 layer_2_ms = 3;
int32 smt_queries = 4;
int32 smt_sat = 5;
int32 smt_unsat = 6;
int32 smt_timeout = 7;
}
message BuildRequest {
repeated SourceFile contracts = 1;
repeated SourceFile implementations = 2;
string target = 3;
bool release = 4;
}
message BuildResponse {
Status status = 1;
repeated Artifact artifacts = 2;
CheckResponse verification = 3;
}
message Artifact {
string path = 1;
int64 size_bytes = 2;
bytes content = 3;
}
message ExplainRequest {
string error_code = 1;
}
message ExplainResponse {
string error_code = 1;
string category = 2;
int32 layer = 3;
string title = 4;
string description = 5;
repeated string examples = 6;
repeated string fix_patterns = 7;
}
message HealthRequest {}
message HealthResponse {
string status = 1;
string version = 2;
string smt_solver = 3;
string smt_solver_version = 4;
}
11.3 Streaming Mode
For AI iteration loops, the CheckStream RPC accepts a stream of
incremental submissions. The AI sends a revised implementation, and
the compiler streams back diagnostics as they are discovered (layer 0
results first, then layer 1, then layer 2). This enables the AI to
start fixing layer 0 errors while layer 2 verification is still
running.
12. Decidability Boundaries
This section defines exactly which checks are decidable (always terminate), semidecidable (may not terminate but have practical mitigations), and undecidable (require timeouts).
12.1 Decidability Map
| Check | SMT Logic | Decidable | Layer | Budget |
|---|---|---|---|---|
| Linearity / ownership | None (algorithmic) | Yes | 0 | N/A |
| Basic typestate (finite) | None (DFA) | Yes | 0 | N/A |
| Pattern exhaustiveness | None (coverage) | Yes | 0 | N/A |
| Effect set inclusion | None (set ops) | Yes | 0 | N/A |
| Scope / lifetime | None (regions) | Yes | 0 | N/A |
| Refinement (quantifier-free) | QF_UFLIA | Yes | 1 | 1s |
| Info flow (finite lattice) | QF_DT | Yes | 1 | 1s |
| Grade arithmetic | QF_LIA | Yes | 1 | 1s |
| Unit/dimension checking | QF_DT | Yes | 1 | 1s |
| API variance | QF_DT | Yes | 1 | 1s |
| Typestate with data guards | QF_DT + QF_LIA | Yes | 1 | 1s |
| Quantified invariants | AUFLIA | No | 2 | 5s |
| Functional correctness | AUFLIA + UF | No | 2 | 10s |
| Complexity bounds | QF_LIA + LP | Yes | 2 | 5s |
| Termination (complex) | LIA + fuel | Semidecidable | 2 | 5s |
| Serialization roundtrip | AUFLIA + DT | No | 2 | 5s |
| Multi-service protocol | HORN | Semidecidable | 2 | 5s |
| Noninterference proof | FOL + quantifiers | No | 2 | 10s |
12.2 Dangerous Combinations
These combinations are known to cause solver instability:
-
Quantified refinements + recursive measures: Can trigger unbounded MBQI. Mitigation: limit fuel, use E-matching triggers.
-
Nonlinear integer arithmetic (NIA): Undecidable. Avoid refinements with multiplication of two variables (
x * y > z). Allow constant multiplication (x * 3 > z, which is LIA). -
Array theory + quantifiers: Can cause exponential blowup. Mitigation: use bounded quantification (
forall i in 0..n). -
Bitvector arithmetic + quantifiers: Decidable but exponential in bit-width. Avoid mixing BV with quantified formulas.
12.3 Timeout Strategy
When the SMT solver times out:
-
Layer 2 timeout: Emit warning, not error. Code compiles. Unverified property is flagged.
-
Layer 1 timeout: Emit error suggesting simplified predicate.
-
Persistent timeout: Suggest lemma hints, property splitting,
trustannotation, or property-based test fallback.
12.4 Trust Escape Hatch
trust "manually reviewed: hash collision resistance"
invariant { forall a, b: if hash(a) == hash(b) then a == b }
Trust annotations:
- Require a justification string
- Are logged in the verification report
- Are flagged in security audits
- Cannot bypass layer 0 checks (syntax, types, linearity)
12.5 Verification Budget Configuration
[verify.budgets]
layer_0_timeout_ms = 100
layer_1_timeout_ms = 1000
layer_2_timeout_ms = 10000
total_timeout_ms = 300000
max_fuel = 10
max_smt_queries = 1000
13. Type Interaction Test Cases
The six type features (refinement, dependent, linear, typestate, effect, information flow) are each well-understood in isolation. The hard problem is their composition. This section defines concrete use cases that stress-test each pairwise and higher-order interaction, identifies what can go wrong, and specifies the expected compiler behavior.
There are C(6,2) = 15 pairwise interactions. We cover all 15, plus 4 three-way and 2 full-stack interactions, for 21 total test cases.
Test Case 1: Refinement + Linear (Ghost Use Problem)
Scenario: A refinement predicate references a linear variable.
fn transfer(
from: Account :_1,
to: Account :_1,
amount: {v: Money<USD> | v > 0 and v <= from.balance}
) -> (Account, Account)
effects: database.write
The problem: The refinement v <= from.balance “reads” from.
Does this count as a use for linearity purposes? If yes, from is
used twice (once in the refinement, once in the body). If no, the
type checker must distinguish ghost/logical uses from computational
uses.
Required behavior: Refinement predicates are ghost (logical, not computational). They do NOT count as a linear use. The grade of a variable in a refinement context is always 0 (erased). The type checker must track two usage contexts:
Delta_computational: tracks runtime uses (must satisfy grades)Delta_logical: tracks refinement uses (always grade 0, unlimited)
Expected result: This code compiles. from is used once
computationally (in the body) and once logically (in the refinement).
The logical use is free.
Test:
-- MUST COMPILE: refinement use is ghost
fn test1(x: Int :_1, y: {v: Int | v < x}) -> Int
effects: pure
{ x + y }
-- MUST REJECT: x used twice computationally
fn test1_bad(x: Int :_1, y: {v: Int | v < x}) -> (Int, Int)
effects: pure
{ (x, x) }
Test Case 2: Refinement + Typestate (Guarded Transitions)
Scenario: A state transition whose validity depends on a refinement predicate over the object’s data.
service LoanService {
type Loan<State> {
amount: Money<USD>,
credit_score: Nat,
approved_amount: Money<USD>?
}
where State in {Applied, UnderReview, Approved, Denied, Disbursed}
fn review(loan: Loan<Applied> :_1) -> Loan<UnderReview> :_1
effects: database.write
fn approve(
loan: Loan<UnderReview> :_1,
approved: {v: Money<USD> | v > 0 and v <= loan.amount}
) -> Loan<Approved> :_1
requires { loan.credit_score >= 650 }
effects: database.write
fn deny(loan: Loan<UnderReview> :_1) -> Loan<Denied> :_1
effects: database.write
fn disburse(loan: Loan<Approved> :_1) -> Loan<Disbursed> :_1
requires { loan.approved_amount is Some }
effects: payment.charge, database.write
}
The problem: The approve transition has BOTH a typestate
requirement (must be UnderReview) AND a refinement requirement
(credit_score >= 650). The type checker must verify both
independently: typestate via DFA, refinement via SMT. But what
happens when the refinement references state-dependent data?
Interaction rule: Typestate checking happens BEFORE refinement checking in the same layer 0 pass. The typestate check confirms the transition is valid. Then the refinement check confirms the data guard. If typestate fails, refinement is not checked (no point).
Test:
-- MUST REJECT A06002: wrong state (Applied, need UnderReview)
fn bad1(loan: Loan<Applied> :_1) -> Loan<Approved> :_1
{ approve(loan, Money.new(1000)) }
-- MUST REJECT A04001: credit_score may be < 650
fn bad2(loan: Loan<UnderReview> :_1) -> Loan<Approved> :_1
{ approve(loan, Money.new(1000)) }
-- MUST COMPILE: both state and refinement satisfied
fn good(
loan: Loan<UnderReview> :_1
) -> Loan<Approved> :_1
requires { loan.credit_score >= 700 }
{ approve(loan, Money.new(loan.amount.value())) }
Test Case 3: Refinement + Dependent (Index Arithmetic)
Scenario: A function that splits a vector at a refined index, producing two vectors whose lengths must add up.
fn split_at<T>(
v: Vec<T, n>,
i: {x: Nat | x <= n}
) -> (Vec<T, i>, Vec<T, n - i>)
effects: pure
ensures { len(result.0) + len(result.1) == n }
The problem: The return type (Vec<T, i>, Vec<T, n - i>) uses
i as both a refinement-checked value AND a dependent index. The
type checker must:
- Verify
i <= n(refinement, SMT) - Compute
n - ias a type-level index (dependent types) - Verify
i + (n - i) == n(ensures clause, SMT)
Interaction rule: Refined values that appear in dependent positions are first checked for their refinement predicate, then their value is lifted into the index domain. The SMT solver sees both the refinement constraint and the index arithmetic in the same query.
Test:
-- MUST COMPILE: i is within bounds, arithmetic checks out
fn test3() -> (Vec<Int, 3>, Vec<Int, 2>)
effects: pure
{
let v: Vec<Int, 5> = [1, 2, 3, 4, 5]
split_at(v, 3)
}
-- MUST REJECT A04005: index may exceed length
fn bad3<T>(v: Vec<T, n>, i: Nat) -> (Vec<T, i>, Vec<T, n - i>)
effects: pure
{ split_at(v, i) }
-- i has no upper bound refinement
Test Case 4: Linear + Effect (Resource-Scoped Effects)
Scenario: A database transaction where the connection is linear and effects are scoped to the connection’s lifetime.
fn with_transaction<T>(
conn: DbConnection :_1,
body: (TxHandle :_1) -> <database.write, database.read> T
) -> T
effects: database.write, database.read
ensures { conn is consumed }
ensures { transaction is committed or rolled back }
The problem: The body closure captures a TxHandle that is
linear (must be committed or rolled back exactly once). The effect
row of the closure must be a SUBSET of the effects declared by
with_transaction. And the linear handle must be consumed inside
the closure, not leaked out.
Interaction rule: When type-checking a closure:
- The closure’s effect row must be a subset of the enclosing function’s effect row
- Linear variables captured by the closure must be consumed within it (they cannot escape)
- The closure itself may be linear (called exactly once)
Test:
-- MUST COMPILE: handle consumed, effects match
fn good4(conn: DbConnection :_1) -> Order
effects: database.write, database.read
{
with_transaction(conn, fn(tx: TxHandle :_1) -> Order {
let order = tx.insert(new_order) -- database.write
tx.commit() -- consumes tx
order
})
}
-- MUST REJECT A05002: tx not consumed (never committed)
fn bad4a(conn: DbConnection :_1) -> Order
effects: database.write, database.read
{
with_transaction(conn, fn(tx: TxHandle :_1) -> Order {
let order = tx.insert(new_order)
order
-- tx dropped without commit or rollback!
})
}
-- MUST REJECT A07001: network effect not in closure's allowed set
fn bad4b(conn: DbConnection :_1) -> Order
effects: database.write, database.read
{
with_transaction(conn, fn(tx: TxHandle :_1) -> Order {
let data = http.get("http://example.com") -- network effect!
tx.commit()
data
})
}
Test Case 5: Typestate + Information Flow (Label Transitions)
Scenario: A medical record system where reviewing a record changes its security label.
service MedicalRecords {
type Record<State> {
patient_name: String @Confidential,
diagnosis: String @Restricted,
summary: String @Internal
}
where State in {Draft, InReview, Approved, Published}
-- Draft records are fully restricted
fn submit_for_review(r: Record<Draft> :_1)
-> Record<InReview> :_1
effects: database.write
-- all fields remain at their original labels
-- Approving declassifies the summary to Public
fn approve(r: Record<InReview> :_1)
-> Record<Approved> :_1
effects: database.write, audit.write
ensures { result.summary @Public }
declassify { r.summary to @Public }
-- Publishing requires the summary to be public
fn publish(r: Record<Approved> :_1)
-> Record<Published> :_1
requires { r.summary @Public }
effects: database.write, network.send
}
The problem: The approve transition does TWO things: changes
the typestate (InReview -> Approved) AND changes the information flow
label of a field (summary from @Internal to @Public via explicit
declassification). The type checker must:
- Track typestate transitions (DFA)
- Track information flow labels per field
- Verify that declassification is explicit
- Verify that
publishcan only be called when summary is @Public
Interaction rule: Typestate and information flow labels are
tracked in separate contexts (Sigma for state, Lambda for labels).
A state transition may include declassify clauses that update the
label context. The label change is only valid inside a declassify
block.
Test:
-- MUST COMPILE: full valid pipeline
fn good5(r: Record<Draft> :_1) -> Record<Published> :_1
effects: database.write, audit.write, network.send
{
let r1 = submit_for_review(r)
let r2 = approve(r1) -- declassifies summary
publish(r2) -- summary is now Public, OK
}
-- MUST REJECT A08001: diagnosis is Restricted, can't go to Public
fn bad5(r: Record<Approved> :_1) -> String @Public
effects: pure
{ r.diagnosis }
-- MUST REJECT A06001: can't publish from InReview (skip approve)
fn bad5b(r: Record<InReview> :_1) -> Record<Published> :_1
effects: database.write, network.send
{ publish(r) }
Test Case 6: Dependent + Effect (Sized IO)
Scenario: A function that reads exactly n bytes from a stream,
where n is a dependent index.
fn read_exact(
stream: InputStream :_omega,
n: Nat
) -> Vec<Byte, n>
effects: io.read
ensures { len(result) == n }
fn read_header(stream: InputStream :_omega) -> Header
effects: io.read
{
let magic: Vec<Byte, 4> = read_exact(stream, 4)
requires { magic == [0x89, 0x50, 0x4E, 0x47] } -- PNG magic
let length_bytes: Vec<Byte, 4> = read_exact(stream, 4)
let length: Nat = parse_u32(length_bytes)
let data: Vec<Byte, length> = read_exact(stream, length)
-- `length` is a runtime value used as a dependent index!
Header { magic, length, data }
}
The problem: length is a runtime value obtained from IO, but
it’s used as a dependent type index in Vec<Byte, length>. The
type checker must:
- Accept
lengthas a valid Nat index (it came fromparse_u32, which returns Nat) - Track that
read_exact(stream, length)returnsVec<Byte, length> - NOT try to statically verify the exact value of
length(it’s runtime-determined)
Interaction rule: Dependent indices that come from effectful
computations are treated as abstract at the type level. The type
checker knows length: Nat but not its value. Index arithmetic
(n + m) works abstractly. Refinement predicates on the index can
be verified only at runtime (they generate debug_assert).
Test:
-- MUST COMPILE: abstract index from IO
fn test6(stream: InputStream :_omega) -> Vec<Byte, n>
effects: io.read
{
let n: Nat = read_u32(stream)
read_exact(stream, n)
}
-- MUST REJECT A03006: index mismatch (4 != 8)
fn bad6() -> Vec<Byte, 8>
effects: pure
{
let v: Vec<Byte, 4> = [1, 2, 3, 4]
v -- returns Vec<Byte, 4> but declared Vec<Byte, 8>
}
Test Case 7: Linear + Information Flow (Secret Key Protocol)
Scenario: A cryptographic signing protocol where the private key is both linear (use once then zeroize) AND restricted (must not leak).
fn sign_once(
key: PrivateKey @Restricted :_1,
message: Bytes @Public
) -> Signature @Public
effects: crypto.sign
ensures { verify(result, message, key.public_key()) }
{
let sig = crypto_sign(key, message)
-- key is consumed (linear) AND restricted (info flow)
-- sig is Public (declassified output of restricted operation)
zeroize(key) -- key memory zeroed after use
sig
}
The problem: The key is simultaneously:
- Linear (grade 1): must be consumed exactly once
- Restricted: must not flow to public sinks
The function produces a Signature @Public from a @Restricted
key. This is a declassification: the SIGNATURE is public, but the
KEY is not. The type checker must verify:
keyis used exactly once (linear check)keynever flows to a public sink (info flow check)- The output
sigis correctly labeled @Public (it’s derived from the key, but the signing operation is a valid declassification)
Interaction rule: Linearity and information flow are orthogonal
axes. A value has both a grade AND a label. The grade tracks
how many times it’s used; the label tracks where it can flow. A
declassify does not change the grade. Consuming a linear value
does not change its label.
Test:
-- MUST COMPILE: key used once, output correctly declassified
fn test7(key: PrivateKey @Restricted :_1, msg: Bytes @Public)
-> Signature @Public
effects: crypto.sign
{ sign_once(key, msg) }
-- MUST REJECT A05001 + A08001: key used twice AND leaked
fn bad7(key: PrivateKey @Restricted :_1) -> PrivateKey @Public
effects: pure
{ key } -- A08001: Restricted -> Public flow
-- AND key is consumed by return, but if we tried to use
-- it again, A05001 would fire
-- MUST REJECT A08002: key bytes logged
fn bad7b(key: PrivateKey @Restricted :_1) -> Unit
effects: log.info
{
log("Key bytes: " ++ key.to_string()) -- A08001: Restricted in Public log
zeroize(key)
}
Test Case 8: Typestate + Effect + Refinement (Three-Way)
Scenario: A payment processor where state transitions require specific effects and are guarded by refinement predicates.
service PaymentProcessor {
type Payment<State> {
amount: Money<USD>,
retries: Nat,
last_error: String?
}
where State in {Pending, Charging, Charged, Failed, Refunded}
fn charge(
p: Payment<Pending> :_1
) -> Payment<Charged> :_1 | Payment<Failed> :_1
requires { p.amount > Money.zero() }
effects: payment.charge, log.info, database.write
fn retry(
p: Payment<Failed> :_1
) -> Payment<Pending> :_1
requires { p.retries < 3 }
ensures { result.retries == p.retries + 1 }
effects: database.write
fn refund(
p: Payment<Charged> :_1
) -> Payment<Refunded> :_1
requires { p.amount > Money.zero() }
effects: payment.refund, database.write, audit.write
}
The problem: Three features interact simultaneously:
- Typestate:
chargeonly fromPending,retryonly fromFailed,refundonly fromCharged - Refinement:
retryonly ifretries < 3(prevents infinite retry loops) - Effect:
refundrequiresaudit.write(audit trail), whichchargedoes not
The type checker must verify all three independently and then compose the results. A retry loop must be provably bounded:
-- MUST COMPILE: bounded retry loop with all three checks
fn charge_with_retry(p: Payment<Pending> :_1)
-> Payment<Charged> :_1 | Payment<Failed> :_1
requires { p.retries == 0 }
effects: payment.charge, log.info, database.write
{
match charge(p) {
Charged(result) => Charged(result),
Failed(failed) => {
if failed.retries < 3 {
let retried = retry(failed) -- Failed -> Pending
charge_with_retry(retried) -- recursive, but bounded
} else {
Failed(failed)
}
}
}
}
Interaction rule: The type checker handles this as:
- Layer 0: typestate (DFA transitions valid), linearity (each payment consumed once per branch), effect (set containment)
- Layer 1: refinement (
retries < 3guard ensures bounded recursion) - The
decreases 3 - p.retriesannotation proves termination
Test Case 9: All Six Features (Full Stack)
Scenario: A secure data pipeline that processes sensitive records with exact resource management.
service SecurePipeline {
type Record<State> {
id: Uuid,
data: Bytes @Restricted,
processed_chunks: Nat,
total_chunks: Nat
}
where State in {Received, Validating, Processing, Complete, Failed}
fn process_chunk(
record: Record<Processing> :_1,
chunk_index: {i: Nat | i == record.processed_chunks
and i < record.total_chunks},
encryption_key: AESKey @Restricted :_1
) -> Record<Processing> :_1
requires { chunk_index < record.total_chunks }
ensures { result.processed_chunks == record.processed_chunks + 1 }
effects: database.write, crypto.encrypt
privacy: record.data purpose {processing, storage}
{
-- 1. TYPESTATE: record must be in Processing state
-- 2. REFINEMENT: chunk_index must equal processed_chunks
-- and be less than total_chunks
-- 3. DEPENDENT: processed_chunks is a Nat index that
-- tracks progress toward total_chunks
-- 4. LINEAR: encryption_key used exactly once per chunk
-- 5. EFFECT: only database.write and crypto.encrypt allowed
-- 6. INFO FLOW: record.data is @Restricted, must not leak
-- to logs or public outputs
let encrypted = encrypt(encryption_key, record.data.chunk(chunk_index))
-- key consumed (linear), data stays Restricted (info flow)
let updated = record.with_processed_chunks(record.processed_chunks + 1)
-- dependent index incremented
updated
}
fn process_all(
record: Record<Validating> :_1,
keys: Vec<AESKey @Restricted :_1, record.total_chunks>
) -> Record<Complete> :_1
requires { record.total_chunks > 0 }
requires { len(keys) == record.total_chunks }
ensures { result.processed_chunks == record.total_chunks }
effects: database.write, crypto.encrypt
decreases record.total_chunks - record.processed_chunks
{
let r = transition(record, Processing)
process_loop(r, keys, 0)
}
fn process_loop(
record: Record<Processing> :_1,
keys: Vec<AESKey @Restricted :_1, n>,
i: {x: Nat | x <= n}
) -> Record<Complete> :_1
requires { i == record.processed_chunks }
requires { n == record.total_chunks }
effects: database.write, crypto.encrypt
decreases n - i
{
if i == record.total_chunks {
transition(record, Complete)
} else {
let (key, remaining_keys) = keys.pop_first()
-- key is linear: popped from vector, used once
let updated = process_chunk(record, i, key)
process_loop(updated, remaining_keys, i + 1)
}
}
}
What this tests simultaneously:
| Feature | What’s Tested |
|---|---|
| Refinement | chunk_index == record.processed_chunks and i < total_chunks |
| Dependent | Vec<AESKey, record.total_chunks> (vector length = total chunks) |
| Linear | Each AESKey consumed exactly once, record consumed per iteration |
| Typestate | Validating -> Processing -> Complete transitions |
| Effect | Only database.write + crypto.encrypt, no logging of data |
| Info flow | record.data @Restricted never reaches public sinks |
What can go wrong:
-
The SMT query for the loop invariant (
i == processed_chunksANDi < total_chunksANDremaining_keys.len == total_chunks - i) involves quantified arithmetic over dependent indices. This could time out. -
The linear key vector
Vec<AESKey :_1, n>means each element is linear. Popping an element consumes it from the vector. The type checker must track that the vector’s length decreases by 1 each iteration AND that the popped key is consumed. -
The
decreases n - itermination measure involves both a refinement value (i) and a dependent index (n). The type checker must unify these two systems to verify termination.
Test Case 10: Conditional Typestate (Branch Divergence)
Scenario: An operation that may transition to different states depending on runtime data.
fn process_order(order: Order<Paid> :_1, inventory: Inventory)
-> Order<Shipped> :_1 | Order<Cancelled> :_1
effects: database.write, logistics.dispatch
{
if inventory.has_stock(order.items) {
let tracking = logistics.create_shipment(order)
ship(order, tracking) -- Paid -> Shipped
} else {
cancel(order, "Out of stock") -- Paid -> Cancelled
}
}
The problem: After the if/else, the order is in DIFFERENT
states in each branch. The return type must be a union
Order<Shipped> | Order<Cancelled>. The type checker must:
- Track each branch independently
- Join the typestate contexts
- Verify the caller handles both possible states
Test:
-- MUST COMPILE: caller handles both branches
fn handle(order: Order<Paid> :_1, inv: Inventory) -> String
effects: database.write, logistics.dispatch, email.send
{
match process_order(order, inv) {
Shipped(o) => {
send_tracking_email(o)
"Shipped"
}
Cancelled(o) => {
send_cancellation_email(o)
"Cancelled"
}
}
}
-- MUST REJECT A10001: non-exhaustive match (missing Cancelled)
fn bad10(order: Order<Paid> :_1, inv: Inventory) -> String
effects: database.write, logistics.dispatch
{
match process_order(order, inv) {
Shipped(o) => "Shipped"
-- Missing Cancelled case!
}
}
Test Case 11: Effect + Information Flow (Labeled Effects)
Scenario: A logging system where the log effect has a security label, preventing sensitive data from being logged.
fn process_user(user: User) -> ProcessingResult
effects: database.write, log.info @Public
{
log.info("Processing user: " ++ user.id) -- OK: id is Public
log.info("User email: " ++ user.email) -- A08002: email is @Internal
log.info("User SSN: " ++ user.ssn) -- A08002: ssn is @Restricted
let result = compute(user)
log.info("Result: " ++ result.summary) -- OK if summary is @Public
result
}
The problem: The log.info effect is annotated @Public,
meaning it can only receive data labeled @Public or lower. When a
@Restricted value (user.ssn) is passed to a @Public effect,
the compiler must reject it.
Interaction rule: Each effect in the effect row carries a maximum security label. Data flowing into an effect must have a label at or below the effect’s label. This is checked by combining the effect row check (layer 0) with the information flow check (layer 1).
Summary: Interaction Matrix
| # | Features Combined | What Breaks If Wrong |
|---|---|---|
| 1 | Refinement + Linear | Ghost use counted as computational use |
| 2 | Refinement + Typestate | State guard not verified before transition |
| 3 | Refinement + Dependent | Index arithmetic with refined bounds |
| 4 | Linear + Effect | Resource leaked in effectful closure |
| 5 | Typestate + Info Flow | Declassification tied to state transition |
| 6 | Dependent + Effect | Runtime-determined index from IO |
| 7 | Linear + Info Flow | Secret key used and leaked |
| 8 | Typestate + Effect + Refinement | Bounded retry with state, effects, and guards |
| 9 | All six | Secure pipeline loop with everything |
| 10 | Typestate + Pattern | Branch divergence in state |
| 11 | Effect + Info Flow | Labeled effects block sensitive data |
Implementation Priority
For the compiler prototype, implement interactions in this order:
- Linear + Typestate (easiest: typestate requires linearity)
- Linear + Effect (common: resource-scoped effects)
- Refinement + Dependent (core value: index safety)
- Effect + Info Flow (core value: preventing PII leaks)
- Refinement + Linear (ghost use rule)
- Typestate + Refinement (guarded transitions)
- Dependent + Effect (abstract indices from IO)
- Typestate + Info Flow (label transitions)
- Three-way combinations (after pairwise is solid)
- Full stack (integration test, last)
Each test case above serves as both a specification and a regression test. When the type checker handles all 11 cases correctly, the interaction problem is solved.
14. Verification Categories
The base language (Sections 1-13) handles application-level services. These categories add domain-specific verification capabilities organized by concern. A project selects which categories to activate via the profile system (Section 1.2).
CORE is always active. All others are opt-in.
14.CORE: Verification Infrastructure
Always active. Cannot be excluded from any profile.
CORE.1 Ghost Code
Variables, functions, and blocks that exist only for verification. They are type-checked, verified by SMT, but completely erased from the generated Rust code. Zero runtime cost.
Motivation
Every mature verification tool has ghost code: SPARK Ada (Ghost
aspect), Dafny (ghost var, ghost method), Verus (proof fn,
tracked), Creusot (#[logic]). The reason is fundamental:
verification often requires tracking state that the runtime code
does not need.
Examples from other Assura features:
- Monotonic state (STOR.5): needs to remember the previous value
to verify
new >= old, but the runtime only needs the current value - Bit-level format (FMT.2): the bit cursor position is a verification concept; the runtime tracks it implicitly via byte_pos + bit_offset
- Multi-pass refinement (TEST.3): quality measurement exists only to prove convergence, not for runtime behavior
- Page cache (STOR.2): pin count invariants need logical tracking of which pages are pinned by whom
Without ghost code, these verification-only values must be
embedded in the runtime code behind #[cfg(debug_assertions)],
which conflates debugging and verification.
Grammar
GhostDecl = 'ghost' GhostItem ;
GhostItem = 'var' Ident ':' TypeExpr ['=' Expr]
| 'fn' FnDecl
| 'type' TypeDecl
| '{' StmtList '}' ;
GhostAttr = '#[ghost]' ;
Full Example
type SortedList<T: Ord> {
data: Vec<T>,
// Ghost: the abstract sequence for verification
ghost var elements: Sequence<T>,
invariant {
// Runtime data and ghost sequence are in sync
data.len() == elements.len(),
for_all(i in 0..data.len(), data[i] == elements[i]),
// Sortedness expressed on the ghost sequence
for_all(i in 0..elements.len()-1,
elements[i] <= elements[i+1]
)
}
}
fn insert(list: &mut SortedList<T>, value: T)
ensures {
// Ghost sequence has the new element
list.elements == old(list.elements).insert_sorted(value)
}
{
let pos = list.data.binary_search(&value)
.unwrap_or_else(|p| p)
list.data.insert(pos, value)
// Ghost: update the logical sequence
ghost { list.elements = list.elements.insert_sorted(value) }
}
// Ghost function: pure mathematical definition
ghost fn insert_sorted(seq: Sequence<T>, val: T) -> Sequence<T>
ensures {
result.len() == seq.len() + 1,
result.contains(val),
is_sorted(result)
}
{
// This body is verified but never compiled
let pos = seq.find_insertion_point(val)
seq.insert_at(pos, val)
}
// Using ghost blocks for verification state
fn binary_search(arr: &[I32], target: I32) -> Option<U32>
requires { is_sorted(arr) }
ensures {
match result {
Some(idx) => arr[idx] == target,
None => for_all(i in 0..arr.len(), arr[i] != target)
}
}
{
let mut lo: U32 = 0
let mut hi: U32 = arr.len() as U32
while lo < hi
invariant {
lo <= hi && hi <= arr.len() as U32,
for_all(i in 0..lo, arr[i] < target),
for_all(i in hi..arr.len() as U32, arr[i] > target)
}
{
let mid = lo + (hi - lo) / 2
if arr[mid] < target {
lo = mid + 1
} else if arr[mid] > target {
hi = mid
} else {
return Some(mid)
}
// Ghost: track iteration for termination
ghost {
assert hi - lo < old(hi) - old(lo)
// proves the loop terminates
}
}
None
}
Verification Rule
- Erasure guarantee: Ghost code cannot affect runtime behavior. Ghost variables cannot appear in non-ghost expressions. Ghost functions cannot be called from non-ghost code. Violations produce A54001
- Type checking: Ghost code is fully type-checked using the same rules as runtime code
- SMT inclusion: Ghost assertions and invariants are included in the SMT query. Ghost function bodies are available for inlining by the solver
- Ghost purity: Ghost functions must be pure (no effects). Ghost variables can only be modified inside ghost blocks
Error Codes
| Code | Message | Cause |
|---|---|---|
| A54001 | Ghost variable V used in non-ghost context | Runtime code depends on verification-only state |
| A54002 | Ghost function F called from non-ghost code | Runtime code calls verification-only function |
| A54003 | Ghost block has side effects | Ghost code must be pure |
| A54004 | Ghost variable not updated to match runtime state | Invariant links ghost and runtime but ghost lags |
| A54005 | Ghost type used in runtime signature | Function parameter or return uses ghost type |
Rust Codegen
Ghost code is completely erased:
#![allow(unused)]
fn main() {
// Assura:
// ghost var elements: Sequence<T>
// ghost { elements = elements.insert_sorted(value) }
//
// Rust: (nothing generated)
// Ghost assertions become debug_assert in debug mode only
#[cfg(debug_assertions)]
{
debug_assert!(is_sorted(&list.data));
}
}
CORE.2 Lemmas and Proof Functions
Functions that prove properties but generate no runtime code. They exist to help the SMT solver establish facts that are needed by other contracts.
Motivation
SMT solvers are powerful but not omniscient. Sometimes the solver cannot prove a property directly but can verify it if given an intermediate step. Lemmas provide those steps.
Examples:
- Proving a sort is correct requires transitivity of comparison
- Proving a hash table has no collisions requires properties of the hash function
- Proving a balanced tree stays balanced after rotation requires height relationships
Dafny has lemma, Verus has proof fn, Lean has theorem.
Without lemmas, Assura must either hope the SMT solver can prove
everything automatically (it often cannot) or encode hints in
awkward ways.
Grammar
LemmaDecl = 'lemma' Ident ['<' TypeParams '>']
'(' ParamList ')'
RequiresClause
EnsuresClause
[LemmaBody] ;
LemmaBody = '{' { LemmaStep } '}' ;
LemmaStep = 'assert' Predicate
| 'apply' LemmaIdent '(' ArgList ')'
| 'induction' Ident
| 'cases' Ident '{' { CaseArm } '}' ;
Full Example
// Lemma: if a list is sorted and we insert in the right
// position, the result is still sorted
lemma insert_preserves_sorted<T: Ord>(
seq: Sequence<T>,
val: T,
pos: U32
)
requires {
is_sorted(seq),
pos <= seq.len(),
pos == 0 || seq[pos - 1] <= val,
pos == seq.len() || val <= seq[pos]
}
ensures {
is_sorted(seq.insert_at(pos, val))
}
{
// Proof by cases on the elements around the insertion point
assert for_all(i in 0..pos,
seq[i] <= val // all before pos are <= val
)
assert for_all(i in pos..seq.len(),
val <= seq[i] // all after pos are >= val
)
// SMT can now derive sortedness of the result
}
// Lemma: transitivity of comparison (helps sort proofs)
lemma comparison_transitive<T: Ord>(a: T, b: T, c: T)
requires { a <= b && b <= c }
ensures { a <= c }
// No body needed: SMT handles this directly
// Lemma: CRC32 distributes over concatenation
// (needed for chunked integrity verification)
lemma crc32_concat(data_a: &[U8], data_b: &[U8])
requires { true }
ensures {
crc32(concat(data_a, data_b)) ==
crc32_combine(crc32(data_a), crc32(data_b), data_b.len())
}
{
induction data_b
// Base case: data_b is empty
// crc32(concat(a, [])) == crc32(a) == crc32_combine(crc32(a), 0, 0)
// Inductive step: data_b = [head] ++ tail
// apply crc32_concat(data_a, tail)
// assert crc32(concat(a, [head]++tail)) == ...
}
// Using a lemma in a function contract
fn merge_sorted(a: &[I32], b: &[I32]) -> Vec<I32>
requires { is_sorted(a) && is_sorted(b) }
ensures { is_sorted(result) && result.len() == a.len() + b.len() }
{
// ... merge logic ...
// Invoke lemma to help the solver
apply insert_preserves_sorted(partial_result, next_val, insert_pos)
}
Verification Rule
- No runtime effect: Lemmas generate no code. They exist only as proof obligations for the SMT solver
- Must be valid: The solver must be able to verify the lemma itself (the ensures follows from the requires + body)
- Can be applied:
apply lemma_name(args)adds the lemma’s ensures clause as an assumption at that point in the proof - Induction support:
induction vargenerates the base case and inductive step for structural induction onvar
Error Codes
| Code | Message | Cause |
|---|---|---|
| A55001 | Lemma L could not be verified | SMT cannot prove ensures from requires |
| A55002 | Lemma applied with unsatisfied preconditions | apply used where requires not met |
| A55003 | Induction variable not inductively defined | induction on non-recursive type |
| A55004 | Lemma has side effects | Lemmas must be pure |
| A55005 | Circular lemma dependency | Lemma A depends on B which depends on A |
Rust Codegen
Lemmas are completely erased. No runtime code is generated.
#![allow(unused)]
fn main() {
// Assura:
// lemma insert_preserves_sorted(...) { ... }
// apply insert_preserves_sorted(partial, val, pos)
//
// Rust: (nothing generated for either declaration or application)
}
CORE.3 Frame Conditions
Explicit declarations of which state a function modifies, enabling the verifier to prove that everything else is unchanged.
Motivation
Assura’s effects system (Section 3) tracks what kind of side effects occur (filesystem, database, network). But it does not track WHICH specific variables or fields change. Frame conditions fill this gap.
Without frame conditions, the verifier must assume a function call could modify anything. This makes modular verification impossible: proving a property about variable X is useless if the next function call might silently change X.
SPARK Ada has Global and Depends aspects. Frama-C has
assigns clauses. Dafny has modifies clauses. All of them
learned that frame conditions are essential for scalable
verification.
Grammar
FrameClause = 'modifies' '{' ModifiesTarget
{ ',' ModifiesTarget } '}'
| 'reads' '{' ReadsTarget
{ ',' ReadsTarget } '}' ;
ModifiesTarget = Ident
| Ident '.' FieldIdent
| Ident '[' Expr ']'
| '*' Ident // all fields of the object ;
ReadsTarget = Ident
| Ident '.' FieldIdent
| '*' Ident ;
Full Example
type BTreeNode {
keys: Vec<I64>,
children: Vec<BTreeNode>,
count: U32,
parent: Option<&BTreeNode>,
}
// Frame condition: only modifies the node's keys and count
fn insert_key(
node: &mut BTreeNode,
key: I64,
pos: U32
)
modifies { node.keys, node.count }
// Implicit: node.children and node.parent are UNCHANGED
requires { pos <= node.count }
ensures {
node.count == old(node.count) + 1,
node.keys[pos] == key,
// children unchanged (guaranteed by frame condition)
node.children == old(node.children)
}
{
node.keys.insert(pos as usize, key)
node.count += 1
}
// Frame condition on a struct field
type PageCache {
pages: HashMap<U32, Page>,
dirty_set: HashSet<U32>,
stats: CacheStats,
}
fn mark_dirty(cache: &mut PageCache, page_id: U32)
modifies { cache.dirty_set, cache.stats }
// pages themselves are NOT modified
requires { cache.pages.contains_key(page_id) }
ensures {
cache.dirty_set.contains(page_id),
cache.pages == old(cache.pages) // frame: pages unchanged
}
{
cache.dirty_set.insert(page_id)
cache.stats.dirty_count += 1
}
// reads clause: function only reads these fields
fn lookup(cache: &PageCache, page_id: U32) -> Option<&Page>
reads { cache.pages }
// Does not read dirty_set or stats
ensures {
result.is_some() == cache.pages.contains_key(page_id)
}
{
cache.pages.get(&page_id)
}
Verification Rule
- Write restriction: If
modifies { a, b }is declared, the function body can only assign toaandb. Writing to any other state is a compile error - Frame inference: For any variable NOT in the
modifiesset, the verifier adds an implicitensures { x == old(x) } - Callee frames: When a function with
modifies { x }calls another function withmodifies { y }, the verifier checks thatyis a subset ofxor thatyis local state - Read restriction: If
reads { a }is declared, the function body can only read froma. This enables the verifier to skip tracking changes to other fields
Error Codes
| Code | Message | Cause |
|---|---|---|
| A56001 | Function modifies X not in modifies clause | Write to undeclared target |
| A56002 | Called function modifies X outside caller’s frame | Callee escapes caller’s modifies set |
| A56003 | Function reads X not in reads clause | Read from undeclared source |
| A56004 | Modifies clause on pure function | Pure functions cannot have modifies |
| A56005 | Frame condition conflict with effects | modifies contradicts effects declaration |
Rust Codegen
Frame conditions are erased (compile-time only). In debug mode, they generate field-level change tracking:
#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
{
let old_children = node.children.clone();
// ... function body ...
debug_assert_eq!(node.children, old_children,
"frame violation: children modified");
}
}
CORE.4 Axiomatic Definitions
Abstract mathematical definitions that the verifier uses for reasoning without requiring an implementation. They define WHAT something means, not HOW to compute it.
Motivation
Some concepts are easier to define mathematically than to implement:
- “A sequence is sorted” is a one-line mathematical definition but a multi-line runtime check
- “Two sets are disjoint” is trivial in math but requires iteration at runtime
- “A tree is balanced” has a clean recursive definition but a complex imperative check
Axioms let the verifier reason about these concepts directly
without needing executable code. Frama-C has axiomatic blocks.
Dafny has predicate and function (which can be ghost).
Why3 has axiom.
Grammar
AxiomDecl = 'axiom' Ident ['<' TypeParams '>']
'(' ParamList ')' ':' TypeExpr
'=' AxiomBody ;
AxiomBody = Predicate
| '{' { AxiomClause } '}' ;
AxiomClause = 'define' ':' Predicate
| 'property' ':' Predicate ;
Full Example
// Axiom: what "sorted" means
axiom is_sorted<T: Ord>(seq: Sequence<T>) : Bool =
for_all(i in 0..seq.len()-1, seq[i] <= seq[i+1])
// Axiom: what "permutation" means
axiom is_permutation<T>(a: Sequence<T>, b: Sequence<T>) : Bool = {
define: a.len() == b.len()
&& for_all(x: T, a.count(x) == b.count(x))
}
// Axiom: what a valid B-tree is
axiom is_valid_btree(node: BTreeNode, order: U32) : Bool = {
define:
// Keys are sorted within each node
is_sorted(node.keys)
// Number of children = number of keys + 1 (for internal nodes)
&& (node.is_leaf() || node.children.len() == node.keys.len() + 1)
// All keys in left subtree < key < all keys in right subtree
&& for_all(i in 0..node.keys.len(),
(i == 0 || all_less(node.children[i], node.keys[i]))
&& (i == node.keys.len() - 1
|| all_greater(node.children[i+1], node.keys[i]))
)
// Recursively valid
&& for_all(child in node.children, is_valid_btree(child, order))
property:
// All leaves are at the same depth
for_all(leaf_a in leaves(node), leaf_b in leaves(node),
depth(leaf_a) == depth(leaf_b)
)
}
// Using axioms in contracts
fn sort(data: &mut [I32])
ensures {
is_sorted(result),
is_permutation(old(data).to_seq(), result.to_seq())
}
{
// ... sorting implementation ...
}
fn btree_insert(tree: &mut BTree, key: I64)
requires { is_valid_btree(tree.root, tree.order) }
ensures { is_valid_btree(tree.root, tree.order) }
{
// ... insertion logic ...
}
Verification Rule
- No runtime evaluation: Axioms are never executed. They exist only in the SMT domain
- Consistency: The verifier checks that axiom definitions
are not contradictory (e.g.,
axiom absurd() : Bool = true && false) - Totality: Recursive axioms must be well-founded (they must terminate on all inputs). The verifier checks structural decrease or explicit fuel bounds
- Properties:
propertyclauses are additional facts the verifier must prove follow from thedefineclause
Error Codes
| Code | Message | Cause |
|---|---|---|
| A57001 | Axiom A is inconsistent | Definition is self-contradictory |
| A57002 | Recursive axiom not well-founded | No structural decrease in recursion |
| A57003 | Axiom property does not follow from definition | Property clause not provable |
| A57004 | Axiom used at runtime | Axiom referenced in non-ghost, non-contract context |
| A57005 | Conflicting axiom definitions | Two axioms define the same concept differently |
Rust Codegen
Axioms are completely erased. In debug mode, simple axioms generate runtime checks:
#![allow(unused)]
fn main() {
// Simple axiom: can generate a runtime check
#[cfg(debug_assertions)]
fn debug_is_sorted(seq: &[i32]) -> bool {
seq.windows(2).all(|w| w[0] <= w[1])
}
// Complex axiom (recursive, quantified): no runtime check possible
// Verified purely at compile time via SMT
}
CORE.5 Quantifier Triggers
Hints that tell the SMT solver how to instantiate universal quantifiers. Without them, the solver either times out or explores an exponential search space.
Motivation
Universal quantifiers (for_all) are the most common source of
SMT solver timeouts. When the solver sees for_all(x: T, P(x)),
it must decide which concrete values of x to try. Without
guidance, it may try none (proof fails) or too many (timeout).
Verus and Dafny both have explicit trigger syntax because they learned this is practically essential. Triggers tell the solver: “when you see an expression matching this pattern, instantiate the quantifier with the matching value.”
This is an implementation concern, not a mathematical one. But without it, real verification systems hit timeout walls on moderate-size programs.
Grammar
TriggerClause = '#[trigger' '(' TriggerPattern
{ ',' TriggerPattern } ')' ']' ;
TriggerPattern = Expr ; // expression pattern the solver matches
AutoTrigger = '#[auto_trigger]' ; // let the solver choose
Full Example
// Without trigger: solver may time out on large arrays
fn lookup_correct(table: &HashMap<K, V>, key: K) -> Option<V>
ensures {
// Trigger: when the solver sees table.get(k) for any k,
// it should instantiate this quantifier with that k
#[trigger(table.get(x))]
for_all(x: K,
result == table.get(key)
)
}
// Sorted array: trigger on array access
fn binary_search_spec(arr: &[I32], target: I32) -> Option<U32>
requires {
// Trigger: when the solver sees arr[i], instantiate with
// that i
#[trigger(arr[i])]
for_all(i in 0..arr.len()-1, arr[i] <= arr[i+1])
}
// Multiple triggers: instantiate when EITHER pattern matches
lemma map_preserves_length<T, U>(
seq: Sequence<T>,
f: fn(T) -> U
)
ensures {
#[trigger(seq[i], mapped[i])]
for_all(i in 0..seq.len(),
mapped[i] == f(seq[i])
)
&& mapped.len() == seq.len()
}
// Auto-trigger: let the solver choose (simpler but less reliable)
fn contains_all(subset: &[I32], superset: &[I32]) -> Bool
ensures {
#[auto_trigger]
for_all(x in subset, superset.contains(x))
}
Verification Rule
- Trigger validity: Trigger patterns must mention the bound variable of the quantifier. A trigger that does not reference the quantified variable is useless (A58001)
- Trigger coverage: The trigger must be specific enough to avoid matching loops (where instantiation creates new matches). The verifier warns on potential matching loops (A58002)
- Auto-trigger fallback: If no trigger is specified and
#[auto_trigger]is not present, the verifier uses Z3/CVC5’s built-in heuristic but warns that explicit triggers are recommended for reliability - Multi-trigger: Multiple trigger patterns create a conjunction: the quantifier is instantiated only when ALL patterns match simultaneously
Error Codes
| Code | Message | Cause |
|---|---|---|
| A58001 | Trigger does not mention bound variable V | Useless trigger pattern |
| A58002 | Potential matching loop in trigger | Trigger may cause infinite instantiation |
| A58003 | Quantifier timeout (no trigger specified) | Solver timed out; add explicit trigger |
| A58004 | Conflicting triggers on same quantifier | Multiple trigger annotations conflict |
| A58005 | Trigger pattern not found in formula | Trigger references expression not in quantifier body |
Rust Codegen
Triggers are erased (they are SMT directives, not runtime code).
#![allow(unused)]
fn main() {
// Triggers affect only the verification pass.
// No Rust code is generated for trigger annotations.
}
CORE.6 Opaque Functions
Functions whose implementation is hidden from the verifier. The solver reasons only about the function’s contract (requires/ ensures), not its body. This prevents the solver from unfolding complex or recursive definitions and timing out.
Motivation
When the verifier encounters a function call, it can either:
- Inline the function body (precise but expensive)
- Use only the contract (fast but requires good contracts)
By default, the solver tries to inline, which causes problems:
- Recursive functions cause infinite unfolding
- Complex functions cause exponential blowup
- Implementation details leak into callers’ proofs, creating fragile verification that breaks when internals change
Dafny has opaque functions. Verus has open/closed specs.
SPARK Ada uses Refined_Post to separate interface from body
contracts. The pattern is universal: hide what you do not need
to expose.
Grammar
OpaqueAttr = '#[opaque]' ;
RevealStmt = 'reveal' FnIdent ;
// temporarily expose the body at this proof point
Full Example
// Opaque: verifier sees only the contract, not the body
#[opaque]
fn fibonacci(n: U32) -> U64
requires { n <= 93 } // fits in u64
ensures {
n == 0 => result == 0,
n == 1 => result == 1,
n >= 2 => result == fibonacci(n - 1) + fibonacci(n - 2)
}
{
// Iterative implementation (efficient)
// Verifier does NOT see this; it uses the ensures clause
let mut a: U64 = 0
let mut b: U64 = 1
for _ in 0..n {
let temp = a + b
a = b
b = temp
}
a
}
// Caller: verifier uses only fibonacci's contract
fn fib_is_monotonic(a: U32, b: U32) -> Bool
requires { a <= b && b <= 93 }
ensures { fibonacci(a) <= fibonacci(b) }
{
// Cannot prove this from contract alone; reveal the body
reveal fibonacci
// Now the verifier can unfold fibonacci's definition
// (with fuel bounds to prevent infinite recursion)
// ... inductive proof ...
true
}
// Opaque type: hide internal representation
#[opaque]
type BloomFilter {
bits: Vec<U64>,
hash_count: U8,
// Verifier cannot see these fields from outside
}
// Public contract: does not expose internal representation
fn bloom_insert(filter: &mut BloomFilter, item: &[U8])
ensures { bloom_may_contain(filter, item) == true }
#[opaque]
fn bloom_may_contain(filter: &BloomFilter, item: &[U8]) -> Bool
ensures {
// False positives possible, false negatives impossible
// (this is the ONLY fact callers can rely on)
}
Verification Rule
- Body hidden: When verifying callers of an opaque function, the solver uses only the requires/ensures contract. The function body is not available for inlining
- Body verified separately: The opaque function itself is verified: its body must satisfy its ensures clause. This happens once, not at every call site
- Reveal:
reveal fn_nametemporarily exposes the function body at a specific proof point. The verifier adds fuel bounds to prevent infinite unfolding of recursive functions - Modular verification: Opaque functions enable modular verification. Changing the body of an opaque function does not require re-verifying callers (as long as the contract is unchanged)
Error Codes
| Code | Message | Cause |
|---|---|---|
| A59001 | Cannot prove property: function F is opaque | Caller needs body but function is hidden; use reveal |
| A59002 | Reveal of non-opaque function | reveal on a transparent function (no-op warning) |
| A59003 | Opaque function contract insufficient | Body satisfies a property the contract does not expose |
| A59004 | Recursive reveal exceeded fuel | reveal on recursive function hit unfolding limit |
| A59005 | Opaque type field accessed externally | Code outside the module accesses hidden field |
Rust Codegen
Opacity is a verification concept only. The generated Rust code is identical whether the function is opaque or not:
#![allow(unused)]
fn main() {
// Opaque functions generate normal Rust code.
// The opacity only affects the verification pass.
pub fn fibonacci(n: u32) -> u64 {
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 0..n {
let temp = a + b;
a = b;
b = temp;
}
a
}
}
CORE.7 Prophecy Variables
Ghost variables determined by future events, not past state. Standard ghost variables (CORE.1) are history variables: their value is computed from the current and past program state. Prophecy variables have a value that is declared now but resolved later, enabling specification of properties that depend on how the execution will proceed.
Motivation
Lock-free data structures with helping mechanisms (Michael-Scott queue, Harris linked list, elimination stack) have non-fixed linearization points: the point where an operation logically takes effect may be inside ANOTHER thread’s code. Proving linearizability of these algorithms requires specifying “operation A linearizes at the point where thread B completes the help,” which is unknown when A starts.
Without prophecy variables, you can only verify algorithms with fixed linearization points (e.g., Treiber stack, where the LP is always the successful CAS). This excludes the most widely deployed lock-free data structures.
Syntax
// Declare a prophecy: value unknown now
ghost prophecy lp: ProgramPoint
// Constrain the prophecy (optional, narrows valid assignments)
ghost prophecy choice: {v: Nat | v < queue.len()}
// Resolve: fix the prophecy's value at this execution point
resolve lp = here
resolve choice = selected_index
// The verifier checks: for every execution, there EXISTS
// a valid prophecy assignment satisfying all constraints
// and all postconditions that reference the prophecy
Example: Michael-Scott Queue Dequeue
fn dequeue(q: &MichaelScottQueue<T>) -> Option<T> {
ghost prophecy lp: ProgramPoint
ghost prophecy result_val: Option<T>
loop {
let head = q.head.load(acquire)
let next = head.next.load(acquire)
if next.is_null() {
resolve result_val = None
resolve lp = here
return None
}
let val = next.data
if q.head.compare_exchange(head, next, acq_rel).is_ok() {
resolve result_val = Some(val)
resolve lp = here // LP is THIS thread's CAS
return Some(val)
}
// CAS failed: another thread helped. LP might be in
// the other thread's code. Prophecy remains unresolved
// for this iteration; loop retries.
}
}
ensures result == result_val // result matches prophecy
ensures linearized_at(lp) // LP is between invoke and return
When thread B helps thread A’s dequeue, the prophecy lp for A’s
operation resolves inside B’s code path. The verifier checks that
for every interleaving, there exists a valid assignment of all
prophecy variables such that the sequential specification holds.
Verification Rule
- Existential check: For each prophecy variable P, the
verifier introduces an existential quantifier:
exists P: constraints(P) && postconditions_hold(P) - Resolution obligation: Every prophecy must be resolved on every execution path. A-CORE-025 fires if an unresolved prophecy reaches function exit
- Single resolution: Each prophecy is resolved exactly once. A-CORE-026 fires on double resolution
- Layer 2: Prophecy verification runs at Layer 2 (bounded SMT, <10s). The existential quantifier is handled via Skolemization or counterexample-guided synthesis
- Interaction with CORE.1: Prophecy variables can be read by ghost code and used in postconditions, lemmas, and axiomatic definitions, just like history ghost variables
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-CORE-025 | Unresolved prophecy at function exit | Prophecy P not resolved on some path |
| A-CORE-026 | Prophecy resolved twice | resolve P appears on two paths that can both execute |
| A-CORE-027 | Prophecy type mismatch | resolve P = expr where expr type differs from P |
| A-CORE-028 | Prophecy constraint violated | Resolved value does not satisfy declared constraint |
Rust Codegen
Prophecy variables are verification-only. No runtime code is generated. The resolved value exists only in the proof:
#![allow(unused)]
fn main() {
// Source Assura has: ghost prophecy lp: ProgramPoint
// Generated Rust: nothing. Prophecies are erased.
fn dequeue(q: &MichaelScottQueue<T>) -> Option<T> {
loop {
let head = q.head.load(Ordering::Acquire);
let next = unsafe { (*head).next.load(Ordering::Acquire) };
if next.is_null() {
return None;
}
let val = unsafe { (*next).data };
if q.head.compare_exchange(
head, next, Ordering::AcqRel, Ordering::Acquire
).is_ok() {
return Some(val);
}
}
}
}
CORE.8 Liveness Contracts
Properties that assert something good eventually happens: a leader is eventually elected, a request is eventually served, a stuck process eventually recovers. These complement safety properties (bad things never happen) with progress guarantees.
Motivation
Safety verification proves your system never does something wrong. But a system that does nothing at all is trivially safe. Liveness proves the system eventually does something right. Critical examples:
- Raft consensus: “A leader is eventually elected after partition recovery” (without this, the cluster could stay leaderless forever)
- FreeRTOS: “The scheduler eventually dispatches every ready task” (without this, a task could starve indefinitely)
- PX4 autopilot: “The controller eventually stabilizes after a disturbance” (without this, the drone could oscillate forever)
- DNS resolver: “Every query eventually receives a response or timeout” (without this, a query could hang indefinitely)
Syntax
// Eventually: at some future point, P holds
liveness leader_election {
assume eventually_always { network.synchronous }
prove eventually { exists n in nodes: n.role == Leader }
}
// Leads-to: whenever P holds, Q eventually follows
liveness request_service {
prove leads_to(
request.state == Pending,
request.state == Served || request.state == TimedOut
)
}
// Bounded liveness: Q follows P within K steps
liveness bounded_election {
assume eventually_always { network.synchronous }
prove eventually_within(steps: 3 * num_nodes) {
exists n in nodes: n.role == Leader
}
}
// Fairness assumption: if action is continuously enabled,
// it is eventually taken
liveness fair_scheduler {
assume fair { task_runnable(t) } implies eventually { task_running(t) }
prove leads_to(
task.state == Ready,
task.state == Running
)
}
Example: FreeRTOS Scheduler Progress
liveness scheduler_progress {
// Fairness: if the tick interrupt fires, the scheduler runs
assume fair { tick_pending }
implies eventually { scheduler_invoked }
// If a task is the highest-priority ready task,
// it eventually gets the CPU
prove leads_to(
task.priority == max_ready_priority()
&& task.state == Ready,
task.state == Running
)
// Bounded version: within 2 tick periods
prove eventually_within(ticks: 2) {
current_task == highest_priority_ready_task()
}
}
Example: Raft Leader Election
liveness raft_election {
// Partial synchrony: network eventually becomes synchronous
assume eventually_always {
forall m in messages:
delivered_within(m, delta)
}
// A leader is eventually elected
prove eventually {
exists n in nodes:
n.role == Leader
&& n.term == max(node.term for node in nodes)
}
// Committed entries are eventually applied
prove leads_to(
entry.committed,
entry.applied
)
}
Verification Rule
Liveness is verified via liveness-to-safety reduction (Biere et al.), which transforms liveness checking into safety checking that SMT solvers handle natively:
- Augmentation: The verifier adds a “lasso detector” to the state space. A lasso is a finite prefix followed by a cycle. If a liveness property fails, there exists a lasso where the bad thing persists forever
- Bounded model checking (BMC): The verifier unrolls the transition relation up to K steps and checks for lassos. If no lasso is found within K steps, the property holds up to that bound
- K-induction (optional): For unbounded proofs, the verifier uses k-induction: if no lasso of length <= k exists AND the lasso-freedom is inductive, the property holds for all lengths. This runs at Layer 2-3 with configurable timeouts
- Fairness encoding:
assume fairconstraints are encoded as compassion (strong fairness) or justice (weak fairness) requirements in the transition system. The lasso detector checks that every cycle satisfies the fairness constraints - Step bound K: Configurable per-project. Default K=1000.
For
eventually_within(steps: N), the bound is exactly N
Layer assignment:
eventually_within → Layer 1 (bounded, decidable, <200ms)
eventually (BMC) → Layer 2 (bounded K, <10s)
eventually (k-ind) → Layer 3 (unbounded, may timeout)
leads_to → Layer 2-3 (depends on state space)
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-CORE-029 | Liveness violation: lasso found | BMC found a cycle where property never holds |
| A31006 | Liveness unproven within bound K | No lasso found but k-induction did not converge |
| A31007 | Missing fairness assumption | Liveness depends on scheduling but no assume fair |
| A-CORE-032 | Invalid fairness target | assume fair references undefined action or state |
| A-CORE-033 | Bounded liveness exceeded | eventually_within(N) but property not reached in N steps |
Rust Codegen
Liveness contracts are verification-only. No runtime code is generated. In debug builds, optional runtime monitors can be emitted:
#![allow(unused)]
fn main() {
// Source Assura has: prove eventually_within(ticks: 2) { ... }
// Generated Rust (debug only):
#[cfg(debug_assertions)]
fn check_liveness_scheduler() {
static TICKS_SINCE_READY: AtomicU32 = AtomicU32::new(0);
let ticks = TICKS_SINCE_READY.fetch_add(1, Ordering::Relaxed);
debug_assert!(
ticks < 2,
"liveness: highest-priority task not scheduled within 2 ticks"
);
}
// Release build: nothing generated. The proof guarantees progress.
}
14.MEM: Memory Safety
MEM.1 Memory Regions
Memory regions are typed, bounds-tracked views into byte buffers. They enable verified pointer arithmetic without unsafe code.
Motivation
6 of 9 recent SQLite CVEs involved buffer overflows caused by
computing an offset into a page buffer without bounds checking.
Example: &aData[get2byte(&aData[cellOffset + 2*iCell])] accesses
a page buffer using a 2-byte big-endian offset read from within
the same buffer. If the offset is out of bounds, memory corruption
occurs.
Grammar
RegionType = 'Region' '<' TypeExpr '>' ;
SliceExpr = Expr '.' 'slice' '(' Expr ',' Expr ')' ;
RegionReadExpr = Expr '.' 'read_u8' '(' Expr ')'
| Expr '.' 'read_u16_be' '(' Expr ')'
| Expr '.' 'read_u32_be' '(' Expr ')'
| Expr '.' 'read_u64_be' '(' Expr ')'
| Expr '.' 'read_i32_be' '(' Expr ')' ;
RegionWriteExpr = Expr '.' 'write_u8' '(' Expr ',' Expr ')'
| Expr '.' 'write_u16_be' '(' Expr ',' Expr ')'
| Expr '.' 'write_u32_be' '(' Expr ',' Expr ')'
| Expr '.' 'write_u64_be' '(' Expr ',' Expr ')' ;
Type System
A Region<Size> is a byte buffer of exactly Size bytes. Every
read and write operation requires a refined offset proving the access
is in bounds:
type Region<Size: Nat> {
invariant { len(data) == Size }
}
fn read_u16_be(
r: Region<n>,
offset: {i: Nat | i + 2 <= n}
) -> {v: Nat | v < 65536}
effects: pure
fn read_u32_be(
r: Region<n>,
offset: {i: Nat | i + 4 <= n}
) -> U32
effects: pure
fn write_u16_be(
r: Region<n> :_1,
offset: {i: Nat | i + 2 <= n},
value: {v: Nat | v < 65536}
) -> Region<n> :_1
effects: pure
fn slice(
r: Region<n>,
start: {s: Nat | s <= n},
len: {l: Nat | s + l <= n}
) -> Region<l>
effects: pure
SQLite Example: Cell Pointer Access
contract CellPointerAccess {
type PageBuffer = Region<PageSize>
fn get_cell_offset(
page: PageBuffer,
cell_index: {i: Nat | i < cell_count},
cell_pointer_offset: {c: Nat | c + 2 * cell_count <= PageSize}
) -> {offset: Nat | offset < PageSize}
effects: pure
{
let raw = page.read_u16_be(cell_pointer_offset + 2 * cell_index)
requires { raw < PageSize }
-- COMPILE ERROR if raw could be >= PageSize
-- (catches SQLite-class buffer overflow at compile time)
raw
}
}
Rust Codegen
Region<n> maps to &[u8] (read) or &mut [u8] (write) with
debug_assert bounds checks:
#![allow(unused)]
fn main() {
pub struct Region<'a> {
data: &'a [u8],
}
impl<'a> Region<'a> {
pub fn read_u16_be(&self, offset: usize) -> u16 {
debug_assert!(offset + 2 <= self.data.len());
u16::from_be_bytes([self.data[offset], self.data[offset + 1]])
}
}
}
The dependent size index n erases. Bounds safety is proven at
compile time by the refinement checker; the debug_assert is a second
safety net.
MEM.2 Fixed-Width Integers
Fixed-width integers model the exact arithmetic behavior of hardware integers, including overflow and truncation.
Motivation
The dominant SQLite CVE pattern is “64-bit value truncated to 32-bit
int in a size calculation.” Example: CVE-2025-52099 where
sz * cnt overflows a 32-bit int in setupLookaside().
Types
// Fixed-width unsigned
type U8 = {v: Nat | v <= 255}
type U16 = {v: Nat | v <= 65535}
type U32 = {v: Nat | v <= 4294967295}
type U64 = {v: Nat | v <= 18446744073709551615}
// Fixed-width signed
type I8 = {v: Int | -128 <= v and v <= 127}
type I16 = {v: Int | -32768 <= v and v <= 32767}
type I32 = {v: Int | -2147483648 <= v and v <= 2147483647}
type I64 = {v: Int | -9223372036854775808 <= v and v <= 9223372036854775807}
// Platform-dependent
type USize = U64 // or U32, selected by target
type ISize = I64
// Checked narrowing: REQUIRES proof that value fits
fn narrow_u64_to_u32(v: U64) -> U32
requires { v <= 4294967295 }
fn narrow_i64_to_i32(v: I64) -> I32
requires { -2147483648 <= v and v <= 2147483647 }
// Checked multiplication: REQUIRES proof of no overflow
fn checked_mul_u32(a: U32, b: U32) -> U32
requires { a * b <= 4294967295 }
ensures { result == a * b }
Arithmetic Overflow Rules
All fixed-width arithmetic generates refinement checks:
fn alloc_size(count: U64, element_size: U64) -> USize
requires { count * element_size <= MAX_ALLOC }
{
let total: U64 = count * element_size
-- Compiler emits SMT query: count * element_size <= U64_MAX
-- If caller can't prove it, error A13004
narrow_u64_to_usize(total)
-- Compiler emits: total <= USIZE_MAX
-- If 32-bit target and total > U32_MAX, error A13004
}
This single feature would have caught CVE-2020-13434, CVE-2022-35737, CVE-2025-3277, CVE-2025-7709, and CVE-2025-52099.
Rust Codegen
Fixed-width types map directly to Rust primitive types:
| Assura | Rust |
|---|---|
| U8 | u8 |
| U16 | u16 |
| U32 | u32 |
| U64 | u64 |
| I32 | i32 |
| I64 | i64 |
Narrowing generates checked casts:
#![allow(unused)]
fn main() {
pub fn narrow_u64_to_u32(v: u64) -> u32 {
debug_assert!(v <= u32::MAX as u64);
v as u32
}
}
MEM.3 Allocator Contracts
Contracts for custom memory allocators that verify structural invariants: no overlap, no double-free, free-list acyclicity.
Grammar
AllocatorDecl = 'allocator' TypeIdent '<' TypeParam '>' '{'
{ AllocatorInvariant }
AllocateOp
FreeOp
'}' ;
AllocatorInvariant = 'invariant' ':' Predicate ;
AllocateOp = 'allocate' '{' { OperationItem } '}' ;
FreeOp = 'free' '{' { OperationItem } '}' ;
Full Example: Buddy Allocator
allocator BuddyAllocator<BufferSize: Nat> {
type Block {
offset: {v: Nat | v < BufferSize},
size: {v: Nat | is_power_of_2(v) and v > 0},
allocated: Bool
}
type FreeList = List<Block>
state {
buffer: Region<BufferSize>,
blocks: Set<Block>,
free_lists: Map<Nat, FreeList> // size -> free blocks of that size
}
// No two blocks overlap
invariant {
forall b1, b2 in blocks:
b1 != b2 =>
b1.offset + b1.size <= b2.offset
or b2.offset + b2.size <= b1.offset
}
// All blocks fit in the buffer
invariant {
forall b in blocks: b.offset + b.size <= BufferSize
}
// Free list is acyclic (no cycle)
invariant {
forall size in free_lists.keys():
is_acyclic(free_lists[size])
}
// Free list only contains unallocated blocks
invariant {
forall size in free_lists.keys():
forall b in free_lists[size]:
b.allocated == false
}
// Total allocated + free == BufferSize
invariant {
sum(b.size for b in blocks) == BufferSize
}
allocate {
input(requested_size: {v: Nat | v > 0 and v <= BufferSize})
output(block: Block :_1)
ensures { block.size >= round_up_power_of_2(requested_size) }
ensures { block.allocated == true }
ensures { block in blocks }
effects: pure
}
free {
input(block: Block :_1)
requires { block.allocated == true }
requires { block in blocks }
ensures { block.allocated == false }
ensures { block in free_lists[block.size] }
effects: pure
}
}
Rust Codegen
Allocator contracts generate a Rust struct implementing
std::alloc::Allocator (unstable) or a custom allocator trait:
#![allow(unused)]
fn main() {
pub struct BuddyAllocator {
buffer: Box<[u8]>,
free_lists: [Vec<Block>; MAX_ORDER],
}
impl BuddyAllocator {
pub fn allocate(&mut self, size: usize) -> Option<&mut [u8]> {
let order = size.next_power_of_two().trailing_zeros() as usize;
debug_assert!(order < MAX_ORDER);
// ... buddy allocation logic ...
}
pub fn free(&mut self, block: Block) {
debug_assert!(block.allocated);
debug_assert!(!self.has_overlap(&block));
// ... return to free list, coalesce buddies ...
}
}
}
MEM.4 Circular Buffer Contracts
Contracts for ring buffers and sliding windows that track wrap-around semantics, logical-to-physical index mapping, and slide operations.
Motivation
zlib’s sliding window (deflate_state->window) is a circular
buffer where the write pointer wraps around, valid history may
be less than window size, and fill_window() physically slides
data by copying the second half to the first and adjusting all
hash chain pointers. This pattern also appears in network stacks
(TCP receive buffers), audio processing (sample ring buffers),
database WAL buffers, and kernel I/O schedulers.
MEM.1 handles linear buffer bounds but cannot express wrap-around indexing, the relationship between physical layout and logical sequence, or the correctness of slide operations that relocate data and update dependent pointers.
Grammar
CircularBufferDecl = 'circular_buffer' TypeIdent '<' TypeParam '>'
'{' CircularBufferBody '}' ;
CircularBufferBody = 'capacity' ':' Expr ','
'write_pos' ':' Ident ','
'valid_count' ':' Ident ','
{ CircularBufferInvariant }
[ SlideOp ] ;
CircularBufferInvariant = 'invariant' ':' Predicate ;
SlideOp = 'slide' '{' { OperationItem } '}' ;
Full Example: zlib Sliding Window
circular_buffer DeflateWindow<WSize: Nat> {
storage: Region<u8>[WSize * 2], // physical: 2x for slide
capacity: WSize,
write_pos: wnext, // next write position (wraps)
valid_count: whave, // bytes of valid history
// Structural invariants
invariant: whave <= WSize
invariant: wnext < WSize * 2
// Logical view: the last `whave` bytes written
ghost fn logical_view(self) -> Seq<u8> {
if wnext >= whave {
self.storage[(wnext - whave)..wnext]
} else {
// Wrap-around case
self.storage[(WSize * 2 + wnext - whave)..] ++
self.storage[..wnext]
}
}
// Read at logical offset (0 = oldest valid)
fn read(self, offset: {v: Nat | v < self.whave}) -> u8
ensures result == self.logical_view()[offset]
{
let phys = (wnext - whave + offset) % (WSize * 2)
self.storage[phys]
}
// Slide operation: move second half to first, adjust pointers
slide {
requires { wnext >= WSize }
ensures { logical_view(post) == logical_view(pre) }
ensures { wnext(post) == wnext(pre) - WSize }
ensures { whave(post) == min(whave(pre), WSize) }
// All dependent pointers adjusted by -WSize
ensures {
forall p in dependent_pointers:
p(post) == max(0, p(pre) - WSize)
}
}
}
Verification Rules
- Every index into the buffer must be reduced modulo capacity or proven within physical bounds
- The
logical_viewghost function must be consistent across wrap-around boundaries - After a
slideoperation, the logical sequence is preserved (bytes in the same logical order, just physically relocated) - Dependent pointers (hash chain entries in zlib) must all be adjusted by the same offset after a slide
- Valid count never exceeds capacity
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-MEM-015 | Circular buffer index out of bounds | Access at offset >= valid_count |
| A-MEM-016 | Slide precondition violated | Slide called when write_pos < capacity |
| A-MEM-017 | Slide breaks logical view | Post-slide logical sequence != pre-slide |
| A-MEM-018 | Dependent pointer not adjusted | Pointer into buffer not updated after slide |
Rust Codegen
Circular buffer contracts generate a Rust struct with modular indexing and a slide method with debug assertions:
#![allow(unused)]
fn main() {
pub struct DeflateWindow {
storage: Box<[u8]>, // 2 * wsize
wsize: usize,
wnext: usize,
whave: usize,
}
impl DeflateWindow {
pub fn read(&self, offset: usize) -> u8 {
debug_assert!(offset < self.whave);
let phys = (self.wnext + self.storage.len()
- self.whave + offset) % self.storage.len();
self.storage[phys]
}
pub fn slide(&mut self, hash_heads: &mut [u16], hash_prev: &mut [u16]) {
debug_assert!(self.wnext >= self.wsize);
// Copy second half to first
self.storage.copy_within(self.wsize..self.wsize * 2, 0);
// Adjust all hash chain pointers
for h in hash_heads.iter_mut() {
*h = h.saturating_sub(self.wsize as u16);
}
for p in hash_prev.iter_mut() {
*p = p.saturating_sub(self.wsize as u16);
}
self.wnext -= self.wsize;
self.whave = self.whave.min(self.wsize);
}
}
}
14.TYPE: Types and Contracts
TYPE.1 Interface Contracts
Contracts on trait objects, vtables, and dynamically dispatched functions. Every implementation must satisfy the interface contract.
Grammar
InterfaceDecl = 'interface' TypeIdent [TypeParams] '{'
{ InterfaceMethod }
{ InvariantDecl }
'}' ;
InterfaceMethod = 'fn' Ident '(' [ParamList] ')' '->' TypeExpr
{ InterfaceClause } ;
InterfaceClause = RequiresClause | EnsuresClause | EffectsClause ;
ImplDecl = 'impl' TypeIdent 'for' TypeIdent '{'
{ FnDecl }
'}' ;
Full Example: Virtual File System
interface VirtualFileSystem {
type FileHandle
fn open(path: String, flags: OpenFlags) -> FileHandle | IOError
ensures { result is FileHandle => result.is_valid() }
effects: filesystem.open
fn read(
fh: FileHandle,
buf: Region<n> :_1,
amount: {a: Nat | a <= n},
offset: Nat
) -> (Region<n> :_1, {bytes_read: Nat | bytes_read <= amount})
requires { fh.is_valid() }
effects: filesystem.read
fn write(
fh: FileHandle,
data: Region<n>,
offset: Nat
) -> {bytes_written: Nat | bytes_written <= n}
requires { fh.is_valid() }
effects: filesystem.write
fn lock(fh: FileHandle, level: LockLevel) -> Bool
requires { valid_lock_upgrade(fh.current_lock(), level) }
ensures { result == true => fh.current_lock() == level }
effects: filesystem.lock
fn unlock(fh: FileHandle, level: LockLevel) -> Bool
requires { level < fh.current_lock() }
ensures { result == true => fh.current_lock() == level }
effects: filesystem.lock
fn sync(fh: FileHandle, full: Bool) -> Bool
requires { fh.is_valid() }
effects: filesystem.sync
// Lock ordering invariant (typestate on FileHandle)
invariant {
forall fh: valid_lock_upgrade(from, to) =>
(from == Unlocked and to == Shared) or
(from == Shared and to == Reserved) or
(from == Reserved and to == Exclusive)
}
}
// Platform-specific implementations
impl UnixVfs for VirtualFileSystem {
// Must satisfy ALL interface contracts
// Compiler verifies each method independently
}
impl WindowsVfs for VirtualFileSystem {
// Different implementation, same contracts
}
Verification Rule
When verifying impl Foo for Interface:
- Each method’s implementation is checked against the interface method’s requires/ensures/effects
- The implementation may have STRONGER preconditions (contravariance)
- The implementation may have WEAKER postconditions (covariance)
- The implementation must not use effects beyond what the interface declares
Rust Codegen
Interface contracts map to Rust traits:
#![allow(unused)]
fn main() {
pub trait VirtualFileSystem {
type FileHandle;
fn open(&self, path: &str, flags: OpenFlags)
-> Result<Self::FileHandle, IOError>;
fn read(&self, fh: &Self::FileHandle, buf: &mut [u8], offset: u64)
-> Result<usize, IOError>;
fn write(&self, fh: &Self::FileHandle, data: &[u8], offset: u64)
-> Result<usize, IOError>;
fn lock(&self, fh: &Self::FileHandle, level: LockLevel) -> bool;
fn unlock(&self, fh: &Self::FileHandle, level: LockLevel) -> bool;
fn sync(&self, fh: &Self::FileHandle, full: bool) -> bool;
}
}
TYPE.2 Recursive Structural Invariants
Invariants that hold recursively across tree, graph, and linked data structures.
Grammar
StructuralInvariant = 'structural_invariant' TypeIdent
[TypeParams] '{'
{ InvariantClause }
'}' ;
Full Example: B-Tree
type BTreeNode<K, V, Level: Nat> {
keys: Vec<K, n> where n >= 1 and n <= 2 * ORDER - 1,
values: Vec<V, n>,
children: Vec<BTreeNode<K, V, Level - 1>, n + 1>
where Level > 0
-- Leaf nodes (Level == 0) have no children
}
structural_invariant BTreeValid<K, V, Level: Nat> {
// 1. Keys are sorted within each node
forall node: BTreeNode<K, V, Level>:
forall i in 0..len(node.keys)-1:
node.keys[i] < node.keys[i+1]
// 2. Subtree ordering: left < key < right
forall node: BTreeNode<K, V, Level> where Level > 0:
forall i in 0..len(node.keys):
max_key(node.children[i]) < node.keys[i]
forall i in 0..len(node.keys):
min_key(node.children[i+1]) > node.keys[i]
// 3. All leaves at same depth (balanced)
depth(leftmost_leaf) == depth(rightmost_leaf)
// 4. Minimum occupancy (except root)
forall node where node != root:
len(node.keys) >= ORDER - 1
// 5. Key count matches value count
forall node:
len(node.keys) == len(node.values)
// 6. Child count is key count + 1 (internal nodes)
forall node where Level > 0:
len(node.children) == len(node.keys) + 1
}
// Measures for recursive properties
measure max_key<K, V, Level>(node: BTreeNode<K, V, Level>) -> K
max_key(leaf) = last(leaf.keys) where Level == 0
max_key(internal) = max_key(last(internal.children))
measure min_key<K, V, Level>(node: BTreeNode<K, V, Level>) -> K
min_key(leaf) = first(leaf.keys) where Level == 0
min_key(internal) = min_key(first(internal.children))
measure depth<K, V, Level>(node: BTreeNode<K, V, Level>) -> Nat
depth(leaf) = 0 where Level == 0
depth(internal) = 1 + depth(first(internal.children))
Verification Approach
Structural invariants are verified by induction over the data structure:
- Base case: Verify the invariant holds for leaf nodes
- Inductive step: Assuming the invariant holds for all subtrees, verify it holds for the parent
This maps to Z3 queries with quantified axioms over the measure functions. Budget: layer 2, 10s timeout.
TYPE.3 Error Propagation Contracts
Contracts that govern how errors flow through the call stack, preventing silent error swallowing, error masking, and error code translation mistakes.
Motivation
SQLite has ~30 error codes (SQLITE_OK through SQLITE_NOTICE)
with dozens of extended codes. Error handling is the #1 source of
subtle bugs in C systems code:
- Catching
SQLITE_CORRUPTand returningSQLITE_OK(hiding database corruption from the caller) - Translating
SQLITE_NOMEMtoSQLITE_ERROR(losing the actionable information that memory is full) - Ignoring the return value of
sqlite3_reset()(it returns the error from the lastsqlite3_step(), not its own status) - Calling
sqlite3_errmsg()after a second API call (the error message is overwritten)
Effects (Section 3) track what side effects occur but not how errors propagate. Transactional rollback (STOR.4) handles the undo path but not the error code correctness. Error propagation contracts bridge this gap.
Grammar
ErrorPropDecl = 'error_policy' TypeIdent '{'
{ ErrorRule }
'}' ;
ErrorRule = 'must_propagate' ':' ErrorCodeList
| 'must_not_mask' ':' ErrorCodePattern
| 'may_translate' ':' ErrorTranslation
| 'must_check' ':' IdentList
| 'must_preserve_detail' ':' ErrorCodeList ;
ErrorCodeList = ErrorCode { ',' ErrorCode } ;
ErrorTranslation = ErrorCode '->' ErrorCode
['when' Predicate] ;
ErrorCodePattern = ErrorCode '->' ErrorCode // forbidden translation ;
PropagateAnnotation = '#[propagate(' ErrorCode ')]'
| '#[swallow(' ErrorCode ',' StringLit ')]' ;
Full Example: SQLite Error Propagation
// Global error policy for the SQLite port
error_policy SqliteErrors {
// These errors MUST propagate to the caller. They cannot be
// caught and turned into SQLITE_OK.
must_propagate:
SQLITE_CORRUPT, // database corruption
SQLITE_NOTADB, // not a database file
SQLITE_NOMEM, // out of memory
SQLITE_IOERR, // disk I/O error
SQLITE_FULL // disk full
// Error masking rules: these translations are FORBIDDEN
must_not_mask:
SQLITE_CORRUPT -> SQLITE_OK, // hiding corruption
SQLITE_CORRUPT -> SQLITE_ERROR, // downgrading corruption
SQLITE_NOMEM -> SQLITE_ERROR, // losing OOM detail
SQLITE_IOERR -> SQLITE_OK // hiding I/O failure
// Allowed translations (explicit and documented)
may_translate:
SQLITE_BUSY -> SQLITE_LOCKED
when holding_table_lock
SQLITE_CONSTRAINT -> SQLITE_CONSTRAINT_UNIQUE
when constraint_type == Unique
SQLITE_CONSTRAINT -> SQLITE_CONSTRAINT_FOREIGNKEY
when constraint_type == ForeignKey
SQLITE_IOERR -> SQLITE_IOERR_READ
when operation == Read
SQLITE_IOERR -> SQLITE_IOERR_WRITE
when operation == Write
// These function return values MUST be checked by the caller
must_check:
sqlite3_reset, // returns error from last step
sqlite3_finalize, // returns error from last step
sqlite3_close, // returns BUSY if statements open
sqlite3_exec // returns error from callback or SQL
// These errors must preserve their detail across translations
// (extended error code must survive)
must_preserve_detail:
SQLITE_IOERR, // keep IOERR_READ vs IOERR_WRITE
SQLITE_CONSTRAINT // keep CONSTRAINT_UNIQUE vs FK
}
// Using error propagation contracts
fn read_page_from_disk(
fd: FileDescriptor,
page_num: U32
) -> Region<PageSize> | SqlError
#[propagate(SQLITE_IOERR)]
#[propagate(SQLITE_CORRUPT)]
effects: filesystem.read
{
let bytes = read_bytes(fd, page_num * PageSize, PageSize)?
// ? propagates SQLITE_IOERR from read_bytes
if not validate_page(bytes) {
return SqlError(SQLITE_CORRUPT, "page checksum mismatch")
}
bytes
}
// COMPILE ERROR: masking corruption
fn bad_error_handling(
fd: FileDescriptor,
page_num: U32
) -> Region<PageSize>
effects: filesystem.read
{
match read_page_from_disk(fd, page_num) {
Ok(bytes) => bytes,
Err(e) => {
// A48002: SQLITE_CORRUPT cannot be masked as SQLITE_OK
// by returning a default page
Region.zeroed() // DANGEROUS: pretending corrupt page is OK
}
}
}
// COMPILE ERROR: ignoring must-check return value
fn bad_cleanup(stmt: Statement :_1)
effects: database.read
{
stmt.reset()
// A48004: return value of sqlite3_reset not checked
// (it carries the error from the last sqlite3_step)
}
// CORRECT: check and propagate
fn good_cleanup(
stmt: Statement :_1
) -> Statement :_1 | SqlError
effects: database.read
{
let result = stmt.reset()
match result {
Ok(s) => s,
Err(e) => {
// Log the error from last step, return it
log_error(e)
return Err(e)
}
}
}
// Error detail preservation
fn handle_constraint_error(
err: SqlError
) -> SqlError
requires { err.code == SQLITE_CONSTRAINT }
ensures {
// Extended code must survive
result.extended_code == err.extended_code
// A48005: must_preserve_detail violated if extended code is lost
}
{
SqlError {
code: err.code,
extended_code: err.extended_code, // preserve!
message: format_constraint_message(err),
}
}
// Swallowing an error explicitly (documented escape hatch)
fn optional_optimization(db: Database :_1) -> Database :_1
effects: database.read
{
// Explicitly documented: we try an optimization, but it's OK
// if it fails (we fall back to the slow path)
#[swallow(SQLITE_BUSY, "optimization hint, slow path is fine")]
let _ = db.try_wal_checkpoint(WAL_CHECKPOINT_PASSIVE)
db // Continue regardless
}
Verification Rule
- Must-propagate: The compiler traces every error path. If a
must_propagateerror is caught and not re-raised, it is a compile error - Must-not-mask: The compiler checks every error translation (match arm, map_err, ? with conversion). Forbidden translations are compile errors
- Must-check: Every call to a
must_checkfunction must have its return value inspected. Discarding the result (includinglet _ = ...without#[swallow]) is a compile error - Detail preservation: Error translations that lose the
extended error code for
must_preserve_detailerrors are compile errors - Explicit swallow: The
#[swallow]annotation documents intentional error suppression with a justification string. Reviewers and auditors can search for all swallowed errors
Error Codes
| Code | Message | Cause |
|---|---|---|
| A48001 | Must-propagate error E swallowed | Catching and hiding a critical error |
| A48002 | Error E masked as F | Forbidden error translation |
| A48003 | Error detail lost: extended code dropped | must_preserve_detail violation |
| A48004 | Return value of F not checked | Ignoring must-check function result |
| A48005 | Undocumented error swallow | Error suppressed without #[swallow] annotation |
Rust Codegen
Error propagation contracts generate Rust error types with
compile-time checking via must_use and custom lints:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
#[must_use = "SqlError must be handled, not silently dropped"]
pub struct SqlError {
pub code: ErrorCode,
pub extended_code: ExtendedErrorCode,
pub message: String,
}
// Critical errors use a wrapper that panics on drop if not handled
#[derive(Debug)]
#[must_use = "Critical errors cannot be silently dropped"]
pub struct CriticalError(SqlError);
impl Drop for CriticalError {
fn drop(&mut self) {
if !std::thread::panicking() {
panic!(
"Critical error {} dropped without handling: {}",
self.0.code, self.0.message,
);
}
}
}
impl CriticalError {
pub fn handle(self) -> SqlError {
let err = self.0.clone();
std::mem::forget(self); // disarm the drop bomb
err
}
}
// Must-check functions return MustUse wrapper
#[must_use = "sqlite3_reset return value must be checked"]
pub fn sqlite3_reset(stmt: &mut Statement) -> Result<(), SqlError> {
// ...
}
// Error conversion with masking prevention
impl From<IoError> for SqlError {
fn from(e: IoError) -> Self {
// Preserves IO error detail
SqlError {
code: ErrorCode::SQLITE_IOERR,
extended_code: match e.kind() {
ErrorKind::NotFound => ExtendedErrorCode::IOERR_READ,
ErrorKind::PermissionDenied => ExtendedErrorCode::IOERR_WRITE,
_ => ExtendedErrorCode::IOERR,
},
message: e.to_string(),
}
}
}
}
14.SEC: Trust and Security
SEC.1 Untrusted Data Taint
A taint tracking system for data that crosses trust boundaries.
Data from disk, network, or user input is untrusted until
explicitly validated.
Motivation
4 of 9 SQLite CVEs were in modules (FTS, R-Tree, JSON, Session) that parse binary data stored in shadow tables. The root cause: these modules trust on-disk metadata (BLOBs) without sufficient validation. The Black Hat 2017 “Many Birds, One Stone” attack exploited this single trust assumption across multiple modules.
Taint Labels
// Taint is a separate axis from security labels
// Security labels: who can SEE the data (confidentiality)
// Taint labels: whether the data is TRUSTED (integrity)
taint_label untrusted // data from outside the trust boundary
taint_label validated // data that passed validation
taint_label trusted // data from within the trust boundary
Grammar
TaintAnnotation = '@taint:' TaintLabel ;
TaintLabel = 'untrusted' | 'validated' | 'trusted' ;
ValidateExpr = 'validate' '{' Predicate '}' Expr ;
Taint Propagation Rules
// All data from disk/network/user input is untrusted
fn read_from_disk(path: String) -> Bytes @taint:untrusted
effects: filesystem.read
// Untrusted data cannot be used where trusted data is expected
fn process(data: Bytes @taint:trusted) -> Result
// COMPILE ERROR if called with @taint:untrusted data
// Validation converts untrusted to validated
fn validate_blob(
data: Bytes @taint:untrusted
) -> ValidBlob @taint:validated | ParseError
effects: pure
{
validate {
len(data) >= HEADER_SIZE
and read_u32(data, 0) == MAGIC_NUMBER
and read_u32(data, 4) <= MAX_SEGMENT_SIZE
} data
}
Taint + Information Flow Interaction
Taint (integrity) and security labels (confidentiality) are orthogonal. Data can be:
@Restricted @taint:trusted(sensitive, from our own database)@Public @taint:untrusted(non-sensitive, from network)@Restricted @taint:untrusted(sensitive user input, not yet validated)
Both axes must be satisfied: data must be at or below the security label AND at or above the taint level.
SQLite Example: FTS Shadow Table
fn read_fts_segment(
db: Database,
segment_id: U64
) -> FtsSegment | CorruptionError
effects: database.read
{
let raw: Bytes @taint:untrusted = db.read_blob("fts_segments", segment_id)
// Every field must be validated before use
let header_size = validate { raw.len() >= 16 } raw.read_u32(0)
or return CorruptionError("header too small")
let segment_size = validate { header_size <= MAX_SEGMENT } header_size
or return CorruptionError("segment too large")
let content = validate { raw.len() >= segment_size } raw.slice(16, segment_size)
or return CorruptionError("truncated segment")
FtsSegment { header_size, content }
// FtsSegment is @taint:validated -- safe to use
}
Error Codes
| Code | Message | Cause |
|---|---|---|
| A28001 | Untrusted data used as trusted | Missing validation |
| A28002 | Validation predicate insufficient | Validation doesn’t cover all fields |
| A28003 | Taint escapes through aliasing | Trusted alias of untrusted data |
Rust Codegen
Taint labels erase at runtime (same as security labels). The compiler has already verified all validation paths. Generated Rust includes debug_assert checks on validation predicates:
#![allow(unused)]
fn main() {
pub fn read_fts_segment(db: &Database, segment_id: u64)
-> Result<FtsSegment, CorruptionError>
{
let raw = db.read_blob("fts_segments", segment_id)?;
if raw.len() < 16 {
return Err(CorruptionError::new("header too small"));
}
let header_size = u32::from_be_bytes(raw[0..4].try_into().unwrap()) as usize;
if header_size > MAX_SEGMENT {
return Err(CorruptionError::new("segment too large"));
}
if raw.len() < header_size {
return Err(CorruptionError::new("truncated segment"));
}
let content = &raw[16..header_size];
Ok(FtsSegment { header_size, content: content.to_vec() })
}
}
SEC.2 FFI Boundary Contracts
Contracts for functions exposed to or called from foreign languages (C, Python, Java via JNI). These define the safety contract at the boundary between verified Assura code and unverified foreign callers.
Motivation
SQLite’s entire value comes from its C API. A Rust port must expose
sqlite3_open(), sqlite3_exec(), etc. as extern "C" functions.
The Rust side is verified; the C caller is not. The FFI contract
defines what the caller must guarantee and what the callee promises,
making the boundary explicit rather than implicit.
Grammar
FfiDecl = 'ffi' StringLit TypeIdent '{'
{ FfiFunction }
'}' ;
FfiFunction = 'export' Ident '(' [FfiParamList] ')'
'->' FfiReturnType
'{' { FfiClause } '}' ;
FfiParam = Ident ':' FfiType [FfiAnnotation] ;
FfiType = 'ptr' '<' TypeExpr '>' // raw pointer
| 'nullable_ptr' '<' TypeExpr '>'
| 'cstring' // null-terminated
| 'buffer' '<' Ident '>' // ptr + length pair
| 'opaque' // void*
| 'c_int' | 'c_uint' | 'c_long'
| 'size_t' ;
FfiAnnotation = '#[not_null]'
| '#[null_terminated]'
| '#[valid_for(' Ident ')]'
| '#[caller_frees]'
| '#[callee_frees]'
| '#[borrowed(' Lifetime ')]' ;
FfiClause = 'caller_guarantees' ':' Predicate
| 'callee_guarantees' ':' Predicate
| 'error_convention' ':' FfiErrorConvention
| 'thread_safety' ':' ThreadSafetyLevel ;
FfiErrorConvention = 'return_code' '(' IntLit '=' Ident
{ ',' IntLit '=' Ident } ')'
| 'null_on_error'
| 'errno' ;
ThreadSafetyLevel = 'single_threaded'
| 'serialized' // safe to call from any thread
| 'multi_threaded' // safe if different connections
;
Full Example: SQLite C API
ffi "C" SqliteApi {
// sqlite3_open(filename, **ppDb) -> int
export sqlite3_open(
filename: cstring #[not_null] #[null_terminated],
ppDb: ptr<ptr<Connection>> #[not_null]
) -> c_int {
caller_guarantees:
filename points to valid null-terminated UTF-8
callee_guarantees:
result == SQLITE_OK =>
*ppDb is a valid Connection pointer
result != SQLITE_OK =>
*ppDb may or may not be set
// Caller MUST call sqlite3_close(*ppDb) even on error
// if *ppDb is non-null (this is a real SQLite requirement)
error_convention: return_code(
0 = SQLITE_OK,
7 = SQLITE_NOMEM,
14 = SQLITE_CANTOPEN
)
thread_safety: serialized
}
// sqlite3_exec(db, sql, callback, arg, errmsg) -> int
export sqlite3_exec(
db: ptr<Connection> #[not_null]
#[valid_for(connection_lifetime)],
sql: cstring #[not_null] #[null_terminated],
callback: nullable_ptr<ExecCallback>,
callback_arg: opaque,
errmsg: nullable_ptr<ptr<c_char>> #[callee_frees]
) -> c_int {
caller_guarantees:
db was returned by sqlite3_open and not yet closed
and sql is valid UTF-8 SQL
callee_guarantees:
result == SQLITE_OK =>
all SQL statements in sql were executed
result == SQLITE_ABORT =>
callback returned non-zero (user cancelled)
result != SQLITE_OK and errmsg != null =>
*errmsg points to a malloc'd error string
that the caller must free with sqlite3_free
error_convention: return_code(
0 = SQLITE_OK,
4 = SQLITE_ABORT,
1 = SQLITE_ERROR
)
thread_safety: multi_threaded
// Safe if no other thread uses the same db connection
}
// sqlite3_close(db) -> int
export sqlite3_close(
db: nullable_ptr<Connection>
) -> c_int {
caller_guarantees:
db is null or was returned by sqlite3_open
and no other thread is using db
and all prepared statements are finalized
callee_guarantees:
result == SQLITE_OK => db is freed, pointer is invalid
result == SQLITE_BUSY => db is NOT freed, still valid
// Caller must finalize statements and retry
error_convention: return_code(
0 = SQLITE_OK,
5 = SQLITE_BUSY
)
thread_safety: single_threaded
}
// sqlite3_prepare_v2(db, sql, nByte, **ppStmt, *pzTail) -> int
export sqlite3_prepare_v2(
db: ptr<Connection> #[not_null],
sql: buffer<nByte> #[not_null],
nByte: c_int,
ppStmt: ptr<ptr<Statement>> #[not_null],
pzTail: nullable_ptr<ptr<c_char>>
) -> c_int {
caller_guarantees:
db is a valid open connection
and sql points to nByte bytes of valid UTF-8
and (nByte >= 0 or nByte == -1)
// nByte == -1 means read until null terminator
callee_guarantees:
result == SQLITE_OK =>
*ppStmt is a valid prepared statement
and (pzTail != null => *pzTail points to first byte
past the end of the first SQL statement in sql)
result != SQLITE_OK =>
*ppStmt is null
error_convention: return_code(0 = SQLITE_OK, 1 = SQLITE_ERROR)
thread_safety: multi_threaded
}
}
Verification Rule
- Callee side: The Assura compiler verifies that the
implementation satisfies all
callee_guaranteesgiven thecaller_guaranteesas axioms - Caller side: When Assura code calls an FFI function, the
compiler verifies that all
caller_guaranteesare met - Lifetime tracking:
#[valid_for]annotations create phantom lifetimes in the generated Rust code - Null safety:
ptr<T>with#[not_null]generatesNonNull<T>in Rust;nullable_ptr<T>generates*mut T - Memory ownership:
#[caller_frees]and#[callee_frees]annotations prevent double-free and leak
Error Codes
| Code | Message | Cause |
|---|---|---|
| A37001 | FFI caller guarantee not provable at call site | Caller can’t prove pointer validity |
| A37002 | FFI callee guarantee not satisfied by implementation | Implementation violates promised postcondition |
| A37003 | FFI memory ownership conflict | Double-free or leak at FFI boundary |
| A37004 | FFI null pointer not checked | Nullable pointer used without null check |
| A37005 | FFI thread safety violation | Function called from wrong threading context |
Rust Codegen
FFI contracts generate extern "C" functions with safety wrappers:
#![allow(unused)]
fn main() {
/// # Safety
/// - `filename` must be non-null, valid null-terminated UTF-8
/// - `ppDb` must be non-null and point to valid memory for *mut Db
#[no_mangle]
pub unsafe extern "C" fn sqlite3_open(
filename: *const c_char,
ppDb: *mut *mut Connection,
) -> c_int {
// Validate caller guarantees (debug only)
debug_assert!(!filename.is_null());
debug_assert!(!ppDb.is_null());
let filename = match CStr::from_ptr(filename).to_str() {
Ok(s) => s,
Err(_) => {
*ppDb = std::ptr::null_mut();
return SQLITE_CANTOPEN;
}
};
match Connection::open(filename) {
Ok(conn) => {
*ppDb = Box::into_raw(Box::new(conn));
SQLITE_OK
}
Err(e) => {
// SQLite contract: ppDb may still be set on error
*ppDb = std::ptr::null_mut();
e.to_sqlite_code()
}
}
}
}
SEC.3 Constant-Time Execution
Contracts that verify a function’s execution time is independent of secret inputs, preventing timing side-channel attacks.
Motivation
WireGuard’s crypto_memneq() compares MACs using XOR accumulation
with no early exit. Curve25519 uses a constant-time Montgomery ladder.
If any branch depends on a secret value, an attacker can measure
execution time to extract the key. No other Assura feature addresses
this: taint tracking verifies where data flows, not how computation
behaves on it.
Grammar
ConstTimeAnnotation = '#[constant_time]' ;
SecretAnnotation = '#[secret]' ;
ConstTimeBlock = 'constant_time' '{' Block '}' ;
Full Example
#[constant_time]
fn mac_verify(
computed: Bytes #[secret],
received: Bytes
) -> Bool
requires computed.len() == received.len()
ensures result == (computed == received)
{
// Compiler rejects: early-exit comparison
// Compiler rejects: secret-dependent array index
// Compiler rejects: secret-dependent branch
let mut acc: U8 = 0
for i in 0..computed.len() {
acc = acc | (computed[i] ^ received[i])
}
acc == 0
}
fn curve25519_scalarmult(
scalar: U256 #[secret],
point: Point
) -> Point
{
// Montgomery ladder: both branches execute identical operations
constant_time {
let mut r0 = IDENTITY
let mut r1 = point
for bit in (0..256).rev() {
let b = scalar.bit(bit) // secret-dependent value
// cswap executes same instructions regardless of b
cswap(b, &mut r0, &mut r1)
r1 = point_add(r0, r1)
r0 = point_double(r0)
cswap(b, &mut r0, &mut r1)
}
r0
}
}
Verification Rule
The verifier performs information-flow analysis on #[secret] data:
- No branch condition may depend on secret data
- No array index may depend on secret data
- No loop bound may depend on secret data
- No variable-time instruction (division, modulo on some architectures) on secret data
constant_time { }blocks are verified as a unit
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-SEC-010 | Secret-dependent branch | if/match condition depends on #[secret] value |
| A-SEC-011 | Secret-dependent index | Array index computed from #[secret] value |
| A-SEC-012 | Variable-time operation on secret | Division or modulo on #[secret] data |
| A-SEC-013 | Secret leaks through timing | Function not marked #[constant_time] uses #[secret] |
| A-SEC-014 | Non-constant-time call in constant_time block | Calling unmarked function from constant_time block |
Rust Codegen
#[constant_time] generates normal Rust code; the timing guarantee is
verified at compile time. The compiler may additionally emit:
core::hint::black_box()around secret comparisons- Platform-specific constant-time intrinsics (e.g.,
subtle::ConstantTimeEq)
#![allow(unused)]
fn main() {
fn mac_verify(computed: &[u8], received: &[u8]) -> bool {
debug_assert_eq!(computed.len(), received.len());
let mut acc = 0u8;
for i in 0..computed.len() {
acc |= computed[i] ^ received[i];
}
core::hint::black_box(acc) == 0
}
}
SEC.4 Secure Erasure
Contracts that guarantee secret data is zeroed in memory when no longer needed, preventing key material from lingering.
Motivation
WireGuard calls memzero_explicit() on all handshake keys after
wg_noise_handshake_begin_session(). Linear types ensure single-use
but not zeroing: a key consumed by linearity could be freed without
overwriting. The compiler’s dead-store elimination may optimize away
a memset(key, 0, 32) if it sees no subsequent read.
Grammar
SecureEraseAnnotation = '#[secure_erase]' ;
EraseExpr = 'erase' '(' Expr ')' ;
Full Example
type SessionKey #[secure_erase] {
key: Bytes(32),
nonce_counter: U64
}
fn handshake_complete(
hs: Handshake #[linear]
) -> SessionKey
ensures memory_zeroed(hs)
{
let session = derive_session(hs.chaining_key, hs.hash)
erase(hs) // compiler emits volatile zero + barrier
session
}
fn session_expired(key: SessionKey #[linear]) -> ()
ensures memory_zeroed(key)
{
erase(key)
}
Verification Rule
Types marked #[secure_erase] must be erased before deallocation:
- Every code path that drops the value must call
erase()first erase()emits a volatile write of zeros + compiler barrier- Copying a
#[secure_erase]value is forbidden (linear by default) - Passing to a function transfers the erasure obligation
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-SEC-015 | Secret dropped without erasure | #[secure_erase] value dropped without erase() |
| A-SEC-016 | Secret copied | #[secure_erase] value cannot be copied |
| A-SEC-017 | Erasure may be optimized out | erase() not using volatile write |
Rust Codegen
erase() emits zeroize::Zeroize trait call or inline volatile
write with a compiler fence:
#![allow(unused)]
fn main() {
impl Drop for SessionKey {
fn drop(&mut self) {
// Volatile write prevents dead-store elimination
unsafe {
core::ptr::write_volatile(&mut self.key as *mut [u8; 32], [0u8; 32]);
}
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
}
SEC.5 Cryptographic Specification Conformance
Contracts that verify a cryptographic implementation correctly implements its mathematical specification, connecting C/Rust code to formal algorithm definitions from standards documents.
Motivation
mbedTLS implements AES (FIPS 197), ECDSA (FIPS 186-5), RSA (PKCS#1 v2.2), and dozens of other algorithms. A wrong constant in the NIST P-256 curve parameters, a subtle error in modular reduction, or an off-by-one in the key schedule would silently produce incorrect cryptographic output. Existing features provide building blocks (CORE.4 axioms define the math, CORE.2 lemmas prove properties, NUM.2 verifies precomputed tables), but no feature expresses the top-level claim: “this function correctly implements AES-128 as defined by FIPS 197.”
This is distinct from FMT.6 (Protocol Grammar Conformance), which verifies parsers against ABNF/BNF grammars. Cryptographic conformance verifies algorithms against algebraic/mathematical specifications. The verification techniques differ: grammar conformance uses language-theoretic checking; crypto conformance uses algebraic reasoning and equational proofs.
Projects like HACL*, Fiat-Crypto, and Jasmin provide this level of assurance. Assura makes it a first-class language feature.
Grammar
CryptoConformanceAnnotation = '#[conforms' '(' SpecRef ')' ']' ;
SpecRef = StringLit ; // e.g., "FIPS_197_AES_128"
AlgorithmDecl = '#[conforms' '(' SpecRef ')' ']'
'fn' Ident '(' ParamList ')' '->' Type
'{' AlgorithmBody '}' ;
AlgorithmBody = { Statement } ;
Full Example: AES-128 Block Cipher
// Mathematical specification (axiomatic)
spec FIPS_197_AES_128 {
// The AES round function
axiom SubBytes(state: Matrix<4,4,GF256>) -> Matrix<4,4,GF256> {
forall i, j: state_out[i][j] == sbox(state_in[i][j])
}
axiom ShiftRows(state: Matrix<4,4,GF256>) -> Matrix<4,4,GF256> {
forall i, j: state_out[i][j] == state_in[i][(j + i) % 4]
}
axiom MixColumns(state: Matrix<4,4,GF256>) -> Matrix<4,4,GF256> {
forall j: column_out[j] == gf_matrix_mul(MIX_MATRIX, column_in[j])
}
axiom AddRoundKey(state: Matrix<4,4,GF256>,
round_key: Matrix<4,4,GF256>) -> Matrix<4,4,GF256> {
forall i, j: state_out[i][j] == state_in[i][j] xor round_key[i][j]
}
// Full AES-128: 10 rounds with specified structure
axiom encrypt(plaintext: Bytes(16), key: Bytes(16)) -> Bytes(16) {
let state = to_matrix(plaintext)
let round_keys = key_expansion(key) // 11 round keys
let s0 = AddRoundKey(state, round_keys[0])
let s_mid = fold(1..10, s0, fn(s, r) {
AddRoundKey(MixColumns(ShiftRows(SubBytes(s))), round_keys[r])
})
let s_final = AddRoundKey(ShiftRows(SubBytes(s_mid)), round_keys[10])
from_matrix(s_final)
}
}
// Implementation verified against the spec
#[conforms("FIPS_197_AES_128")]
fn aes_128_encrypt(
plaintext: &[u8; 16],
key: &[u8; 16]
) -> [u8; 16]
ensures result == FIPS_197_AES_128.encrypt(plaintext, key)
{
let mut state = load_state(plaintext)
let round_keys = expand_key(key)
state = xor_state(state, round_keys[0])
for r in 1..10 {
state = sub_bytes(state)
state = shift_rows(state)
state = mix_columns(state)
state = xor_state(state, round_keys[r])
}
state = sub_bytes(state)
state = shift_rows(state)
state = xor_state(state, round_keys[10])
store_state(state)
}
Verification Rules
- The
#[conforms(spec)]annotation binds the function to a named specification; the verifier must prove the function’s output matches the spec’s definition for all valid inputs - Helper functions (e.g.,
sub_bytes,mix_columns) may have their own#[conforms]annotations for compositional proof - Precomputed tables used by the implementation (S-boxes, round constants, curve parameters) are verified against the spec’s axiomatic definitions via NUM.2
- Optimized implementations (AESNI intrinsics, fast NIST reduction) must also conform; PERF.1 escape requires the same conformance proof as the reference implementation
- The spec itself is trusted (axiomatic); the implementation is verified against it
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-SEC-018 | Algorithm does not conform to spec | Implementation output differs from spec for some input |
| A-SEC-019 | Missing conformance spec | #[conforms] references undefined spec |
| A-SEC-020 | Helper does not conform | Sub-function used by conforming function fails its own spec |
| A-SEC-021 | Optimized path diverges | PERF.1 escape produces different output than spec |
Rust Codegen
Conformance contracts generate debug-mode comparison against a reference implementation derived from the spec:
#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
fn aes_128_encrypt_checked(plaintext: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
let result = aes_128_encrypt(plaintext, key);
let reference = aes_128_reference(plaintext, key);
debug_assert_eq!(result, reference,
"AES-128 conformance failure: implementation diverges from FIPS 197");
result
}
// Release mode: direct implementation, no runtime check
// (correctness proven at verification time)
#[cfg(not(debug_assertions))]
fn aes_128_encrypt_checked(plaintext: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
aes_128_encrypt(plaintext, key)
}
}
14.CONC: Concurrency
CONC.1 Shared Memory Protocols
Verification of concurrent access patterns across OS processes that share memory-mapped regions.
Motivation
SQLite’s WAL-Reset Bug (present for 16 years, 2010-2026) was a data
race between two processes accessing the -shm (shared memory)
file. It required precise timing to reproduce and was never caught
by testing, fuzzing, or 100% MC/DC coverage. A formal model of the
shared memory locking protocol would have caught it.
Grammar
SharedMemoryDecl = 'shared_memory' TypeIdent '{'
LayoutDecl
{ ProtocolDecl }
{ SharedInvariant }
'}' ;
LayoutDecl = 'layout' '{' { LayoutField } '}' ;
LayoutField = Ident ':' SharedFieldType ';' ;
SharedFieldType = 'Lock'
| 'Atomic' '<' TypeExpr '>'
| 'Array' '<' TypeExpr ',' IntLit '>'
| TypeExpr ;
SharedInvariant = 'shared_invariant' ':' Predicate ;
ProtocolDecl = 'protocol' TypeIdent '{'
{ ProtocolStep }
'}' ;
ProtocolStep = 'acquire' '(' Ident ')'
| 'release' '(' Ident ')'
| 'atomic_load' '(' Ident ')'
| 'atomic_store' '(' Ident ',' Expr ')'
| 'read' '(' Ident ')'
| 'write' '(' Ident ',' Expr ')'
| ProtocolStep '->' ProtocolStep ;
Type System
Shared memory protocols are verified using multiparty session types extended to shared memory. The compiler models each participant (process) as a party and verifies:
- No data race: Reads and writes to non-atomic fields are protected by locks
- Lock ordering: Locks are acquired in a consistent order across all protocols (no deadlock)
- Atomic correctness: Atomic operations use appropriate memory ordering
- Protocol compliance: Each process follows its declared protocol
Full Example: WAL Protocol
shared_memory WalIndex {
layout {
write_lock: Lock;
checkpoint_lock: Lock;
recover_lock: Lock;
read_locks: Array<Lock, 5>;
max_frame: Atomic<U32>;
max_page: Atomic<U32>;
frame_checksums: Array<U64, MAX_WAL_FRAMES>;
checkpoint_seq: Atomic<U32>;
}
// Writer protocol
protocol Writer {
acquire(write_lock)
-> read(frame_checksums) // read under write lock
-> write(frame_checksums, new) // append new frames
-> atomic_store(max_frame, new_max) // publish atomically
-> release(write_lock)
}
// Reader protocol
protocol Reader {
atomic_load(max_frame) // snapshot max_frame
-> acquire(read_locks[slot]) // mark our snapshot slot
-> read(frame_checksums) // read frames up to snapshot
-> release(read_locks[slot])
}
// Checkpoint protocol
protocol Checkpointer {
acquire(checkpoint_lock)
-> atomic_load(max_frame)
-> for_each read_locks[i]: // wait for readers past checkpoint
wait_until(read_locks[i].snapshot >= checkpoint_frame)
-> write(database_file, pages) // copy WAL pages to DB
-> atomic_store(checkpoint_seq, seq + 1)
-> release(checkpoint_lock)
}
// COMPILER VERIFIES:
// 1. Writer and Reader never write the same non-atomic field
// without lock protection
// 2. Checkpointer waits for all readers before overwriting
// 3. Lock ordering: write_lock > checkpoint_lock > read_locks
// (no deadlock possible)
// 4. max_frame is always written by Writer before read by Reader
// (no stale snapshot)
shared_invariant:
forall frame_index in 0..max_frame:
frame_checksums[frame_index] == compute_checksum(wal_file, frame_index)
}
Verification Approach
Multi-process protocols are verified using bounded model checking (Kani/CBMC-style) at layer 2. The compiler explores all interleavings of protocol steps up to a configurable bound:
[verify.shared_memory]
interleaving_bound = 100 # max interleaving steps to explore
process_count = 3 # max concurrent processes
timeout_ms = 30000 # 30s budget for model checking
Error Codes
| Code | Message | Cause |
|---|---|---|
| A29001 | Data race on shared field F | Unprotected concurrent access |
| A29002 | Deadlock possible | Lock ordering violation |
| A29003 | Stale read: F may be modified between load and use | Missing lock or atomic |
| A29004 | Protocol violation: step S out of order | Process doesn’t follow protocol |
| A29005 | Reader may see partial write | Non-atomic multi-field update |
Rust Codegen
Shared memory protocols generate Rust code using platform-specific primitives:
#![allow(unused)]
fn main() {
use std::sync::atomic::{AtomicU32, Ordering};
use memmap2::MmapMut;
pub struct WalIndex {
mmap: MmapMut,
}
impl WalIndex {
pub fn max_frame(&self) -> u32 {
let ptr = &self.mmap[MAX_FRAME_OFFSET] as *const u8 as *const AtomicU32;
unsafe { (*ptr).load(Ordering::Acquire) }
}
pub fn set_max_frame(&self, val: u32) {
let ptr = &self.mmap[MAX_FRAME_OFFSET] as *const u8 as *const AtomicU32;
unsafe { (*ptr).store(val, Ordering::Release) }
}
}
}
The compiler generates correct Ordering annotations (Acquire for
loads, Release for stores) based on the protocol analysis.
CONC.2 Callback and Re-entrancy Safety
Contracts on user-supplied callbacks that restrict what operations the callback may perform, preventing re-entrancy bugs and deadlocks.
Motivation
SQLite has 12+ callback hooks: authorizer, busy handler, progress handler, commit hook, rollback hook, update hook, WAL hook, collation callback, function callback, etc. If a callback calls back into the database (e.g., executing a query inside the authorizer), the result is corruption or deadlock. SQLite documents these restrictions in prose; Assura enforces them at compile time.
Grammar
CallbackDecl = 'callback' TypeIdent '{'
'signature' ':' FnType
{ CallbackConstraint }
'}' ;
CallbackConstraint = 'must_not_call' ':' IdentList
| 'must_not_reenter' ':' IdentList
| 'max_duration' ':' DurationExpr
| 'may_call' ':' IdentList
| 'must_be' ':' CallbackProperty ;
CallbackProperty = 'pure' | 'deterministic' | 'infallible'
| 'idempotent' | 'thread_safe' ;
CallbackInstall = 'on_install' ':' Predicate ;
Full Example: SQLite Callbacks
// Authorizer: called during query compilation
callback Authorizer {
signature: (action: AuthAction, arg1: String?,
arg2: String?, db_name: String?,
trigger: String?) -> AuthResult
// MUST NOT call any database operation (re-entrancy)
must_not_reenter: Connection
// MUST NOT allocate from the database's allocator
must_not_call: sqlite3_malloc, sqlite3_free
// Must complete quickly (called per-statement-node)
must_be: infallible
// May only read connection configuration
may_call: sqlite3_db_config_read
}
// Busy handler: called when a lock cannot be acquired
callback BusyHandler {
signature: (context: Opaque, count: Nat) -> Bool
// MUST NOT call operations that acquire locks
must_not_call: sqlite3_exec, sqlite3_step, sqlite3_prepare
// MUST NOT re-enter the connection that triggered it
must_not_reenter: Connection
// Should eventually return false (termination)
// (not enforced at compile time, but generates a warning)
}
// Progress handler: called periodically during long queries
callback ProgressHandler {
signature: (context: Opaque) -> Bool
// May call read-only operations on OTHER connections
may_call: sqlite3_exec where connection != self.connection
// MUST NOT modify the connection that installed it
must_not_reenter: Connection
must_be: thread_safe
}
// Collation callback: called for string comparison
callback CollationCallback {
signature: (a: Region<n>, b: Region<m>) -> Ordering
must_be: deterministic // Same inputs must give same ordering
must_be: pure // No side effects
must_be: infallible // Cannot fail
// Transitivity: if a < b and b < c then a < c
invariant {
forall a, b, c:
compare(a, b) == Less and compare(b, c) == Less
=> compare(a, c) == Less
}
// Antisymmetry: if a < b then b > a
invariant {
forall a, b:
compare(a, b) == Less <=> compare(b, a) == Greater
}
// Reflexivity: a == a
invariant { forall a: compare(a, a) == Equal }
}
// Installing a callback
service Database {
operation set_authorizer {
input(auth: Authorizer)
// Only one authorizer at a time
ensures { self.authorizer == Some(auth) }
// Previous authorizer is replaced (linear: old one dropped)
ensures { old(self.authorizer) is dropped }
on_install: self.state @ Open
}
}
Verification Rule
- When a callback type is used, the compiler collects all
must_not_callandmust_not_reenterconstraints - Any implementation of the callback is checked: its call graph
must not include any function in the
must_not_calllist - Re-entrancy is checked by verifying the callback’s transitive
call graph does not include any method on the
must_not_reentertarget must_be: deterministictriggers determinism analysis (see CONC.3)must_be: pureverifies no effects beyondpure- Callback invariants (e.g., transitivity for collation) are checked via SMT at Layer 2
Error Codes
| Code | Message | Cause |
|---|---|---|
| A34001 | Callback C re-enters T via call chain F1->F2->... | Re-entrancy detected in transitive call graph |
| A34002 | Callback C calls prohibited function F | Direct or indirect call to must_not_call target |
| A34003 | Callback C is not deterministic | Non-deterministic operation in callback body |
| A34004 | Callback C may fail but is marked infallible | Error path exists in callback body |
| A34005 | Callback invariant not satisfiable | Transitivity/antisymmetry not provable |
Rust Codegen
Callback contracts generate trait bounds with marker types:
#![allow(unused)]
fn main() {
/// Marker: callback must not re-enter Connection
pub trait NoReenter<T> {}
/// Authorizer callback trait
pub trait Authorizer: NoReenter<Connection> + Send {
fn authorize(
&self,
action: AuthAction,
arg1: Option<&str>,
arg2: Option<&str>,
db_name: Option<&str>,
trigger: Option<&str>,
) -> AuthResult;
// No Result<> return: infallible
}
/// Collation callback trait
pub trait Collation: Send + Sync {
fn compare(&self, a: &[u8], b: &[u8]) -> std::cmp::Ordering;
// Pure + deterministic: no &mut self, no interior mutability
}
impl Connection {
pub fn set_authorizer<A: Authorizer>(&mut self, auth: A) {
debug_assert!(self.state == ConnectionState::Open);
self.authorizer = Some(Box::new(auth));
}
}
}
CONC.3 Determinism Contracts
Contracts that guarantee identical output for identical input, which is stronger than purity. Pure functions have no side effects; deterministic functions additionally produce bit-identical results across invocations, platforms, and compiler versions.
Motivation
SQLite guarantees that the same query on the same database produces the same result bytes. This is critical for:
- Replication (replicas must agree)
- Testing (golden file tests)
- Digital signatures over query results
- WAL checksums (same data must produce same checksum)
Rust’s HashMap iteration order is non-deterministic (randomized
seed). Floating-point operations can differ across platforms.
Instant::now() introduces time-dependence. A pure function
could use any of these.
Grammar
DeterministicAnnotation = '#[deterministic]' ;
DeterministicConstraints = 'deterministic_requires' '{'
{ DeterministicRule }
'}' ;
DeterministicRule = 'no_hash_iteration'
| 'no_float_transcendentals'
| 'no_pointer_comparison'
| 'no_allocation_address'
| 'no_time_dependence'
| 'no_random'
| 'ordered_collections_only'
| 'fixed_float_rounding' ;
Full Example: Deterministic Query Execution
#[deterministic]
fn execute_select(
db: Database,
stmt: PreparedStatement,
params: List<Value>
) -> List<Row>
requires { stmt.is_valid() }
requires { db.state @ Open }
ensures {
// Same db + same stmt + same params = same result, always
forall db1, db2, params1, params2:
db1.content == db2.content
and params1 == params2
=> execute_select(db1, stmt, params1)
== execute_select(db2, stmt, params2)
}
effects: database.read
{
// Inside a #[deterministic] function, the compiler rejects:
// - HashMap (use BTreeMap)
// - HashSet (use BTreeSet)
// - Instant::now() or any time source
// - thread::current().id()
// - pointer-to-integer casts
// - address-dependent comparisons
// - f64::sin/cos/exp (platform-dependent rounding)
// unless #[fixed_float_rounding] is active
}
#[deterministic]
fn wal_checksum(
data: Region<n>,
seed: (U32, U32)
) -> (U32, U32)
effects: pure
{
// Checksum must be deterministic so crash recovery can
// verify frames written by a different process
let (mut s1, mut s2) = seed
for i in 0..n/4 {
let word = read_u32_native(data, i * 4)
s1 = s1 +% word // wrapping add
s2 = s2 +% s1
}
(s1, s2)
}
// COMPILE ERROR: non-deterministic function marked deterministic
#[deterministic]
fn bad_example(items: Map<String, Int>) -> List<String>
effects: pure
{
items.keys() // A35001: Map iteration order is not deterministic
// Use BTreeMap or sort the keys explicitly
}
Banned Patterns
The compiler maintains a list of non-deterministic operations. Any
call to these from a #[deterministic] function is a compile error:
| Pattern | Why Non-Deterministic | Alternative |
|---|---|---|
HashMap / HashSet | Randomized hash seed | BTreeMap / BTreeSet |
f64::sin, f64::exp | Platform-dependent rounding | Fixed-point or #[fixed_float_rounding] |
Instant::now() | Time-dependent | Pass timestamp as parameter |
thread_rng() | Random | Pass seed as parameter |
ptr as usize | Address-dependent | Use indices, not pointers |
Arc::as_ptr comparison | Allocation-dependent | Compare by value |
TypeId::of | Compiler-dependent | Use explicit discriminant |
Verification Rule
- The compiler performs taint analysis: any non-deterministic source taints all downstream values
- A
#[deterministic]function’s return value must not be tainted - Calling a non-deterministic function from a deterministic one is an error, even if the result is unused (may affect control flow)
#[deterministic]impliespure(no side effects)#[deterministic]is transitive: all callees must also be deterministic
Error Codes
| Code | Message | Cause |
|---|---|---|
| A35001 | Non-deterministic collection in deterministic context | HashMap/HashSet used |
| A35002 | Platform-dependent float in deterministic context | Transcendental function used |
| A35003 | Time/random source in deterministic context | Instant, SystemTime, or RNG used |
| A35004 | Pointer-derived value in deterministic context | Address used in computation |
| A35005 | Callee F is not deterministic | Calling non-deterministic function |
Rust Codegen
Determinism contracts are primarily compile-time. The generated Rust code uses lint attributes to catch accidental non-determinism:
#![allow(unused)]
fn main() {
// Deterministic function: generated with ordered collections
#[cfg_attr(debug_assertions, track_caller)]
pub fn execute_select(
db: &Database,
stmt: &PreparedStatement,
params: &[Value],
) -> Vec<Row> {
// Compiler ensures BTreeMap is used internally, not HashMap
let mut results: BTreeMap<RowId, Row> = BTreeMap::new();
// ... query execution ...
results.into_values().collect()
}
// Deterministic checksum
#[inline]
pub fn wal_checksum(data: &[u8], seed: (u32, u32)) -> (u32, u32) {
let (mut s1, mut s2) = seed;
for chunk in data.chunks_exact(4) {
let word = u32::from_ne_bytes(chunk.try_into().unwrap());
s1 = s1.wrapping_add(word);
s2 = s2.wrapping_add(s1);
}
(s1, s2)
}
}
CONC.4 Lock Ordering
Contracts that specify a ranked hierarchy of locks and verify no code path acquires them out of order, preventing deadlocks.
Motivation
jemalloc defines ~30 ranked lock levels (witness system in
witness.c). witness_lock_error_impl() aborts on rank reversal at
runtime. Shared memory (CONC.1) verifies data-race freedom on
individual variables; it does not express “mutex A (rank 45) must be
acquired before mutex B (rank 0x1000)” across a call graph.
Grammar
LockRankDecl = 'lock_rank' Ident '=' IntLit ';' ;
LockAnnotation = '#[lock_rank(' Ident ')]' ;
LockOrderRule = 'lock_order' '{' LockRankDecl { LockRankDecl } '}' ;
Full Example
lock_order {
lock_rank CORE = 0
lock_rank BIN = 10
lock_rank ARENA = 20
lock_rank EXTENT = 30
lock_rank TCACHE = 40
lock_rank PROF = 50
}
type BinMutex #[lock_rank(BIN)] { inner: Mutex }
type ArenaMutex #[lock_rank(ARENA)] { inner: Mutex }
fn arena_bin_alloc(
arena: &Arena,
bin: &Bin
) -> Ptr
requires held_locks_below(BIN)
{
arena.lock.acquire() // rank ARENA = 20
bin.lock.acquire() // rank BIN = 10, ERROR: 10 < 20
}
Verification Rule
- Each lock acquisition records the rank in a ghost set
- Acquiring a lock with rank <= max(held_ranks) is an error
- Releasing a lock removes it from the ghost set
- Equal-rank locks may use address ordering (annotated explicitly)
- Call graph analysis propagates lock requirements across functions
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-CONC-010 | Lock order violation | Acquiring lock with rank <= held max |
| A-CONC-011 | Missing lock rank annotation | Mutex without #[lock_rank] |
| A-CONC-012 | Equal-rank without address order | Same-rank locks without ordering tiebreaker |
Rust Codegen
Lock ordering is verified at compile time and erased. In debug mode, a runtime witness system mirrors jemalloc’s approach:
#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
thread_local! {
static HELD_RANKS: RefCell<Vec<u32>> = RefCell::new(Vec::new());
}
fn acquire_lock(lock: &Mutex, rank: u32) {
#[cfg(debug_assertions)]
HELD_RANKS.with(|ranks| {
let ranks = ranks.borrow();
if let Some(&max) = ranks.iter().max() {
debug_assert!(rank > max, "lock order violation: {rank} <= {max}");
}
});
lock.lock();
#[cfg(debug_assertions)]
HELD_RANKS.with(|ranks| ranks.borrow_mut().push(rank));
}
}
CONC.5 Temporal State Deadlines
Contracts that bind state transitions to time bounds, verifying that time-triggered actions occur within their deadlines.
Motivation
WireGuard rekeying is timer-driven: REKEY_AFTER_TIME = 120s,
REJECT_AFTER_TIME = 180s. After 180 seconds, a keypair MUST be
zeroed. Typestate tracks which transitions are valid; monotonic state
tracks values that increase. Neither expresses “after T seconds in
state S, must transition to state S’.” WireGuard’s timers.c has 5
concurrent per-peer timers with interacting deadlines.
Grammar
DeadlineAnnotation = '#[deadline(' Duration ')]' ;
TimeoutTransition = 'timeout' '(' State ',' Duration ')' '->' State ;
Duration = IntLit ('ms' | 's' | 'min') ;
Full Example
states KeypairState {
Created -> Active -> Expired -> Zeroed
}
transition keypair_lifecycle {
timeout(Active, 120s) -> Expired // REKEY_AFTER_TIME
timeout(Expired, 60s) -> Zeroed // REJECT_AFTER_TIME - REKEY
timeout(Created, 5s) -> Zeroed // REKEY_TIMEOUT
}
contract WireGuardPeer {
invariant keypair.state != Active
|| elapsed(keypair.activated_at) < 180s
invariant keypair.state == Zeroed
==> memory_zeroed(keypair.key)
}
Verification Rule
- Timer-triggered transitions are verified against the state machine
- The verifier checks that all timer handlers exist and transition to the declared target state
- Overlapping deadlines are detected (two timers on same state)
- Missing timeout handlers generate compile errors
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-CONC-013 | Missing timeout handler | State has deadline but no timer handler |
| A-CONC-014 | Deadline exceeded | State held beyond declared timeout |
| A-CONC-015 | Conflicting deadlines | Two timeouts on same state |
Rust Codegen
Deadlines generate timer registration and handler dispatch:
#![allow(unused)]
fn main() {
impl Peer {
fn activate_keypair(&mut self, keypair: Keypair) {
self.keypair = keypair;
self.timers.set(Timer::Rekey, Duration::from_secs(120));
self.timers.set(Timer::Reject, Duration::from_secs(180));
}
fn handle_timer(&mut self, timer: Timer) {
match timer {
Timer::Rekey => self.initiate_rekey(),
Timer::Reject => {
self.keypair.zeroize();
self.keypair_state = KeypairState::Zeroed;
}
}
}
}
}
CONC.6 Weak Memory Ordering
Contracts for programs that use non-sequential-consistency atomic orderings. Instead of a single global shared state, each thread maintains a ghost view of memory. Atomic operations with different orderings (Relaxed, Acquire, Release, AcqRel, SeqCst) generate different ghost view constraints.
Motivation
Every high-performance Rust concurrent data structure uses weak memory orderings. crossbeam-epoch uses Relaxed loads for fast pinning checks. parking_lot uses Relaxed loads in spin loops. arc-swap uses Acquire-Release pairs. seqlock uses Relaxed loads with explicit fences.
Without weak memory support, Assura can only verify under sequential consistency, which is sound (a program correct under SeqCst is correct under weaker orderings) but incomplete (programs that are correct under Acquire-Release may not be expressible under SeqCst, because the SeqCst model adds unnecessary ordering constraints).
Syntax
// Atomic operations carry their ordering annotation
// (matching Rust's std::sync::atomic)
shared x: AtomicU64 {
// Per-thread views: each thread has its own view of x
ghost view: Map<ThreadId, ViewTimestamp>
}
fn writer(x: &AtomicU64, val: u64) {
// Release store: publishes all prior writes to x's view
x.store(val, ordering: release)
// Ghost effect: x.view[self_tid] = merge(x.view[self_tid], self.view)
// All writes before this point become visible to any
// thread that does an acquire load of x
}
fn reader(x: &AtomicU64) -> u64 {
// Acquire load: merges x's published view into this thread's view
let v = x.load(ordering: acquire)
// Ghost effect: self.view = merge(self.view, x.view[writer_tid])
// All writes the writer did before its release store
// are now visible to this thread
ensures v == x.value // value is consistent with the view
}
fn fast_check(x: &AtomicU64) -> u64 {
// Relaxed load: no view synchronization
let v = x.load(ordering: relaxed)
// Ghost effect: none. This thread's view is NOT updated.
// The value may be stale (from this thread's current view)
ensures v == x.view[self_tid].value_of(x)
}
The View Model
Each thread carries a ghost view map: a partial function from memory locations to the last-seen write timestamp. Atomic operations update views according to their ordering:
| Ordering | Ghost View Effect |
|---|---|
relaxed | No view change. Read from this thread’s current view |
acquire (load) | Merge source location’s published view into reader’s view |
release (store) | Publish writer’s current view to the location |
acq_rel (RMW) | Both: merge then publish |
seq_cst | Total order maintained. All SeqCst ops see a single global view |
fence(acquire) | Merge all prior relaxed loads’ source views |
fence(release) | Publish current view to all subsequent relaxed stores |
The view model is based on the GPS/RSL (Relaxed Separation Logic) approach, adapted for SMT verification.
Example: seqlock Reader
struct SeqLock<T> {
seq: AtomicU64, // even = unlocked, odd = write in progress
data: T,
}
fn read(lock: &SeqLock<T>) -> T {
loop {
let s1 = lock.seq.load(ordering: acquire)
requires s1 % 2 == 0 // must read even (unlocked)
fence(ordering: acquire)
let result = lock.data.clone()
let s2 = lock.seq.load(ordering: acquire)
if s1 == s2 {
// No writer intervened: result is consistent
ensures result == lock.data.at_view(self.view)
return result
}
// Writer intervened: retry. The relaxed observation of
// an odd sequence number means the data may be torn.
}
}
Example: crossbeam Epoch Pin Check
fn is_pinned(guard: &Guard) -> bool {
// Fast path: relaxed load is safe because we only need
// a hint. If stale, the worst case is an extra epoch advance.
let epoch = GLOBAL_EPOCH.load(ordering: relaxed)
// Under weak memory, this might read a stale epoch.
// The contract says: the result is valid for this thread's
// current view, but may not reflect other threads' advances.
ensures result == (guard.local_epoch == epoch)
|| stale_view(self.view, GLOBAL_EPOCH)
}
Verification Rule
- View tracking: The verifier maintains a ghost view per thread. Each atomic operation generates view constraints based on its ordering annotation
- Consistency check: The verifier checks that all read values are consistent with the reading thread’s current view. A Relaxed load may read any value that was written and not yet overwritten in the thread’s view
- Happens-before: An acquire-load from location X after a release-store to X creates a happens-before edge. The verifier checks that all assertions that depend on happens-before are reachable only through valid edges
- Layer 1: View tracking for Acquire-Release is decidable (QF_UFLIA). Relaxed with fences may require Layer 2
- Interaction with CONC.1: The shared memory protocol (CONC.1) specifies the protocol-level state machine. CONC.6 specifies the memory-ordering semantics WITHIN that protocol. They compose: the protocol says “state A transitions to state B via this CAS”; CONC.6 says “the CAS uses AcqRel ordering, so views merge accordingly”
Error Codes
| Code | Message | Cause |
|---|---|---|
| A23016 | Relaxed read without view check | Read depends on value ordering but uses Relaxed |
| A-CONC-017 | Missing release before acquire | Data written without Release read with Acquire |
| A-CONC-018 | View inconsistency | Thread asserts value visible but view has not merged |
| A23019 | Fence ordering mismatch | Fence type does not match surrounding operations |
| A-CONC-020 | SeqCst total order violation | SeqCst operations form a cycle in total order |
Rust Codegen
CONC.6 contracts compile to the same Rust atomic operations the user specified. The ordering annotations are preserved exactly. In debug builds, view assertions generate runtime checks:
#![allow(unused)]
fn main() {
// Source: x.store(val, ordering: release)
// Generated Rust:
x.store(val, Ordering::Release);
// Source: x.load(ordering: acquire)
// Generated Rust:
let v = x.load(Ordering::Acquire);
// Debug build: optional view consistency assertion
#[cfg(debug_assertions)]
{
// View merge simulation for testing
debug_assert!(
THREAD_VIEW.lock().unwrap().is_consistent_with(v),
"CONC.6: acquire load returned value inconsistent with view"
);
}
}
14.STOR: Storage and Durability
STOR.1 Crash Recovery Contracts
Contracts that specify what happens when a system crashes at any point during a multi-step operation. Unlike typestate (which tracks normal state transitions), crash recovery reasons about non-deterministic failure points and the procedure that restores invariants on restart.
Motivation
SQLite’s WAL commit has ~12 distinct crash points. If power fails between writing a WAL frame and updating the WAL index, the recovery algorithm must detect the inconsistency and replay valid frames. SQLite’s rollback journal has a similar protocol. These protocols were designed by hand and verified by crash-testing. Assura makes them verifiable at compile time.
Grammar
RecoveryDecl = 'recovery' TypeIdent '{'
RecoveryStateDecl
{ CrashPointDecl }
RecoveryProc
RecoveryInvariant
'}' ;
RecoveryStateDecl = 'durable_state' '{' { FieldDecl } '}' ;
CrashPointDecl = 'crash_point' Ident '{'
'at' ':' StringLit
'observable' ':' IdentList
'recovers_to' ':' Ident
'}' ;
RecoveryProc = 'recover' '{' { OperationItem } '}' ;
RecoveryInvariant = 'post_recovery' ':' Predicate ;
Full Example: WAL Commit Recovery
recovery WalCommit {
// What is persisted on disk (survives crash)
durable_state {
wal_file: File,
db_file: File,
wal_index: SharedMemory<WalIndexHeader>,
frames: List<WalFrame>
}
// Crash point 1: frame written, index not updated
crash_point frame_written_index_stale {
at: "after fsync(wal_file), before updating wal_index"
observable: wal_file contains frame F
but wal_index.max_frame < F.frame_number
recovers_to: consistent
}
// Crash point 2: index updated, not fsynced
crash_point index_updated_not_synced {
at: "after wal_index update, before fsync(wal_index)"
observable: wal_index.max_frame >= F.frame_number
but index may contain garbage beyond valid frames
recovers_to: consistent
}
// Crash point 3: checkpoint in progress
crash_point checkpoint_partial {
at: "during checkpoint, some pages copied to db_file"
observable: db_file has mix of old and new pages
recovers_to: consistent
}
// Crash point 4: WAL reset after checkpoint
crash_point wal_reset_partial {
at: "after checkpoint complete, during WAL truncate"
observable: WAL file may be truncated but salt not updated
recovers_to: consistent
}
// Recovery procedure: runs on database open
recover {
input(
wal: File,
db: File,
index: SharedMemory<WalIndexHeader>
)
// Step 1: Reconstruct valid frame set from WAL file
// Walk WAL from beginning, verify each frame's checksum
// Stop at first invalid checksum (crash boundary)
requires { wal.exists() }
// Step 2: Rebuild WAL index from valid frames
ensures {
forall f in valid_frames(wal):
index.contains(f.page_number, f.frame_number)
}
// Step 3: Any page in db_file is either:
// - the version from before the transaction, OR
// - the version from the last fully committed transaction
ensures {
forall page_num in 1..db.page_count():
page_content(db, page_num) == original_content(page_num)
or page_content(db, page_num) == committed_content(page_num)
}
effects: filesystem.read, filesystem.write
}
// After recovery, the database is always consistent
post_recovery:
database_invariant(db) and
wal_index_matches_wal(index, wal) and
no_partial_transactions_visible(db, wal)
}
Verification Approach
Crash recovery is verified using bounded model checking (BMC):
- Enumerate all crash points in the protocol
- For each crash point, symbolically execute the recovery procedure
- Verify that
post_recoveryholds after recovery from each point - Verify that no crash point can produce a state where recovery is impossible (liveness)
Budget: Layer 2, 10s per crash point. Total budget scales linearly with the number of declared crash points.
Error Codes
| Code | Message | Cause |
|---|---|---|
| A32001 | Crash point P has no recovery path | Recovery procedure doesn’t handle this state |
| A32002 | Recovery may not restore invariant I | Post-recovery predicate not provable |
| A32003 | Durable state modified without crash point | Write to disk without declaring what happens on crash |
| A32004 | Recovery procedure has side effects beyond repair | Recovery does more than restore consistency |
Rust Codegen
Crash recovery contracts generate recovery functions with exhaustive state detection:
#![allow(unused)]
fn main() {
pub struct WalRecovery {
wal_path: PathBuf,
db_path: PathBuf,
}
impl WalRecovery {
pub fn recover(&self) -> Result<(), RecoveryError> {
let wal = File::open(&self.wal_path)?;
let valid_frames = self.scan_valid_frames(&wal)?;
// Rebuild index from valid frames only
let index = WalIndex::rebuild_from(&valid_frames)?;
// Verify post-recovery invariant in debug mode
debug_assert!(self.verify_consistency(&index, &valid_frames));
Ok(())
}
fn scan_valid_frames(&self, wal: &File)
-> Result<Vec<WalFrame>, RecoveryError>
{
let mut frames = Vec::new();
let mut offset = WAL_HEADER_SIZE;
while offset < wal.metadata()?.len() as usize {
match WalFrame::read_and_verify(wal, offset) {
Ok(frame) => {
frames.push(frame);
offset += WAL_FRAME_SIZE;
}
Err(_) => break, // Crash boundary: stop here
}
}
Ok(frames)
}
}
}
STOR.2 Page Cache Contracts
Contracts for reference-counted page caches with pin/unpin semantics, dirty tracking, and eviction policies.
Motivation
SQLite’s pager layer is the most complex subsystem after the B-tree. Every database page passes through a cache with these semantics:
- Fetch: get a page from cache or read from disk
- Pin: mark a page as in-use (cannot be evicted)
- Unpin: release a page (eligible for eviction)
- MakeDirty: mark a page as modified (must be written back)
- MakeClean: mark a page as written (can be discarded)
- Evict: remove unpinned clean pages under memory pressure
Bugs in this layer are catastrophic: evicting a dirty page loses data, using an evicted page is use-after-free, double-unpin corrupts the reference count. Allocator contracts (MEM.3) handle memory pools but cannot express pin/unpin reference counting, dirty tracking, or the interaction between eviction policy and transaction safety.
Grammar
CacheDecl = 'cache' TypeIdent '<' TypeParam '>' '{'
CacheCapacity
{ CacheInvariant }
{ CacheOperation }
'}' ;
CacheCapacity = 'capacity' ':' Expr ;
CacheOperation = 'fetch' | 'pin' | 'unpin'
| 'make_dirty' | 'make_clean' | 'evict' ;
PinState = 'pinned' '(' Ident ')' | 'unpinned' '(' Ident ')' ;
DirtyState = 'dirty' '(' Ident ')' | 'clean' '(' Ident ')' ;
Full Example: SQLite Page Cache
cache PageCache<Page> {
capacity: config.cache_size // runtime-configurable
type CacheEntry {
page_number: U32,
data: Region<PageSize>,
pin_count: {v: Nat | v >= 0},
is_dirty: Bool,
in_journal: Bool // written to rollback journal
}
// Core invariants
invariant {
// Pinned pages cannot be evicted
forall entry in self.entries:
pinned(entry) => not evictable(entry)
}
invariant {
// Dirty pages cannot be evicted unless journaled
forall entry in self.entries:
dirty(entry) and not entry.in_journal
=> not evictable(entry)
}
invariant {
// Pin count matches number of active references
forall entry in self.entries:
entry.pin_count == count(active_refs(entry))
}
invariant {
// Cache size does not exceed capacity
// (only unpinned clean pages are evicted to maintain this)
count(self.entries where pinned(e) or dirty(e))
<= self.capacity
}
fetch {
input(page_num: U32)
output(entry: CacheEntry :_1)
ensures {
// Returned entry is pinned (pin_count >= 1)
pinned(entry)
}
ensures {
// If page was in cache, return cached version
// If not, read from disk and add to cache
entry.page_number == page_num
and entry.data == disk_page(page_num)
}
effects: filesystem.read
}
pin {
input(entry: CacheEntry)
requires { entry in self.entries }
ensures { entry.pin_count == old(entry.pin_count) + 1 }
ensures { pinned(entry) }
effects: pure
}
unpin {
input(entry: CacheEntry)
requires { pinned(entry) }
requires { entry.pin_count >= 1 }
ensures { entry.pin_count == old(entry.pin_count) - 1 }
// After unpin, page may or may not still be pinned
// (depends on whether other references exist)
ensures {
entry.pin_count == 0 => unpinned(entry)
}
effects: pure
}
make_dirty {
input(entry: CacheEntry)
requires { pinned(entry) }
// Cannot dirty an unpinned page (someone must be using it)
ensures { dirty(entry) }
ensures { entry.is_dirty == true }
effects: pure
}
make_clean {
input(entry: CacheEntry)
requires { dirty(entry) }
requires {
// Page must be written to disk or journal first
entry.data == disk_page(entry.page_number)
or entry.in_journal
}
ensures { clean(entry) }
ensures { entry.is_dirty == false }
effects: pure
}
evict {
input(entry: CacheEntry)
requires { unpinned(entry) }
requires { clean(entry) }
// CANNOT evict pinned or dirty pages
ensures { entry not in self.entries }
effects: pure
}
}
// COMPILE ERROR: using page after unpin without re-fetch
fn bad_page_use(cache: PageCache<Page>) -> Region<PageSize>
{
let entry = cache.fetch(42) // pinned
let data = entry.data
cache.unpin(entry) // unpinned
data // A44001: accessing data from unpinned cache entry
// (entry may be evicted, data is dangling)
}
// CORRECT: pin while using, unpin when done
fn good_page_use(
cache: PageCache<Page> :_1
) -> (PageCache<Page> :_1, Region<PageSize>)
{
let entry = cache.fetch(42) // pinned, pin_count = 1
let data = copy(entry.data) // copy data while pinned
cache.unpin(entry) // safe to evict now
(cache, data) // return copied data
}
Error Codes
| Code | Message | Cause |
|---|---|---|
| A44001 | Accessing data from unpinned cache entry | Use after unpin (may be evicted) |
| A44002 | Evicting pinned page | Pin count > 0 when evict called |
| A44003 | Evicting dirty unjournaled page | Data loss: dirty page not written |
| A44004 | Double unpin: pin count already zero | Unpin called more times than pin |
| A44005 | Dirtying unpinned page | make_dirty on entry with pin_count 0 |
Rust Codegen
Page cache contracts generate a cache struct with runtime pin tracking and debug assertions:
#![allow(unused)]
fn main() {
pub struct PageCache {
entries: HashMap<u32, CacheEntry>,
capacity: usize,
}
pub struct CacheEntry {
page_number: u32,
data: Box<[u8; PAGE_SIZE]>,
pin_count: AtomicU32,
is_dirty: bool,
in_journal: bool,
}
pub struct PinnedPage<'cache> {
entry: &'cache CacheEntry,
cache: &'cache PageCache,
}
impl PageCache {
pub fn fetch(&self, page_num: u32) -> PinnedPage<'_> {
let entry = self.entries.get(&page_num)
.unwrap_or_else(|| self.read_from_disk(page_num));
entry.pin_count.fetch_add(1, Ordering::Relaxed);
PinnedPage { entry, cache: self }
}
}
// PinnedPage auto-unpins on drop (RAII)
impl<'cache> Drop for PinnedPage<'cache> {
fn drop(&mut self) {
let prev = self.entry.pin_count.fetch_sub(1, Ordering::Relaxed);
debug_assert!(prev >= 1, "Double unpin on page {}",
self.entry.page_number);
}
}
impl<'cache> PinnedPage<'cache> {
pub fn data(&self) -> &[u8; PAGE_SIZE] {
&self.entry.data
}
pub fn make_dirty(&mut self) {
debug_assert!(self.entry.pin_count.load(Ordering::Relaxed) >= 1);
// ... mark dirty ...
}
}
}
STOR.3 MVCC and Snapshot Isolation
Contracts for multi-version concurrency control that guarantee readers see a consistent snapshot while writers modify data concurrently.
Motivation
SQLite’s WAL mode allows concurrent readers and a single writer. Each reader sees the database as it existed at the moment the read transaction started, even if the writer commits new data afterward. This is snapshot isolation: reader R at version V sees page P at version V, not version V+1 that the writer just committed.
Shared memory protocols (CONC.1) handle the atomic read/write of shared state. Snapshot isolation is a higher-level property: it defines which version of data each transaction can see, based on when the transaction started relative to commits.
Grammar
SnapshotDecl = 'snapshot' TypeIdent '{'
VersionType
SnapshotInvariant
{ SnapshotRule }
'}' ;
VersionType = 'version' ':' TypeExpr ;
SnapshotInvariant = 'isolation' ':' IsolationLevel ;
IsolationLevel = 'snapshot'
| 'serializable'
| 'read_committed'
| 'read_uncommitted' ;
SnapshotRule = 'visible' '(' Ident ',' Ident ')' ':'
Predicate ;
VersionRef = 'at_version' '(' Ident ',' Expr ')' ;
Full Example: WAL Snapshot Isolation
snapshot WalSnapshot {
version: U32 // monotonically increasing commit counter
isolation: snapshot
// A transaction sees the state at the version when it started
type Transaction {
start_version: U32,
is_writer: Bool,
// Writer sees its own uncommitted changes
pending_pages: Map<U32, Region<PageSize>>
}
// Core visibility rule: which page version does a reader see?
visible(txn: Transaction, page: PageNumber):
if page in txn.pending_pages and txn.is_writer {
// Writer sees its own pending changes
page_data == txn.pending_pages[page]
} else {
// Read the latest version <= txn.start_version
page_data == page_at_version(page, txn.start_version)
}
// Snapshot consistency: all pages within a transaction
// come from the same version
invariant {
forall txn: Transaction, p1: PageNumber, p2: PageNumber:
version_of(visible(txn, p1)) == version_of(visible(txn, p2))
// A reader never sees page 5 at version 10 and
// page 8 at version 11
}
// Writer exclusion: only one writer at a time
invariant {
count(active_transactions where is_writer) <= 1
}
// Version monotonicity: commits increase the version
invariant {
forall commit c1 before c2:
c1.version < c2.version
}
// No lost updates: a committed version is always readable
invariant {
forall committed_version v:
forall txn where txn.start_version >= v:
can_read(txn, v)
}
// Read-your-writes: writer sees its own pending changes
invariant {
forall txn where txn.is_writer:
forall page in txn.pending_pages:
visible(txn, page) == txn.pending_pages[page]
}
}
// Using snapshots in queries
fn read_consistent_pair(
txn: Transaction,
page_a: U32,
page_b: U32
) -> (Region<PageSize>, Region<PageSize>)
requires { not txn.is_writer }
ensures {
// Both pages are from the same snapshot
version_of(fst(result)) == version_of(snd(result))
and version_of(fst(result)) == txn.start_version
}
effects: database.read
{
let a = read_page(txn, page_a)
// Even if a writer commits between these two reads,
// txn still sees the old version of page_b
let b = read_page(txn, page_b)
(a, b)
}
// COMPILE ERROR: reading without a transaction
fn bad_read(db: Database, page: U32) -> Region<PageSize>
{
db.read_page(page)
// A45001: page read outside transaction context
// (no snapshot version, result may be inconsistent)
}
// COMPILE ERROR: write-write conflict detection
fn concurrent_writers(
db: Database :_1
) -> Database :_1
{
let txn1 = db.begin_write() // OK: first writer
let txn2 = db.begin_write() // A45003: writer already active
// ...
}
Verification Rule
- Snapshot consistency: The compiler verifies that all page reads within a transaction use the same version (the transaction’s start_version)
- Writer exclusion: At most one write transaction can be active. Starting a second writer is a compile error if the first hasn’t committed or rolled back
- No dirty reads: A reader never sees uncommitted writer data (unless the reader IS the writer)
- No phantom reads: The set of visible rows doesn’t change during a read transaction
- Version tracking: The compiler tracks which version each page reference belongs to and rejects mixing versions
Error Codes
| Code | Message | Cause |
|---|---|---|
| A45001 | Page read outside transaction context | No snapshot version for consistency |
| A45002 | Mixed versions in single transaction | Pages from different snapshots |
| A45003 | Concurrent writer conflict | Second writer while first is active |
| A45004 | Stale snapshot: version V no longer available | WAL checkpoint removed old version |
| A45005 | Write to read-only transaction | Modifying page in non-writer txn |
Rust Codegen
Snapshot isolation generates transaction wrappers with version tracking:
#![allow(unused)]
fn main() {
pub struct ReadTransaction<'db> {
db: &'db Database,
snapshot_version: u32,
}
pub struct WriteTransaction<'db> {
db: &'db mut Database, // exclusive borrow: one writer
snapshot_version: u32,
pending: HashMap<u32, Box<[u8; PAGE_SIZE]>>,
}
impl<'db> ReadTransaction<'db> {
pub fn read_page(&self, page_num: u32) -> &[u8; PAGE_SIZE] {
// Always reads from snapshot_version, never newer
self.db.wal.page_at_version(page_num, self.snapshot_version)
}
}
impl<'db> WriteTransaction<'db> {
pub fn read_page(&self, page_num: u32) -> &[u8; PAGE_SIZE] {
// Writer sees its own pending changes first
if let Some(page) = self.pending.get(&page_num) {
return page;
}
self.db.wal.page_at_version(page_num, self.snapshot_version)
}
pub fn write_page(&mut self, page_num: u32, data: [u8; PAGE_SIZE]) {
self.pending.insert(page_num, Box::new(data));
}
pub fn commit(self) -> Result<(), CommitError> {
// Atomically advance version and write pending pages to WAL
self.db.wal.commit(self.snapshot_version, &self.pending)
}
}
impl Database {
pub fn begin_read(&self) -> ReadTransaction<'_> {
ReadTransaction {
db: self,
snapshot_version: self.wal.current_version(),
}
}
// &mut self guarantees at most one writer (Rust borrow checker)
pub fn begin_write(&mut self) -> WriteTransaction<'_> {
WriteTransaction {
db: self,
snapshot_version: self.wal.current_version(),
pending: HashMap::new(),
}
}
}
}
STOR.4 Transactional Rollback
Contracts that guarantee atomic success-or-rollback semantics for multi-step operations. If any step fails (OOM, I/O error, constraint violation), the entire operation is undone and the system returns to its pre-operation state.
Motivation
SQLite handles out-of-memory gracefully: every sqlite3_malloc()
call can fail, and the system rolls back to a consistent state.
This is extraordinarily hard to get right in C (and Rust). There
are ~1,200 malloc calls in SQLite, and every single one has a
failure path. The SQLITE_NOMEM error propagates up the call
stack while unwinding all partial state changes.
Grammar
AtomicDecl = '#[atomic]' FnDecl ;
RollbackClause = 'on_failure' ':' 'rollback_to' '(' Ident ')' ;
SavepointDecl = 'savepoint' Ident '=' Expr ;
Full Example: Atomic Insert with OOM Recovery
#[atomic]
fn btree_insert(
tree: BtCursor :_1,
key: Region<k>,
data: Region<d>
) -> (BtCursor :_1) | OutOfMemoryError | DiskFullError
requires { tree.state @ ReadyToWrite }
ensures {
result is BtCursor =>
contains(result, key) and
BTreeValid(result.tree)
}
on_failure: rollback_to(tree)
// If ANY allocation or I/O fails, tree is unchanged
effects: database.write
{
// Save pre-operation state
savepoint pre = snapshot(tree)
// Step 1: Find insertion point (may allocate)
let pos = find_position(tree, key)?
// Step 2: Insert cell (may allocate overflow pages)
let tree = insert_cell(tree, pos, key, data)?
// Step 3: Rebalance if needed (may allocate new pages)
let tree = if needs_balance(tree, pos) {
balance(tree, pos)?
} else {
tree
}
// If we get here, all steps succeeded
tree
// If any ? propagates an error:
// 1. All allocated pages are freed
// 2. All modified pages are reverted to savepoint
// 3. tree is returned in its original state
}
// Compound atomic operation
#[atomic]
fn execute_insert_statement(
conn: Connection :_1,
table: String,
values: List<Value>
) -> (Connection :_1) | SqlError
on_failure: rollback_to(conn)
effects: database.write
{
let cursor = conn.open_cursor(table)?
let encoded = encode_record(values)?
let cursor = btree_insert(cursor, encoded.key, encoded.data)?
// Update indices
for idx in conn.indices_for(table) {
let idx_cursor = conn.open_cursor(idx.name)?
let idx_key = idx.extract_key(values)?
let idx_cursor = btree_insert(idx_cursor, idx_key, encoded.rowid)?
}
conn
}
Verification Rule
- Savepoint capture: At entry to an
#[atomic]function, the compiler inserts a logical savepoint capturing all mutable state - Error propagation: Every
?operator is a potential rollback point - Rollback proof: The compiler verifies that the
on_failurestate is reachable from every error point (no partial cleanup that leaves state inconsistent) - Nested atomicity:
#[atomic]functions may call other#[atomic]functions; the inner one’s failure triggers the outer one’s rollback - Linear state: The rollback target must be the original linear value (can’t roll back to an intermediate state)
Error Codes
| Code | Message | Cause |
|---|---|---|
| A36001 | Atomic function F has unrecoverable error path | Error path exists that cannot restore pre-state |
| A36002 | Savepoint S escapes atomic scope | Savepoint used outside its #[atomic] function |
| A36003 | Partial state modified without rollback path | Mutable state changed before error check |
| A36004 | Nested atomic function F swallows error | Inner failure caught without propagating to outer |
Rust Codegen
Atomic operations generate Rust code with explicit savepoint and rollback logic:
#![allow(unused)]
fn main() {
pub fn btree_insert(
tree: &mut BTree,
key: &[u8],
data: &[u8],
) -> Result<(), InsertError> {
// Save rollback state
let savepoint = tree.savepoint();
let result = (|| -> Result<(), InsertError> {
let pos = tree.find_position(key)?;
tree.insert_cell(pos, key, data)?;
if tree.needs_balance(pos) {
tree.balance(pos)?;
}
Ok(())
})();
match result {
Ok(()) => {
savepoint.commit();
Ok(())
}
Err(e) => {
savepoint.rollback(tree);
debug_assert!(tree.verify_invariants());
Err(e)
}
}
}
}
STOR.5 Monotonic State Contracts
Contracts for values that must never decrease (or never increase) over time: counters, version numbers, timestamps, sequence IDs.
Motivation
SQLite has many monotonic values:
- File change counter (header offset 24): incremented on every commit, never decremented
- Schema cookie (offset 40): incremented when schema changes
- WAL frame numbers: always increase within a WAL
- Transaction IDs: monotonically increasing
- Auto-increment rowids: always advance, never reuse
If any of these decreases, it indicates corruption, a bug in the
port, or a recovery error. Refinement types express constraints on
current values (v > 0) but not temporal constraints (v(t+1) >= v(t)).
Typestate tracks state transitions but not numeric progression.
Monotonicity needs its own concept.
Grammar
MonotonicDecl = 'monotonic' MonotonicKind Ident ':' TypeExpr
[MonotonicBound] ;
MonotonicKind = 'increasing' | 'decreasing' | 'non_decreasing'
| 'non_increasing' ;
MonotonicBound = 'wraps_at' Expr // for bounded counters
| 'saturates_at' Expr ;
MonotonicCheck = 'assert_monotonic' '(' Ident ')' ;
Full Example: SQLite Monotonic Values
module database_header {
// File change counter: incremented on each commit
monotonic non_decreasing file_change_counter: U32
wraps_at U32.MAX
// When it wraps, version_valid_for is set to a
// value not matching the counter (triggers full
// schema reload)
// Schema cookie: changes when schema changes
monotonic non_decreasing schema_cookie: U32
// Auto-increment counter per table
monotonic increasing auto_increment: I64
// Strict: never reuses values, even after DELETE
}
module wal {
// WAL frame numbers always increase
monotonic increasing frame_number: U32
// Salt values change on WAL reset (not monotonic)
// but within a single WAL epoch, they are constant
// Max frame in WAL index (updated atomically)
monotonic non_decreasing max_valid_frame: U32
}
// Using monotonic values
fn commit_transaction(
header: DatabaseHeader :_1,
changes: List<PageChange>
) -> DatabaseHeader :_1
ensures {
// Counter must advance (not stay the same)
header.file_change_counter > old(header.file_change_counter)
or (old(header.file_change_counter) == U32.MAX
and header.file_change_counter == 0)
// Wrapping is the ONLY case where new < old
}
effects: database.write
{
let counter = header.file_change_counter
let new_counter = if counter == U32.MAX {
header.version_valid_for = 0 // invalidate
0 // wrap
} else {
counter + 1
}
header.file_change_counter = new_counter
header
}
// COMPILE ERROR: decrementing a monotonic value
fn bad_rollback(header: DatabaseHeader :_1) -> DatabaseHeader :_1
{
header.file_change_counter = header.file_change_counter - 1
// A47001: monotonic non_decreasing value decreased
header
}
// COMPILE ERROR: reusing auto-increment
fn bad_insert(table: Table :_1, deleted_rowid: I64) -> Table :_1
{
let row = Row { rowid: deleted_rowid, ... }
// A47002: monotonic increasing value reused
// deleted_rowid was previously assigned, cannot be reused
table.insert(row)
}
// WAL frame number contract
fn append_wal_frame(
wal: WalFile :_1,
frame: WalFrame
) -> WalFile :_1
requires {
frame.frame_number > wal.last_frame_number
// Strict increasing: no duplicate frame numbers
}
ensures {
wal.last_frame_number == frame.frame_number
}
effects: filesystem.write
{
assert_monotonic(wal.max_valid_frame)
wal.write_frame(frame)
wal.max_valid_frame = frame.frame_number
wal
}
Verification Rule
- Write analysis: Every assignment to a monotonic variable is checked. The new value must satisfy the monotonic constraint relative to the old value
- Wrapping:
wraps_at Xallows exactly one transition from X to a smaller value (wrap-around). All other decreases are errors - Increasing vs non_decreasing:
increasingforbidsnew == old;non_decreasingallows it - Cross-function: Monotonicity is tracked across function boundaries via the linear type system (the value is linear, so only one function modifies it at a time)
- Recovery exception: During crash recovery (STOR.1), the compiler suspends monotonicity checks on values being restored, since recovery may roll back to a previous valid state
Error Codes
| Code | Message | Cause |
|---|---|---|
| A47001 | Monotonic value V decreased | Assignment violates non_decreasing |
| A47002 | Monotonic value V reused | Assignment violates increasing (strict) |
| A47003 | Monotonic value V decreased outside wrap | Decrease without wraps_at or not at boundary |
| A47004 | Monotonic value V overflows without wrap policy | Saturates_at or wraps_at not declared |
Rust Codegen
Monotonic contracts generate wrapper types that enforce the constraint at runtime in debug mode:
#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct MonotonicU32<const STRICT: bool> {
value: u32,
#[cfg(debug_assertions)]
previous: u32,
}
impl<const STRICT: bool> MonotonicU32<STRICT> {
pub fn new(initial: u32) -> Self {
MonotonicU32 {
value: initial,
#[cfg(debug_assertions)]
previous: initial,
}
}
pub fn get(&self) -> u32 { self.value }
pub fn advance(&mut self, new_value: u32) {
#[cfg(debug_assertions)]
{
if STRICT {
debug_assert!(new_value > self.value,
"Monotonic increasing: {} not > {}",
new_value, self.value);
} else {
debug_assert!(new_value >= self.value,
"Monotonic non-decreasing: {} not >= {}",
new_value, self.value);
}
self.previous = self.value;
}
self.value = new_value;
}
pub fn advance_wrapping(&mut self, new_value: u32) {
// Allow exactly one wrap from MAX to smaller value
#[cfg(debug_assertions)]
{
if new_value < self.value && self.value != u32::MAX {
panic!("Non-wrapping decrease: {} -> {}",
self.value, new_value);
}
}
self.value = new_value;
}
}
// Type aliases for SQLite usage
pub type ChangeCounter = MonotonicU32<false>; // non-decreasing
pub type SchemaCookie = MonotonicU32<false>; // non-decreasing
pub type FrameNumber = MonotonicU32<true>; // strict increasing
pub type AutoIncrement = MonotonicU32<true>; // strict increasing
}
STOR.6 Storage Failure Model
Contracts that define the physical failure semantics of the underlying storage medium, enabling verification of crash recovery logic against a concrete failure model.
Motivation
littlefs’s correctness depends on specific flash physics: programming
is idempotent for already-programmed bits, erased state is all-1s,
and a torn program leaves only the affected prog_size region
indeterminate. The FCRC mechanism CRCs the next prog-sized region
to detect torn writes. Crash recovery (STOR.1) specifies what must
be recovered; this feature specifies what can go wrong.
Grammar
StorageModelDecl = 'storage_model' Ident '{' StorageRule { StorageRule } '}' ;
StorageRule = 'on_crash_during' Ident ':' FailureSpec ';' ;
FailureSpec = 'affected_region' '(' Expr ')' 'becomes' ('indeterminate' | 'unchanged') ;
EraseSemantics = 'erase_value' ':' IntLit ';' ;
ProgSemantics = 'prog_idempotent' ':' BoolLit ';' ;
Full Example
storage_model FlashDevice {
erase_value: 0xFF
prog_idempotent: true
block_size: config.block_size
prog_size: config.prog_size
on_crash_during prog:
affected_region(offset..offset + prog_size) becomes indeterminate
// all other regions unchanged
on_crash_during erase:
affected_region(0..block_size) becomes indeterminate
// block may be partially erased
on_crash_during sync:
// no effect; sync is a barrier, not a write
}
contract MetadataPairWrite {
// Write to inactive block only
requires target == pair[1]
requires pair[0].crc_valid()
// After crash during prog: pair[0] still valid (untouched)
crash_safe {
pair[0].crc_valid()
pair[0].revision >= last_known_revision
}
// After successful commit: pair[1] now has higher revision
ensures pair[1].revision > pair[0].revision
ensures pair[1].crc_valid()
}
Verification Rule
- The storage model declares what physical operations can fail and how
- Crash recovery contracts (STOR.1) reference the storage model
- The verifier checks that recovery logic handles every
indeterminateregion correctly (by detecting via CRC or FCRC) - Operations on erased blocks must verify
erase_valueassumption
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-STOR-010 | Unhandled torn write | Recovery logic does not check indeterminate region |
| A-STOR-011 | Erase assumption violated | Code assumes zeros after erase on 0xFF flash |
| A-STOR-012 | Missing failure model | Storage operation without storage_model declaration |
Rust Codegen
Storage model declarations generate test infrastructure:
#![allow(unused)]
fn main() {
/// Fault-injection test harness for flash failure model
struct FlashFaultInjector {
fail_at: Option<(usize, usize)>, // (block, offset) to corrupt
}
impl FlashFaultInjector {
fn prog(&mut self, block: usize, off: usize, data: &[u8]) -> Result<()> {
if self.fail_at == Some((block, off)) {
// Write partial data (torn write simulation)
let partial = &data[..data.len() / 2];
self.device.write(block * self.block_size + off, partial)?;
return Err(Error::PowerLoss);
}
self.device.write(block * self.block_size + off, data)
}
}
}
14.FMT: Data Formats and Parsing
FMT.1 Binary Format Contracts
Contracts for on-disk binary formats that enforce backward compatibility across versions.
Grammar
FormatDecl = 'format' TypeIdent '{'
{ FormatField }
[CompatibilityDecl]
'}' ;
FormatField = 'offset' IntLit ',' 'size' IntLit ':'
Ident ['=' Expr] [WhereClause] ;
CompatibilityDecl = 'compatibility' '{'
{ FrozenDecl }
{ ExtensibleDecl }
'}' ;
FrozenDecl = 'frozen' ':' IdentList ;
ExtensibleDecl = 'extensible' ':' IdentList ;
Full Example: SQLite Database Header
format DatabaseHeader {
offset 0, size 16: magic = "SQLite format 3\0"
offset 16, size 2: page_size
where page_size in {512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}
-- Note: value 1 means 65536 (special encoding)
offset 18, size 1: write_version
where write_version in {1, 2}
-- 1 = rollback journal, 2 = WAL
offset 19, size 1: read_version
where read_version in {1, 2}
offset 20, size 1: reserved_space
where reserved_space >= 0
offset 21, size 1: max_embedded_payload_fraction = 64
offset 22, size 1: min_embedded_payload_fraction = 32
offset 23, size 1: leaf_payload_fraction = 32
offset 24, size 4: file_change_counter
offset 28, size 4: database_size_pages
where database_size_pages >= 0
offset 32, size 4: first_freelist_trunk_page
offset 36, size 4: freelist_page_count
offset 40, size 4: schema_cookie
offset 44, size 4: schema_format_number
where schema_format_number in {1, 2, 3, 4}
offset 48, size 4: default_cache_size
offset 52, size 4: largest_root_btree_page
offset 56, size 4: text_encoding
where text_encoding in {1, 2, 3}
-- 1 = UTF-8, 2 = UTF-16le, 3 = UTF-16be
offset 60, size 4: user_version
offset 64, size 4: incremental_vacuum_mode
offset 68, size 4: application_id
offset 72, size 20: reserved_expansion = 0
offset 92, size 4: version_valid_for
offset 96, size 4: sqlite_version_number
compatibility {
// These fields can NEVER change their offset, size, or meaning
frozen: magic, page_size, write_version, read_version,
text_encoding, schema_format_number
// These fields may be given new valid values in future versions
extensible: application_id, user_version
}
}
Verification Rules
- Frozen fields: Any change to a frozen field’s offset, size, encoding, or valid values is a compile error (A31001)
- Extensible fields: May add new valid values but cannot change offset or size
- New fields: May only use
reserved_expansionspace - Read compatibility: A reader must accept all valid values from all versions
- Write compatibility: A writer must only produce values valid for the target version
Cross-Version Testing
// The compiler can verify format compatibility across versions
format_test DatabaseHeader {
// v1 header must be readable by v2 reader
compatible: v1 readable_by v2
// v2 header with new features must be rejected by v1 reader
// (if write_version > 1)
incompatible: v2_wal rejected_by v1_reader
when write_version == 2
}
Error Codes
| Code | Message | Cause |
|---|---|---|
| A31001 | Frozen format field modified | Changed offset/size/meaning |
| A31002 | Format field overlaps | Two fields share byte range |
| A31003 | Gap in format layout | Unaccounted bytes between fields |
| A31004 | Format exceeds expected size | Header larger than spec |
| A31005 | Reserved space violated | Non-zero value in reserved field |
Rust Codegen
Binary format contracts generate zero-copy parser/serializer code:
#![allow(unused)]
fn main() {
pub struct DatabaseHeader<'a> {
data: &'a [u8; 100],
}
impl<'a> DatabaseHeader<'a> {
pub fn magic(&self) -> &[u8; 16] {
self.data[0..16].try_into().unwrap()
}
pub fn page_size(&self) -> u32 {
let raw = u16::from_be_bytes([self.data[16], self.data[17]]);
if raw == 1 { 65536 } else { raw as u32 }
}
pub fn write_version(&self) -> u8 {
self.data[18]
}
pub fn validate(&self) -> Result<(), FormatError> {
if self.magic() != b"SQLite format 3\0" {
return Err(FormatError::BadMagic);
}
let ps = self.page_size();
if !ps.is_power_of_two() || ps < 512 || ps > 65536 {
return Err(FormatError::BadPageSize);
}
// ... validate all fields ...
Ok(())
}
}
}
FMT.2 Bit-Level Format Contracts
Contracts for sub-byte parsing: reading individual bits, variable-length bit fields, and bit-packed structures common in compressed data formats.
Motivation
FMT.1 (Binary Format) handles byte-aligned structures like PNG chunk headers and BMP file headers. But many real-world formats require bit-level parsing:
- JPEG Huffman decoding: reads variable-length bit sequences (1-16 bits) that are NOT byte-aligned
- DEFLATE (zlib): 3-bit block headers, 5-bit length codes, variable-width distance codes
- GIF LZW: variable-width codes (starting at
min_code_size + 1bits, growing as the dictionary fills) - PNG interlacing: bit-packed scanline filters
Without bit-level contracts, the verifier cannot track the bit position cursor, cannot prove that a read of N bits does not exceed the remaining bits, and cannot verify that Huffman table lookups are safe.
Grammar
BitFormatDecl = 'bit_format' Ident '{'
{ BitFieldDecl }
'}' ;
BitFieldDecl = BitField | BitChoice | BitAlign ;
BitField = Ident ':' 'bits' '(' Expr ')'
[BitEndian] [BitConstraint] ;
BitChoice = 'match_bits' '(' Expr ')' '{'
{ BitPattern '=>' BitFieldDecl }
'}' ;
BitAlign = 'align_to' '(' Expr ')' ;
BitEndian = 'msb_first' | 'lsb_first' ;
BitConstraint = 'where' Predicate ;
BitCursor = 'bit_position' '(' Ident ')'
| 'bits_remaining' '(' Ident ')' ;
Full Example: JPEG Huffman Decoding
// Bit-level format for JPEG entropy-coded data
bit_format JpegBitstream {
// The bit cursor tracks position across byte boundaries
invariant { bits_remaining(self) >= 0 }
}
// Huffman decode: read variable-length code
fn huffman_decode(
stream: &mut JpegBitstream :read_bits,
table: &HuffmanTable :verified
) -> U8 | DecodeError
requires { bits_remaining(stream) >= 1 }
ensures {
// Consumed between 1 and 16 bits
old(bit_position(stream)) < bit_position(stream),
bit_position(stream) - old(bit_position(stream)) <= 16
}
{
let mut code: U16 = 0
for len in 1..=16 {
requires { bits_remaining(stream) >= 1 }
let bit = stream.read_bits(1) as U16 // read exactly 1 bit
code = (code << 1) | bit
if table.has_code(code, len) {
return table.lookup(code, len)
}
}
DecodeError::InvalidHuffmanCode
}
// DEFLATE block header: 3 bits, not byte-aligned
bit_format DeflateBlockHeader {
is_final: bits(1) msb_first,
block_type: bits(2) msb_first
where block_type <= 2 // 0=stored, 1=fixed, 2=dynamic; 3=reserved
}
// GIF LZW: variable-width codes
fn read_lzw_code(
stream: &mut BitReader :read_bits,
code_size: U8
) -> U16 | DecodeError
requires { code_size >= 2 && code_size <= 12 }
requires { bits_remaining(stream) >= code_size as U32 }
ensures { result <= (1 << code_size) - 1 }
{
stream.read_bits(code_size as U32) as U16
}
Verification Rule
- Bit cursor tracking: The verifier maintains a symbolic bit
position for each
BitReader/BitWriter. Everyread_bits(N)advances the cursor by N and requiresbits_remaining >= N - Cross-byte safety: When a bit read spans a byte boundary,
the verifier confirms the underlying byte buffer has sufficient
data.
bits_remaining(stream) >= Nimpliesbytes_remaining(stream.buffer) >= ceil(N + bit_offset, 8) - Variable-width bounds: For variable-width reads (Huffman, LZW), the verifier checks that the maximum possible read does not exceed remaining bits. Loop invariants must prove the upper bound
- Alignment:
align_to(8)skips bits to reach a byte boundary. The verifier proves the skip does not exceed 7 bits
Error Codes
| Code | Message | Cause |
|---|---|---|
| A49001 | Bit read exceeds remaining bits | read_bits(N) where bits_remaining < N |
| A49002 | Bit field width must be constant or bounded | Variable-width field without upper bound |
| A49003 | Invalid bit alignment target | align_to(0) or non-power-of-2 alignment |
| A49004 | Bit cursor used after byte-level read | Mixing bit and byte reads without re-alignment |
| A49005 | Bit field constraint not satisfiable | where predicate on bit field is always false |
Rust Codegen
Bit-level format contracts generate a BitReader wrapper with
bounds-checked accessors:
#![allow(unused)]
fn main() {
pub struct BitReader<'a> {
data: &'a [u8],
byte_pos: usize,
bit_offset: u8, // 0-7, bits consumed in current byte
}
impl<'a> BitReader<'a> {
#[inline]
pub fn read_bits(&mut self, n: u32) -> Result<u32, DecodeError> {
debug_assert!(n <= 25, "read_bits limited to 25 for u32");
if self.bits_remaining() < n as usize {
return Err(DecodeError::UnexpectedEnd);
}
let mut value: u32 = 0;
let mut remaining = n;
while remaining > 0 {
let available = 8 - self.bit_offset as u32;
let take = remaining.min(available);
let mask = (1u32 << take) - 1;
let shift = available - take;
value = (value << take)
| ((self.data[self.byte_pos] as u32 >> shift) & mask);
remaining -= take;
self.bit_offset += take as u8;
if self.bit_offset >= 8 {
self.bit_offset = 0;
self.byte_pos += 1;
}
}
Ok(value)
}
#[inline]
pub fn bits_remaining(&self) -> usize {
(self.data.len() - self.byte_pos) * 8
- self.bit_offset as usize
}
}
}
FMT.3 String Encoding Contracts
Contracts that track text encoding (UTF-8, UTF-16LE, UTF-16BE) through the type system, ensuring encoding-aware operations and preventing encoding mismatch bugs.
Motivation
SQLite supports three text encodings: UTF-8, UTF-16LE, and UTF-16BE. The encoding is set per-database and affects how strings are stored, compared, and returned through the API. Encoding bugs are subtle:
- Comparing a UTF-8 string with a UTF-16LE string without conversion
- Returning UTF-16 bytes through a UTF-8 API
- Computing string length in bytes vs characters vs code points
- Truncating a multi-byte character at a buffer boundary
- Collation functions that assume a specific encoding
Rust’s String is always UTF-8, but a SQLite port must handle all
three encodings internally and at the FFI boundary.
Grammar
EncodingType = 'Utf8' | 'Utf16Le' | 'Utf16Be' ;
EncodedString = 'Text' '<' EncodingType '>' ;
EncodingDecl = 'encoding' Ident ':' EncodingType ;
TranscodeExpr = 'transcode' '(' Expr ',' EncodingType ')' ;
EncodingConstraint = 'encoding_matches' '(' Ident ',' Ident ')'
| 'valid_encoding' '(' Ident ')' ;
Full Example: Multi-Encoding Text Handling
// Text type parameterized by encoding
type Text<E: Encoding> {
bytes: Region<n>,
encoding: E,
char_count: Nat,
byte_length: Nat
}
// Encoding is a type-level parameter
enum Encoding { Utf8, Utf16Le, Utf16Be }
// Database-level encoding setting
type DatabaseEncoding = {e: Encoding |
e in {Utf8, Utf16Le, Utf16Be}
}
// String comparison requires same encoding
fn compare_text<E: Encoding>(
a: Text<E>,
b: Text<E>,
collation: Collation<E>
) -> Ordering
requires { valid_encoding(a) }
requires { valid_encoding(b) }
ensures { result is deterministic }
effects: pure
// COMPILE ERROR: comparing different encodings
fn bad_compare(
a: Text<Utf8>,
b: Text<Utf16Le>
) -> Ordering
{
compare_text(a, b) // A43001: encoding mismatch
// a is Utf8, b is Utf16Le
}
// Transcoding with validation
fn transcode<From: Encoding, To: Encoding>(
input: Text<From>
) -> Text<To> | EncodingError
requires { valid_encoding(input) }
ensures {
result is Text<To> =>
char_count(result) == char_count(input)
and valid_encoding(result)
}
effects: pure
// Length operations are encoding-aware
fn text_length<E: Encoding>(t: Text<E>) -> LengthResult
requires { valid_encoding(t) }
effects: pure
{
LengthResult {
bytes: t.byte_length,
chars: t.char_count,
// For UTF-16: code_units may differ from chars (surrogate pairs)
code_units: match E {
Utf8 => t.byte_length,
Utf16Le | Utf16Be => t.byte_length / 2,
}
}
}
// Truncation must respect character boundaries
fn truncate_text<E: Encoding>(
t: Text<E>,
max_bytes: Nat
) -> Text<E>
requires { valid_encoding(t) }
ensures {
result.byte_length <= max_bytes
and valid_encoding(result)
// Never splits a multi-byte character
and result.char_count == count_complete_chars(t, max_bytes)
}
effects: pure
// FFI: returning text in the caller's expected encoding
#[feature(ffi)]
fn sqlite3_column_text(
stmt: ptr<Statement>,
col: c_int
) -> cstring
callee_guarantees:
// Return value encoding matches the database encoding
encoding_matches(result, stmt.db.encoding)
and valid_encoding(result)
// Null-terminated
and result[len(result)] == 0
fn sqlite3_column_text16(
stmt: ptr<Statement>,
col: c_int
) -> ptr<U16>
callee_guarantees:
// Always returns UTF-16 in native byte order
valid_encoding(result)
and encoding_of(result) == native_utf16()
// Collation registration must specify encoding
fn register_collation<E: Encoding>(
conn: Connection,
name: String,
collation: Collation<E>
) -> Connection
requires { conn.state @ Open }
ensures {
// Collation is only used for comparisons in encoding E
conn.collations[name].encoding == E
}
// Index storage encoding must match table encoding
invariant {
forall index in database.indices:
forall column in index.columns:
encoding_of(index.stored_key(column))
== database.encoding
}
// COMPILE ERROR: building index with wrong encoding
fn build_index_wrong_encoding(
db: Database, // encoding = Utf8
text: Text<Utf16Le>
) -> IndexKey
{
IndexKey.from_text(text)
// A43003: index key encoding (Utf16Le) doesn't match
// database encoding (Utf8)
}
Verification Rule
- Encoding matching: Operations that combine two text values require the same encoding type parameter. Mismatch is a compile error, not a runtime transcode
- Valid encoding: The
valid_encoding(t)predicate asserts the bytes are well-formed for the declared encoding. This is checked at input boundaries (file read, FFI receive) and preserved through operations (the compiler proves concat, truncate, etc. preserve validity) - Transcode tracking: Every
transcode()call is explicit. The compiler rejects implicit encoding conversion - Boundary safety: Truncation and substring operations must not split multi-byte sequences. The compiler checks this via the encoding’s character boundary rules
- FFI encoding:
sqlite3_column_textreturns the database encoding;sqlite3_column_text16returns native UTF-16. The contract makes this explicit
Error Codes
| Code | Message | Cause |
|---|---|---|
| A43001 | Encoding mismatch: E1 vs E2 | Operating on texts with different encodings |
| A43002 | Text truncated at multi-byte boundary | Truncation splits a character |
| A43003 | Storage encoding doesn’t match database encoding | Index/column uses wrong encoding |
| A43004 | Invalid encoding: byte sequence not valid E | Bytes don’t form valid UTF-8/16 |
| A43005 | Implicit transcode detected | Encoding conversion without explicit transcode() |
Rust Codegen
Encoding contracts generate parameterized string types:
#![allow(unused)]
fn main() {
/// Marker types for encoding
pub struct Utf8;
pub struct Utf16Le;
pub struct Utf16Be;
pub trait Encoding: sealed::Sealed {
fn validate(bytes: &[u8]) -> bool;
fn char_count(bytes: &[u8]) -> usize;
fn truncate_to_char_boundary(bytes: &[u8], max: usize) -> usize;
}
impl Encoding for Utf8 {
fn validate(bytes: &[u8]) -> bool {
std::str::from_utf8(bytes).is_ok()
}
fn char_count(bytes: &[u8]) -> usize {
std::str::from_utf8(bytes)
.map(|s| s.chars().count())
.unwrap_or(0)
}
fn truncate_to_char_boundary(bytes: &[u8], max: usize) -> usize {
if max >= bytes.len() { return bytes.len(); }
let mut i = max;
while i > 0 && bytes[i] & 0xC0 == 0x80 { i -= 1; }
i
}
}
impl Encoding for Utf16Le {
fn validate(bytes: &[u8]) -> bool {
if bytes.len() % 2 != 0 { return false; }
let units: Vec<u16> = bytes.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units).is_ok()
}
fn char_count(bytes: &[u8]) -> usize {
let units: Vec<u16> = bytes.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units)
.map(|s| s.chars().count())
.unwrap_or(0)
}
fn truncate_to_char_boundary(bytes: &[u8], max: usize) -> usize {
let max = max & !1; // round down to even
if max >= bytes.len() { return bytes.len(); }
// Check for surrogate pair split
if max >= 2 {
let unit = u16::from_le_bytes([bytes[max - 2], bytes[max - 1]]);
if (0xD800..=0xDBFF).contains(&unit) {
return max - 2; // Don't split surrogate pair
}
}
max
}
}
/// Text with compile-time encoding guarantee
pub struct Text<E: Encoding> {
bytes: Vec<u8>,
_encoding: std::marker::PhantomData<E>,
}
impl<E: Encoding> Text<E> {
pub fn new(bytes: Vec<u8>) -> Result<Self, EncodingError> {
if !E::validate(&bytes) {
return Err(EncodingError::InvalidBytes);
}
Ok(Text { bytes, _encoding: PhantomData })
}
pub fn byte_length(&self) -> usize { self.bytes.len() }
pub fn char_count(&self) -> usize { E::char_count(&self.bytes) }
pub fn truncate(&self, max_bytes: usize) -> Self {
let safe = E::truncate_to_char_boundary(&self.bytes, max_bytes);
Text {
bytes: self.bytes[..safe].to_vec(),
_encoding: PhantomData,
}
}
}
/// Transcoding between encodings
pub fn transcode<From: Encoding, To: Encoding>(
input: &Text<From>,
) -> Result<Text<To>, EncodingError> {
// Decode to Unicode code points, then re-encode
let s = decode_to_string::<From>(&input.bytes)?;
let bytes = encode_from_string::<To>(&s)?;
Ok(Text { bytes, _encoding: PhantomData })
}
// COMPILE ERROR: this doesn't compile because E1 != E2
// fn compare<E1: Encoding, E2: Encoding>(a: &Text<E1>, b: &Text<E2>)
// -> Ordering { ... }
// Only same-encoding comparison compiles
pub fn compare<E: Encoding>(a: &Text<E>, b: &Text<E>)
-> std::cmp::Ordering
{
a.bytes.cmp(&b.bytes) // Safe: both validated as encoding E
}
}
FMT.4 Codec Registry / Format Dispatch
Contracts for multi-format systems where input is dispatched to format-specific decoders based on magic bytes, headers, or file codecs, with per-format contract sets.
Motivation
stb_image supports 9 image formats: PNG, JPEG, GIF, BMP, PSD, TGA, HDR, PIC, PNM. The dispatch logic is:
- Read first N bytes of input
- Match magic bytes to determine format
- Call the format-specific decoder
- Return pixels in a uniform output format
Each format has its own contract surface (PNG needs chunk validation, JPEG needs Huffman tables, GIF needs LZW bounds). Interface contracts (TYPE.1) handle polymorphism but do not express:
- “The dispatch correctly identifies the format from magic bytes”
- “Each codec satisfies format-specific contracts AND the common output contract”
- “No format is silently dropped or misidentified”
This is a common pattern beyond image decoding: file format libraries, network protocol handlers, serialization frameworks.
Grammar
CodecRegistryDecl = 'codec_registry' Ident '{'
'output' ':' TypeExpr ','
{ CodecEntry }
'}' ;
CodecEntry = 'codec' Ident '{'
'magic' ':' MagicPattern ','
'decoder' ':' FnIdent ','
[CodecContracts]
'}' ;
MagicPattern = BytePattern
| 'extension' '(' StringLit { ',' StringLit } ')'
| 'probe' '(' FnIdent ')' ;
BytePattern = '[' ByteExpr { ',' ByteExpr } ']'
| '[' ByteExpr { ',' ByteExpr } ',' '..' ']' ;
CodecContracts = 'contracts' ':' '{' { ContractDecl } '}' ;
Full Example: stb_image Format Registry
// Common output type for all image decoders
type ImageOutput {
pixels: Region<output_size>,
width: U32,
height: U32,
channels: U8,
invariant {
width >= 1 && height >= 1,
channels >= 1 && channels <= 4,
output_size == width as U64 * height as U64 * channels as U64,
output_size <= MAX_IMAGE_ALLOC
}
}
codec_registry ImageCodecs {
output: ImageOutput,
codec Png {
magic: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A],
decoder: decode_png,
contracts: {
// PNG-specific: chunk CRC validation
ensures chunk_crcs_valid(input),
// PNG-specific: IHDR dimensions match output
ensures result.width == ihdr.width,
ensures result.height == ihdr.height
}
}
codec Jpeg {
magic: [0xFF, 0xD8, 0xFF, ..],
decoder: decode_jpeg,
contracts: {
// JPEG-specific: Huffman tables valid
requires valid_huffman_tables(input),
// JPEG-specific: IDCT accuracy
ensures idct_meets_ieee1180(result)
}
}
codec Gif {
magic: [0x47, 0x49, 0x46, 0x38, ..], // "GIF8"
decoder: decode_gif,
contracts: {
// GIF-specific: LZW code size bounds
ensures lzw_codes_in_range(input)
}
}
codec Bmp {
magic: [0x42, 0x4D, ..], // "BM"
decoder: decode_bmp
}
codec Hdr {
probe: is_hdr_format, // complex header detection
decoder: decode_hdr,
contracts: {
ensures result.channels == 3 // HDR is always RGB
}
}
}
// The registry generates the dispatch function
fn decode_image(
data: &[U8] :tainted
) -> ImageOutput | DecodeError
ensures {
// Common contract: output satisfies ImageOutput invariant
result.pixels.len() == result.width as U64
* result.height as U64 * result.channels as U64
}
{
// Auto-generated: try each codec's magic pattern in order
// First match wins
ImageCodecs.dispatch(data)
}
Verification Rule
- Magic uniqueness: The verifier checks that no two codecs
have overlapping magic patterns. If PNG starts with
[0x89, 0x50, ...]and another codec also matches those bytes, it is a compile error - Codec completeness: If a codec has no
contractsblock, it inherits only the common output contract. The verifier warns if format-specific invariants are likely missing - Contract inheritance: Every codec’s decoder must satisfy both its format-specific contracts AND the registry’s common output type invariant
- Probe functions:
probefunctions must be pure (no side effects) and total. They receive a read-only slice and return bool
Error Codes
| Code | Message | Cause |
|---|---|---|
| A52001 | Overlapping magic patterns for codecs A and B | Ambiguous dispatch |
| A52002 | Codec decoder does not return registry output type | Return type mismatch |
| A52003 | Codec-specific contract violated by decoder | Format-specific ensures failed |
| A52004 | Probe function has side effects | Probe must be pure |
| A52005 | No codec matches input | All magic patterns failed, no fallback |
Rust Codegen
Codec registries generate dispatch functions with pattern matching:
#![allow(unused)]
fn main() {
pub fn decode_image(data: &[u8]) -> Result<ImageOutput, DecodeError> {
if data.len() >= 8 && data[..8] == [0x89, 0x50, 0x4E, 0x47,
0x0D, 0x0A, 0x1A, 0x0A] {
decode_png(data)
} else if data.len() >= 3 && data[0] == 0xFF
&& data[1] == 0xD8 && data[2] == 0xFF {
decode_jpeg(data)
} else if data.len() >= 4 && &data[..4] == b"GIF8" {
decode_gif(data)
} else if data.len() >= 2 && &data[..2] == b"BM" {
decode_bmp(data)
} else if is_hdr_format(data) {
decode_hdr(data)
} else {
Err(DecodeError::UnknownFormat)
}
}
}
FMT.5 Checksum and Integrity Contracts
Contracts that enforce data integrity verification before use.
Grammar
IntegrityDecl = 'integrity' TypeIdent '{'
VerifyClause
OnReadClause
OnWriteClause
'}' ;
VerifyClause = 'verify' ':' Predicate ;
OnReadClause = 'on_read' ':' IntegrityAction ;
OnWriteClause = 'on_write' ':' IntegrityAction ;
IntegrityAction = 'MUST' 'verify' 'before' 'using' Ident
| 'MUST' 'compute' Ident 'from' IdentList ;
Full Example: WAL Frame
type WalFrame {
page_number: U32,
commit_size: U32,
salt: (U32, U32),
checksum: (U32, U32),
data: Region<PageSize>
}
integrity WalFrame {
verify: checksum == wal_checksum(page_number, commit_size, salt, data)
on_read: MUST verify before using data
on_write: MUST compute checksum from page_number, commit_size, salt, data
}
// COMPILE ERROR: using frame.data without verification
fn bad_read(frame: WalFrame) -> Page
effects: pure
{ Page.from_region(frame.data) }
// A30001: integrity not verified
// CORRECT: verify then use
fn good_read(frame: WalFrame) -> Page | CorruptionError
effects: pure
{
if not frame.verify_integrity() {
return CorruptionError("checksum mismatch")
}
Page.from_region(frame.data)
}
// COMPILE ERROR: writing frame without computing checksum
fn bad_write(page: Page, page_num: U32) -> WalFrame
effects: pure
{
WalFrame {
page_number: page_num,
data: page.to_region(),
checksum: (0, 0) // A30002: checksum not computed from fields
}
}
Error Codes
| Code | Message | Cause |
|---|---|---|
| A30001 | Data used without integrity verification | Missing checksum check |
| A30002 | Integrity field not computed from sources | Checksum hardcoded or stale |
| A30003 | Integrity check result ignored | Verified but result not used in branch |
Rust Codegen
Integrity contracts generate wrapper types that enforce the verify-before-use pattern:
#![allow(unused)]
fn main() {
pub struct Unverified<T>(T);
pub struct Verified<T>(T);
impl Unverified<WalFrame> {
pub fn verify(self) -> Result<Verified<WalFrame>, CorruptionError> {
let frame = &self.0;
let expected = wal_checksum(
frame.page_number, frame.commit_size,
frame.salt, &frame.data,
);
if frame.checksum != expected {
return Err(CorruptionError::ChecksumMismatch);
}
Ok(Verified(self.0))
}
}
impl Verified<WalFrame> {
pub fn data(&self) -> &[u8] { &self.0.data }
}
// Unverified<WalFrame> has NO .data() method -- can't access without verify
}
FMT.6 Protocol Grammar Conformance
Contracts that verify a parser accepts exactly the language defined by a protocol specification (RFC ABNF), with explicit annotations for intentional deviations.
Motivation
picohttpparser’s get_token_to_eol accepts bare LF as a line
terminator (deviating from RFC 9112 which requires CRLF). Its
chunked decoder rejects bare LF in chunk headers. This inconsistency
within the same library is the exact class of differential that
CVE-2023-30589 (Node.js llhttp) exploited. No existing Assura feature
verifies a parser against its protocol grammar or documents deliberate
deviations.
Grammar
ProtocolDecl = 'protocol' Ident '{' RfcRef { ',' RfcRef } '}' ;
RfcRef = 'rfc' '(' IntLit ')' ;
ConformsAnnotation = '#[conforms(' Ident ')]' ;
DeviationAnnotation = '#[deviation(' StringLit ')]' ;
AcceptsRule = 'accepts' ':' ProductionRef ';' ;
RejectsRule = 'rejects' ':' ProductionRef ';' ;
ProductionRef = Ident '::' Ident ;
Full Example
protocol HTTP1 {
rfc(9110), // HTTP Semantics
rfc(9112) // HTTP/1.1
}
#[conforms(HTTP1)]
fn parse_request_line(
buf: Bytes @taint:untrusted
) -> RequestLine | ParseError
{
let method = parse_token(buf) // RFC 9110 token production
expect_sp(buf) // exactly one SP
#[deviation("Accept bare LF per RFC 9112 Section 2.2 robustness")]
let line_end = find_line_ending(buf) // CRLF or LF
// Verifier checks:
// 1. parse_token matches RFC 9110 ABNF 'token' production
// 2. expect_sp accepts exactly SP (0x20), not HT
// 3. deviation is documented for bare LF acceptance
}
#[conforms(HTTP1)]
fn parse_chunk_header(
buf: Bytes @taint:untrusted
) -> ChunkHeader | ParseError
{
let size = parse_hex(buf)
expect_crlf(buf) // NO deviation: strict CRLF required
// If this also accepted bare LF, verifier would flag
// inconsistency with parse_request_line's deviation
}
Verification Rule
- Functions marked
#[conforms(Protocol)]are checked against the referenced RFC productions - Any behavior that deviates from the RFC MUST have a
#[deviation]annotation explaining why - Inconsistent deviations across functions in the same protocol are flagged (one function accepts bare LF, another rejects it)
- Strict mode: deviations are compile errors (for security-critical parsers)
Error Codes
| Code | Message | Cause |
|---|---|---|
| A-FMT-010 | RFC production mismatch | Parser accepts/rejects input the RFC does not |
| A-FMT-011 | Undocumented deviation | Parser deviates from RFC without #[deviation] |
| A-FMT-012 | Inconsistent deviation | Same protocol, different functions, contradictory leniency |
| A-FMT-013 | Deviation in strict mode | #[deviation] used but strict conformance required |
Rust Codegen
Protocol conformance is verified at compile time. Deviations generate documentation comments and optional runtime logging:
#![allow(unused)]
fn main() {
/// Parses HTTP request line per RFC 9112.
/// DEVIATION: Accepts bare LF (RFC 9112 Section 2.2 robustness).
fn parse_request_line(buf: &[u8]) -> Result<RequestLine, ParseError> {
let method = parse_token(buf)?;
let sp = expect_byte(buf, b' ')?;
// Deviation: accept LF or CRLF
let line_end = buf.iter().position(|&b| b == b'\n')
.ok_or(ParseError::Incomplete)?;
#[cfg(feature = "log_deviations")]
if buf[line_end.saturating_sub(1)] != b'\r' {
log::debug!("bare LF accepted in request line");
}
Ok(RequestLine { method, ..})
}
}
14.NUM: Numerical and Precision
NUM.1 Numerical Precision Contracts
Contracts that specify acceptable floating-point precision, rounding behavior, and numerical accuracy bounds for mathematical computations.
Motivation
CONC.3 (Determinism) bans floating-point transcendentals entirely, which works for SQLite (integer-heavy). But image and signal processing require floating-point math with controlled precision:
- JPEG IDCT: IEEE 1180-1990 specifies maximum error of +/-1 for any pixel, and RMS error < 0.02 across an 8x8 block. The Rust implementation must meet this standard or images decode differently.
- HDR tone mapping: Converts linear float values to display gamma. Precision matters for banding artifacts.
- Color space conversion: sRGB to linear involves
pow(x, 2.4). Different implementations ofpowgive different results at different ULP (Unit of Least Precision) levels. - PNG gamma correction:
pow(sample / max, gamma)must be accurate enough that round-trip encode/decode preserves the original within 1 bit.
The problem is not “floats are bad” but “floats need contracts.” The absence of precision contracts means silent image corruption that no safety check catches.
Grammar
PrecisionDecl = 'precision' Ident '{'
{ PrecisionRule }
'}' ;
PrecisionRule = 'max_ulp_error' ':' Expr
| 'max_abs_error' ':' Expr
| 'rms_error' ':' Expr
| 'rounding' ':' RoundingMode
| 'reference' ':' FnIdent ;
RoundingMode = 'nearest_even' | 'toward_zero'
| 'toward_inf' | 'toward_neg_inf' ;
PrecisionAttr = '#[precision(' Ident ')]' ;
Full Example: JPEG IDCT
// Precision contract for JPEG IDCT
// Based on IEEE 1180-1990 accuracy requirements
precision IdctAccuracy {
max_abs_error: 1 // any output pixel: |actual - ref| <= 1
rms_error: 0.02 // across 8x8 block
reference: reference_idct_f64 // double-precision reference
}
#[precision(IdctAccuracy)]
fn idct_8x8(coefficients: &[I16; 64]) -> [U8; 64]
requires {
for_all(i in 0..64, coefficients[i] >= -2048
&& coefficients[i] <= 2047)
}
ensures {
for_all(i in 0..64, result[i] <= 255)
}
{
// Fixed-point or float implementation
// Verifier checks against reference_idct_f64
// using the IdctAccuracy bounds
let mut workspace = [0f32; 64]
// Column pass
for col in 0..8 {
idct_1d_column(coefficients, &mut workspace, col)
}
// Row pass + clamp to [0, 255]
let mut output = [0u8; 64]
for row in 0..8 {
idct_1d_row(&workspace, &mut output, row)
}
output
}
// Reference implementation (double precision, not perf-optimized)
fn reference_idct_f64(coefficients: &[I16; 64]) -> [F64; 64]
must_be deterministic
{
let mut output = [0.0f64; 64]
for y in 0..8 {
for x in 0..8 {
let mut sum = 0.0f64
for v in 0..8 {
for u in 0..8 {
let cu = if u == 0 { 1.0 / 2.0.sqrt() } else { 1.0 }
let cv = if v == 0 { 1.0 / 2.0.sqrt() } else { 1.0 }
sum += cu * cv
* coefficients[v * 8 + u] as F64
* cos((2.0 * x as F64 + 1.0) * u as F64 * PI / 16.0)
* cos((2.0 * y as F64 + 1.0) * v as F64 * PI / 16.0)
}
}
output[y * 8 + x] = sum / 4.0
}
}
output
}
// Color space conversion with precision
precision SrgbPrecision {
max_ulp_error: 4 // within 4 ULPs of reference
rounding: nearest_even
}
#[precision(SrgbPrecision)]
fn srgb_to_linear(srgb: F32) -> F32
requires { srgb >= 0.0 && srgb <= 1.0 }
ensures { result >= 0.0 && result <= 1.0 }
{
if srgb <= 0.04045 {
srgb / 12.92
} else {
((srgb + 0.055) / 1.055).powf(2.4)
}
}
Verification Rule
- Reference comparison: The verifier evaluates both the implementation and the reference function on a representative set of inputs. For IDCT, this is the IEEE 1180 test vectors. Violations produce A51001
- ULP tracking: For
max_ulp_error, the verifier tracks floating-point operations and their cumulative error propagation using interval arithmetic - RMS computation: For
rms_error, the verifier evaluates the reference and implementation on the test set, computes the RMS difference, and checks it against the bound - Rounding mode:
roundingconstraints generatef32::round_ties_even()or equivalent in Rust codegen, and the verifier confirms no implicit rounding mode change
Error Codes
| Code | Message | Cause |
|---|---|---|
| A51001 | Absolute error exceeds bound: actual E, max M | Output deviates from reference beyond max_abs_error |
| A51002 | ULP error exceeds bound: actual E, max M | Floating-point result too far from exact value |
| A51003 | RMS error exceeds bound across test set | Aggregate precision below standard |
| A51004 | No reference function for precision contract | precision block has no reference |
| A51005 | Reference function uses restricted operations | Reference must be deterministic and total |
Rust Codegen
Precision contracts generate test harnesses that verify accuracy against the reference implementation:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod precision_tests {
use super::*;
#[test]
fn idct_ieee1180_accuracy() {
// IEEE 1180 test vectors
for block in ieee1180_test_vectors() {
let actual = idct_8x8(&block);
let reference = reference_idct_f64(&block);
let mut sum_sq_err = 0.0f64;
for i in 0..64 {
let err = (actual[i] as f64 - reference[i]).abs();
assert!(err <= 1.0,
"pixel {i}: error {err} exceeds max 1.0");
sum_sq_err += err * err;
}
let rms = (sum_sq_err / 64.0).sqrt();
assert!(rms < 0.02,
"RMS error {rms} exceeds max 0.02");
}
}
}
}
NUM.2 Precomputed Table Verification
Contracts that prove lookup tables are correct precomputations of their generating functions. Ensures tables match their mathematical definitions at compile time.
Motivation
Systems code is full of precomputed tables used for performance:
- Huffman tables: prebuilt from code length arrays, used millions of times per decode. If a table entry is wrong, the output is silently corrupt.
- Zigzag reorder table: maps linear indices to zigzag scan order for JPEG’s 8x8 DCT blocks. One wrong entry produces subtly wrong image output.
- CRC32 tables: 256 entries precomputed from the polynomial. A wrong entry produces wrong checksums that silently accept corrupt data.
- Quantization tables: JPEG quality scaling applied to base tables. Off-by-one in scaling produces slightly wrong images.
These tables are typically defined as static const arrays with
no connection to their mathematical definition. If someone edits
the table or a code generator has a bug, there is no compile-time
check. Assura’s integrity contracts (FMT.5) verify checksums on
data, but not that a table matches its generating formula.
Grammar
TableDecl = 'table' Ident ':' '[' TypeExpr ';' Expr ']'
'=' 'precompute' '(' GeneratingExpr ')'
[TableVerify] ;
GeneratingExpr = 'for' Ident 'in' RangeExpr '=>' Expr ;
TableVerify = 'verify_against' ':' FnIdent
| 'verify_property' ':' Predicate ;
Full Example: JPEG and CRC Tables
// Zigzag reorder table: maps linear 0..63 to zigzag scan order
table ZIGZAG_ORDER: [U8; 64] = precompute(
for i in 0..64 => zigzag_index(i)
)
verify_against: zigzag_index
// The verifier confirms: for all i in 0..64,
// ZIGZAG_ORDER[i] == zigzag_index(i)
// The generating function (pure, total)
fn zigzag_index(linear: U8) -> U8
requires { linear < 64 }
ensures { result < 64 }
must_be deterministic
{
// Zigzag traversal of 8x8 matrix
let row = linear / 8
let col = linear % 8
// ... zigzag logic ...
}
// CRC32 table: 256 entries from polynomial 0xEDB88320
table CRC32_TABLE: [U32; 256] = precompute(
for byte in 0..256 => crc32_for_byte(byte as U8)
)
verify_property: for_all(i in 0..256,
CRC32_TABLE[i] == crc32_for_byte(i as U8)
)
fn crc32_for_byte(byte: U8) -> U32
must_be deterministic
{
let mut crc: U32 = byte as U32
for _ in 0..8 {
if crc & 1 == 1 {
crc = (crc >> 1) ^ 0xEDB88320
} else {
crc = crc >> 1
}
}
crc
}
// JPEG default quantization table with quality scaling
fn scaled_quant_table(
base: &[U8; 64],
quality: U8
) -> [U8; 64]
requires { quality >= 1 && quality <= 100 }
ensures {
for_all(i in 0..64,
result[i] >= 1 && result[i] <= 255
)
}
must_be deterministic
{
let scale = if quality < 50 {
5000 / (quality as U16)
} else {
200 - 2 * (quality as U16)
}
let mut out = [0u8; 64]
for i in 0..64 {
let val = ((base[i] as U16 * scale + 50) / 100)
.clamp(1, 255) as U8
out[i] = val
}
out
}
Verification Rule
- Exhaustive check: For tables with bounded index ranges (0..N), the verifier evaluates the generating function for every index and confirms equality. This is Layer 0 (structural) when N is small (<= 65536), Layer 1 otherwise
- Property check:
verify_propertypredicates are checked via SMT for all indices in the domain - Determinism required: Generating functions must be marked
deterministic(CONC.3). A table generated from a non-deterministic function is a compile error - Referential transparency: If the table is used in verified
code, the verifier may substitute
TABLE[i]withgenerating_fn(i)for proof purposes
Error Codes
| Code | Message | Cause |
|---|---|---|
| A50001 | Table entry T[i] does not match generating function | T[i] != gen(i) for some i |
| A50002 | Generating function is not deterministic | precompute requires must_be deterministic |
| A50003 | Table index range exceeds type bounds | Index type cannot hold the range |
| A50004 | Generating function is not total over range | Function may fail for some index in range |
| A50005 | Table size mismatch | Declared size does not match range |
Rust Codegen
Precomputed tables generate const arrays with compile-time
evaluation or build-script generation:
#![allow(unused)]
fn main() {
// Small tables: const evaluation
const ZIGZAG_ORDER: [u8; 64] = {
let mut table = [0u8; 64];
let mut i = 0;
while i < 64 {
table[i] = zigzag_index(i as u8);
i += 1;
}
table
};
// Large tables: build.rs generation + test verification
#[cfg(test)]
mod table_tests {
#[test]
fn verify_crc32_table() {
for i in 0..256u32 {
assert_eq!(
CRC32_TABLE[i as usize],
crc32_for_byte(i as u8),
"CRC32 table mismatch at index {i}"
);
}
}
}
}
14.PLAT: Platform and Configuration
PLAT.1 Platform Abstraction Contracts
Contracts that hold across all platform-specific implementations. When code is conditionally compiled, the compiler verifies that every platform variant satisfies the shared contract.
Motivation
SQLite runs on every platform: Linux, Windows, macOS, iOS, Android, embedded RTOS, bare metal. It uses a VFS abstraction layer with ~15 platform-specific implementations. Each must provide the same guarantees (atomicity of writes, lock semantics, sync durability) despite wildly different OS primitives.
Grammar
PlatformDecl = 'platform' TypeIdent '{'
{ PlatformVariant }
SharedContract
'}' ;
PlatformVariant = '#[cfg(' CfgExpr ')]'
'variant' TypeIdent '{'
{ FnDecl }
'}' ;
SharedContract = 'contract' '{'
{ RequiresClause | EnsuresClause
| InvariantDecl | EffectsClause }
'}' ;
CfgExpr = Ident '=' StringLit
| 'not' '(' CfgExpr ')'
| 'any' '(' CfgExpr { ',' CfgExpr } ')'
| 'all' '(' CfgExpr { ',' CfgExpr } ')' ;
Full Example: Platform File Sync
platform FileSync {
// Unix: fdatasync or fsync
#[cfg(os = "unix")]
variant UnixSync {
fn sync(fd: FileDescriptor, full: Bool) -> Bool
effects: filesystem.sync
{
if full { fsync(fd) } else { fdatasync(fd) }
}
fn atomic_write(fd: FileDescriptor, data: Region<n>,
offset: Nat) -> Bool
requires { n <= SECTOR_SIZE }
effects: filesystem.write
{
pwrite(fd, data, offset)
}
}
// Windows: FlushFileBuffers
#[cfg(os = "windows")]
variant WindowsSync {
fn sync(fd: FileDescriptor, full: Bool) -> Bool
effects: filesystem.sync
{
FlushFileBuffers(fd)
// Windows has no fdatasync equivalent; always full sync
}
fn atomic_write(fd: FileDescriptor, data: Region<n>,
offset: Nat) -> Bool
requires { n <= SECTOR_SIZE }
effects: filesystem.write
{
WriteFile(fd, data, offset)
}
}
// Embedded: direct flash write
#[cfg(os = "none")]
variant BareMetalSync {
fn sync(fd: FileDescriptor, full: Bool) -> Bool
effects: filesystem.sync
{
flash_barrier()
}
fn atomic_write(fd: FileDescriptor, data: Region<n>,
offset: Nat) -> Bool
requires { n <= SECTOR_SIZE }
effects: filesystem.write
{
flash_write(fd, data, offset)
}
}
// ALL variants must satisfy these contracts
contract {
// After sync returns true, data is durable
ensures {
forall write W before sync():
sync() == true => W is durable on storage
}
// Atomic writes are all-or-nothing
ensures {
forall atomic_write(fd, data, offset):
result == true =>
read(fd, offset, len(data)) == data
// On crash during write: either old data or new data,
// never partial
}
// Sync must be idempotent
invariant { sync(fd, full) ; sync(fd, full) == sync(fd, full) }
// Platform must not introduce effects beyond what's declared
effects: filesystem.sync, filesystem.write
}
}
Verification Rule
- Each variant is verified independently against the shared contract
- The shared contract becomes a precondition for all callers, regardless of which variant is active at compile time
- If a variant cannot satisfy the shared contract (e.g., a platform
lacks atomic write guarantees), it must be explicitly excluded
with a
#[cfg(not(...))]guard on the caller
Error Codes
| Code | Message | Cause |
|---|---|---|
| A33001 | Platform variant V violates shared contract | Variant doesn’t satisfy ensures/invariant |
| A33002 | Missing variant for target platform P | Compilation target has no matching #[cfg] |
| A33003 | Variant has effects beyond shared contract | Platform-specific side effects not declared |
Rust Codegen
Platform contracts generate cfg-gated modules with a unified trait:
#![allow(unused)]
fn main() {
pub trait FileSync {
fn sync(&self, fd: RawFd, full: bool) -> bool;
fn atomic_write(&self, fd: RawFd, data: &[u8], offset: u64) -> bool;
}
#[cfg(unix)]
mod unix {
pub struct UnixSync;
impl FileSync for UnixSync {
fn sync(&self, fd: RawFd, full: bool) -> bool {
if full { libc::fsync(fd) == 0 }
else { libc::fdatasync(fd) == 0 }
}
fn atomic_write(&self, fd: RawFd, data: &[u8], offset: u64) -> bool {
debug_assert!(data.len() <= SECTOR_SIZE);
libc::pwrite(fd, data.as_ptr() as _, data.len(), offset as _) >= 0
}
}
}
#[cfg(windows)]
mod windows {
pub struct WindowsSync;
impl FileSync for WindowsSync {
fn sync(&self, fd: RawFd, _full: bool) -> bool {
unsafe { FlushFileBuffers(fd as _) != 0 }
}
fn atomic_write(&self, fd: RawFd, data: &[u8], offset: u64) -> bool {
debug_assert!(data.len() <= SECTOR_SIZE);
// ... WriteFile with OVERLAPPED ...
true
}
}
}
}
PLAT.2 Compile-Time Feature Flags
Contracts that adapt to compile-time feature configuration. When a feature is omitted, the contracts that depend on it are eliminated and the compiler verifies the remaining system is still consistent.
Motivation
SQLite has over 200 compile-time options (SQLITE_OMIT_* to remove
features, SQLITE_MAX_* to set limits, SQLITE_ENABLE_* to add
optional features). Examples: SQLITE_OMIT_WAL removes WAL support,
SQLITE_OMIT_FOREIGN_KEY removes foreign key enforcement,
SQLITE_MAX_PAGE_SIZE caps page size at compile time. When a feature
is omitted, all code paths that depend on it must be removed, and
any contract that references the removed feature must adapt. In C
this is done with #ifdef blocks scattered across 150,000 lines.
Assura makes this structured and verifiable.
Grammar
FeatureFlagDecl = 'feature' Ident [FeatureDefault]
[FeatureDeps] [FeatureExcludes] ;
FeatureDefault = '=' 'enabled' | '=' 'disabled' ;
FeatureDeps = 'requires' ':' IdentList ;
FeatureExcludes = 'excludes' ':' IdentList ;
FeatureGate = '#[feature(' Ident ')]' ;
FeatureMaxDecl = 'feature_max' Ident ':' TypeExpr '=' Expr ;
ConditionalContract = '#[feature(' Ident ')]' ContractClause ;
Full Example: SQLite Feature Configuration
// Feature declarations (in config module)
module config {
feature wal = enabled
feature fts5 = enabled
requires: wal // FTS5 needs WAL for atomic indexing
feature rtree = disabled
feature foreign_keys = enabled
feature json = enabled
feature icu = disabled
excludes: builtin_collation // ICU replaces built-in collation
// Compile-time maximums that narrow refinement types
feature_max max_page_size: Nat = 65536
feature_max max_sql_length: Nat = 1_000_000_000
feature_max max_column: Nat = 2000
feature_max max_attached: Nat = 10
feature_max max_variable_number: Nat = 32766
feature_max max_page_count: Nat = 4294967294 // 2^32 - 2
}
// Contracts that adapt to features
service Database {
// WAL operations only exist when WAL is enabled
#[feature(wal)]
operation wal_checkpoint {
input(mode: WalCheckpointMode)
output(pages_checkpointed: Nat)
requires { self.state @ Open }
effects: database.write, filesystem.write
}
// Page size is bounded by the compile-time maximum
type PageSize = {v: Nat |
v in {512, 1024, 2048, 4096, 8192, 16384, 32768, 65536}
and v <= config.max_page_size
}
// Column count is bounded
type ColumnIndex = {v: Nat | v < config.max_column}
// Foreign key enforcement only when feature is on
#[feature(foreign_keys)]
invariant {
forall fk in self.foreign_keys:
exists row in fk.parent_table:
row[fk.parent_column] == fk.child_value
}
}
// FTS5 module only exists when enabled
#[feature(fts5)]
module fts5 {
service FullTextSearch {
operation search {
input(query: FtsQuery, table: String)
output(results: List<FtsResult>)
effects: database.read
}
}
}
// COMPILE ERROR: using FTS5 without feature gate
fn bad_search(db: Database, query: String) -> List<Row>
effects: database.read
{
db.fts5_search(query) // A38001: fts5 feature not enabled
}
// CORRECT: gate the caller too
#[feature(fts5)]
fn search_if_available(db: Database, query: String) -> List<Row>
effects: database.read
{
db.fts5_search(query) // OK: caller is also gated
}
Verification Rule
- Feature consistency: If feature A requires feature B, enabling A without B is a compile error
- Feature exclusion: If A excludes B, enabling both is a compile error
- Contract narrowing: When
feature_max max_page_size = 4096, all refinement types involving page_size are automatically narrowed (the SMT context includespage_size <= 4096) - Dead code elimination: Gated modules/operations/contracts are fully removed when the feature is disabled; no codegen, no verification cost
- Feature propagation: If a function calls a feature-gated function, it must itself be gated or guard the call with a feature check
Error Codes
| Code | Message | Cause |
|---|---|---|
| A38001 | Feature F not enabled | Using feature-gated item without gate |
| A38002 | Feature F requires G which is disabled | Dependency not met |
| A38003 | Features F and G are mutually exclusive | Exclusion violated |
| A38004 | Feature max M too small for invariant | Max value makes contract unsatisfiable |
Rust Codegen
Feature flags map directly to Rust’s cfg and feature system:
#![allow(unused)]
fn main() {
// Cargo.toml
[features]
default = ["wal", "fts5", "foreign_keys", "json"]
wal = []
fts5 = ["wal"]
rtree = []
foreign_keys = []
json = []
icu = []
// Generated code
#[cfg(feature = "wal")]
pub mod wal {
pub fn checkpoint(db: &mut Database, mode: CheckpointMode)
-> Result<usize, SqlError>
{
debug_assert!(db.state == ConnectionState::Open);
// ...
}
}
// Compile-time constants from feature_max
pub const MAX_PAGE_SIZE: usize = 65536;
pub const MAX_SQL_LENGTH: usize = 1_000_000_000;
pub const MAX_COLUMN: usize = 2000;
// Refined types use the constant
pub struct PageSize(u32);
impl PageSize {
pub fn new(v: u32) -> Option<Self> {
if v.is_power_of_two() && v >= 512 && v <= MAX_PAGE_SIZE as u32 {
Some(PageSize(v))
} else {
None
}
}
}
}
PLAT.3 Resource Limit Contracts
Contracts for runtime-configurable resource limits that change what inputs are valid and how the system behaves when limits are reached.
Motivation
SQLite’s sqlite3_limit() lets users change 12 resource limits at
runtime: max SQL length, max column count, max expression depth, max
compound SELECT count, max VDBE operations, max function arguments,
max attached databases, max LIKE pattern length, max trigger depth,
max worker threads, max page count. These limits change the domain
of valid inputs. A query with 3000 columns is valid when
SQLITE_LIMIT_COLUMN is set to 4000 but rejected when set to 2000.
Static refinement types cannot express this because the bound is
not known at compile time.
Grammar
LimitDecl = 'limit' Ident ':' TypeExpr '{'
'default' ':' Expr
'min' ':' Expr
'max' ':' Expr
'on_exceed' ':' LimitAction
'}' ;
LimitAction = 'error' '(' Ident ')'
| 'truncate'
| 'deny' ;
LimitRef = 'limit(' Ident ')' ;
Full Example: SQLite Runtime Limits
module limits {
limit sql_length: Nat {
default: 1_000_000
min: 1
max: config.max_sql_length // compile-time upper bound
on_exceed: error(SQLITE_TOOBIG)
}
limit column_count: Nat {
default: 2000
min: 1
max: config.max_column
on_exceed: error(SQLITE_LIMIT)
}
limit expr_depth: Nat {
default: 1000
min: 1
max: 10000
on_exceed: error(SQLITE_LIMIT)
}
limit attached_db: Nat {
default: 10
min: 0
max: config.max_attached
on_exceed: error(SQLITE_LIMIT)
}
limit page_count: Nat {
default: 1073741823 // 2^30 - 1
min: 1
max: config.max_page_count
on_exceed: error(SQLITE_FULL)
}
limit vdbe_ops: Nat {
default: 0 // 0 = unlimited
min: 0
max: Nat.MAX
on_exceed: error(SQLITE_INTERRUPT)
}
}
// Using limits in contracts
fn parse_sql(
conn: Connection,
sql: String
) -> PreparedStatement | SqlError
requires { len(sql) <= limit(sql_length) }
requires { len(sql) > 0 }
effects: pure
{
let ast = parse(sql)?
// Column count checked against runtime limit
if count_columns(ast) > conn.limit(column_count) {
return SqlError(SQLITE_LIMIT, "too many columns")
}
// Expression depth checked against runtime limit
if expr_depth(ast) > conn.limit(expr_depth) {
return SqlError(SQLITE_LIMIT, "expression tree too deep")
}
prepare(conn, ast)
}
// VDBE execution with operation limit
fn vdbe_execute(
vm: VdbeEngine :_1,
limit: Nat // from conn.limit(vdbe_ops)
) -> (VdbeEngine :_1, VdbeResult)
ensures {
limit > 0 => vm.ops_executed <= limit
}
ensures {
result is VdbeResult.Interrupted =>
vm.ops_executed == limit
}
effects: database.read, database.write
Verification Rule
- Limit bounds: The compiler verifies that
min <= default <= maxand thatmax <= feature_max(if a compile-time upper bound exists) - Dynamic refinement: Inside a function that accesses a limit,
the SMT context includes
1 <= limit(X) <= max(X)but the exact value is symbolic - Exceed handling: Every code path where a limit could be exceeded must either check the limit or propagate the error
- Limit monotonicity: If a limit is lowered during execution, the compiler warns about existing objects that may violate the new limit
Error Codes
| Code | Message | Cause |
|---|---|---|
| A39001 | Limit L may be exceeded without check | No bounds check before limit-bounded operation |
| A39002 | Limit default outside [min, max] | Invalid limit configuration |
| A39003 | Limit max exceeds compile-time feature_max | Runtime max above static maximum |
| A39004 | Limit change may invalidate existing state | Lowering limit after creating objects at old limit |
Rust Codegen
Limits generate a configuration struct with runtime getter/setter and const bounds:
#![allow(unused)]
fn main() {
pub struct Limits {
sql_length: usize,
column_count: usize,
expr_depth: usize,
attached_db: usize,
page_count: usize,
vdbe_ops: usize,
}
impl Limits {
pub const fn defaults() -> Self {
Limits {
sql_length: 1_000_000,
column_count: 2000,
expr_depth: 1000,
attached_db: 10,
page_count: 1073741823,
vdbe_ops: 0,
}
}
pub fn set(&mut self, which: LimitId, value: usize) -> usize {
let old = self.get(which);
let clamped = value.clamp(
LimitId::min(which),
LimitId::max(which),
);
match which {
LimitId::SqlLength => self.sql_length = clamped,
LimitId::ColumnCount => self.column_count = clamped,
// ...
}
old
}
}
}
14.PERF: Performance
PERF.1 Unsafe Escape with Proof Obligation
A controlled escape hatch for performance-critical inner loops that need raw pointer arithmetic, unchecked indexing, or SIMD intrinsics, with a mandatory proof that safety invariants hold at the boundary.
Motivation
SQLite’s performance-critical code includes: cell parsing
(deserializing B-tree cells from raw bytes), record format
decoding (varint reading), memcpy for page operations, comparison
functions for sorting, and checksum computation. A naive safe Rust
port of these would add bounds checks on every byte access,
degrading performance by 10-30%. The unsafe escape lets the
programmer use unchecked operations inside a function while proving
at the boundary that all accesses are in-bounds.
This differs from invariant suspension (MISC.2): suspension pauses a data structure invariant temporarily. Unsafe escape pauses memory safety checks for performance, with a different proof obligation (bounds, alignment, aliasing rather than structural invariants).
Grammar
UnsafeEscape = '#[unsafe_escape(' ProofList ')]' FnDecl ;
ProofList = Proof { ',' Proof } ;
Proof = 'bounds' '(' Ident ',' Expr ',' Expr ')'
| 'aligned' '(' Ident ',' Expr ')'
| 'no_alias' '(' Ident ',' Ident ')'
| 'initialized' '(' Ident ',' Expr ',' Expr ')'
| 'valid_utf8' '(' Ident ')'
| 'non_null' '(' Ident ')' ;
Full Example: Fast Cell Parsing
// Safe wrapper with proof obligations
#[unsafe_escape(
bounds(page, offset, offset + cell_size),
bounds(page, header_offset, header_offset + header_size),
aligned(page, 1) // byte-aligned (no alignment requirement)
)]
fn parse_cell_fast(
page: Region<PageSize>,
offset: {v: Nat | v < PageSize},
cell_size: {v: Nat | v > 0 and offset + v <= PageSize}
) -> CellHeader
requires { offset + cell_size <= len(page) }
ensures {
result.payload_size == read_varint(page, offset)
and result.row_id == read_varint(page, offset + result.header_bytes)
}
effects: pure
{
// Inside this function, the compiler allows:
// - Unchecked indexing: page[i] without bounds check
// - Raw pointer arithmetic: ptr.add(n) without overflow check
// - Transmute for varint decoding
//
// The compiler verifies AT THE BOUNDARY that:
// - All indices are within [offset, offset + cell_size)
// - No read extends past offset + cell_size
// - The bounds proof (requires clause) guarantees safety
// Fast varint read (no bounds check per byte)
let (payload_size, bytes1) = read_varint_unchecked(page, offset)
let (row_id, bytes2) = read_varint_unchecked(page, offset + bytes1)
CellHeader {
payload_size,
row_id,
header_bytes: bytes1 + bytes2,
}
}
// Fast memory comparison for B-tree key ordering
#[unsafe_escape(
bounds(a, 0, a_len),
bounds(b, 0, b_len),
no_alias(a, b)
)]
fn memcmp_fast(
a: Region<a_len>,
b: Region<b_len>,
compare_len: {v: Nat | v <= a_len and v <= b_len}
) -> Ordering
requires { compare_len <= a_len and compare_len <= b_len }
ensures {
result == compare_bytes(a[0..compare_len], b[0..compare_len])
}
effects: pure
{
// SIMD-accelerated comparison when available
// Falls back to word-at-a-time comparison
// No per-byte bounds checks inside the loop
}
// COMPILE ERROR: unsafe escape without sufficient proof
#[unsafe_escape(
bounds(page, offset, offset + size)
// Missing: no proof that offset + size <= len(page)
)]
fn bad_parse(
page: Region<PageSize>,
offset: Nat, // Not refined! Could be >= PageSize
size: Nat
) -> CellHeader
// No requires clause proving bounds
// A42001: bounds proof obligation not satisfiable
{
// ...
}
Verification Rule
- Proof obligations: Each proof in the
#[unsafe_escape]list becomes an SMT query. If any proof fails, the function is rejected - Boundary checking: The compiler verifies proofs at the function boundary (entry + all exit points), not inside the body
- Internal freedom: Inside the function body, bounds checks, overflow checks, and alignment checks are elided
- Transitive safety: An unsafe escape function may only be
called from a context that satisfies its
requiresclauses - Audit trail: The compiler emits a report listing all unsafe escape functions and their proof status, for security review
Error Codes
| Code | Message | Cause |
|---|---|---|
| A42001 | Bounds proof not satisfiable for R[a..b] | Cannot prove index range is valid |
| A42002 | Alignment proof not satisfiable for P | Cannot prove pointer is aligned |
| A42003 | No-alias proof not satisfiable for P, Q | Cannot prove pointers don’t alias |
| A42004 | Unsafe escape without proof obligation | Using escape hatch without any proof |
| A42005 | Proof obligation references out-of-scope variable | Proof variable not accessible |
Rust Codegen
Unsafe escape generates Rust unsafe blocks with the proof
obligations as comments and debug assertions:
#![allow(unused)]
fn main() {
/// # Safety
/// Caller guarantees:
/// - offset + cell_size <= page.len()
/// - All reads are within [offset, offset + cell_size)
#[inline(always)]
pub fn parse_cell_fast(
page: &[u8],
offset: usize,
cell_size: usize,
) -> CellHeader {
debug_assert!(offset + cell_size <= page.len());
unsafe {
let ptr = page.as_ptr().add(offset);
// Varint decode without bounds checks
let (payload_size, bytes1) = read_varint_unchecked(ptr);
let (row_id, bytes2) = read_varint_unchecked(ptr.add(bytes1));
CellHeader {
payload_size,
row_id,
header_bytes: bytes1 + bytes2,
}
}
}
/// # Safety
/// Caller guarantees:
/// - a[0..compare_len] and b[0..compare_len] are valid
/// - a and b do not alias
#[inline(always)]
pub fn memcmp_fast(a: &[u8], b: &[u8], compare_len: usize)
-> std::cmp::Ordering
{
debug_assert!(compare_len <= a.len());
debug_assert!(compare_len <= b.len());
unsafe {
let result = libc::memcmp(
a.as_ptr() as *const _,
b.as_ptr() as *const _,
compare_len,
);
result.cmp(&0)
}
}
}
PERF.2 Complexity Bounds
Contracts that specify the asymptotic time and space complexity of operations, enabling the compiler to reject implementations that violate performance guarantees.
Motivation
SQLite guarantees B-tree lookup in O(log N) time. Hash table lookup is O(1) amortized. Full table scan is O(N). These are documented, tested, and users depend on them. Totality checking (Section 2) proves a function terminates, but it says nothing about how fast it terminates. A function that scans the entire database for every lookup is total but violates the O(log N) contract.
Complexity bounds are critical for a database: users choose indices, query plans, and schema designs based on complexity guarantees. A port that changes O(log N) lookup to O(N) is functionally correct but practically broken.
Grammar
ComplexityAnnotation = '#[complexity(' ComplexitySpec ')]' ;
ComplexitySpec = 'time' '=' BigO
| 'space' '=' BigO
| 'amortized_time' '=' BigO
| 'io_reads' '=' BigO
| 'io_writes' '=' BigO ;
BigO = 'O(1)' | 'O(log ' Ident ')'
| 'O(' Ident ')' | 'O(' Ident ' log ' Ident ')'
| 'O(' Ident '^2)' | 'O(' Ident '^' IntLit ')' ;
Full Example: SQLite Operation Complexity
// B-tree point lookup: O(log N) time, O(log N) IO reads
#[complexity(
time = O(log N),
space = O(1),
io_reads = O(log N)
)]
fn btree_lookup(
tree: BTree<K, V>,
key: K
) -> Option<V>
where N = tree.entry_count
requires { BTreeValid(tree) }
effects: database.read
// B-tree insertion: O(log N) amortized (rebalancing is rare)
#[complexity(
amortized_time = O(log N),
space = O(log N), // stack frames for rebalance
io_reads = O(log N),
io_writes = O(log N)
)]
fn btree_insert(
tree: BtCursor :_1,
key: K,
value: V
) -> BtCursor :_1
where N = tree.entry_count
effects: database.write
// Full table scan: O(N)
#[complexity(time = O(N), io_reads = O(N))]
fn table_scan(
cursor: BtCursor :_1
) -> (BtCursor :_1, List<Row>)
where N = cursor.tree.entry_count
effects: database.read
// Hash table lookup: O(1) amortized
#[complexity(amortized_time = O(1), space = O(1))]
fn hash_lookup(
table: HashTable<K, V>,
key: K
) -> Option<V>
effects: pure
// Sort: O(N log N) using merge sort
#[complexity(
time = O(N log N),
space = O(N),
io_reads = O(N),
io_writes = O(N)
)]
fn sort_result_set(
rows: List<Row>,
order_by: List<SortKey>
) -> List<Row>
where N = len(rows)
effects: database.read, database.write // spill to temp file
// COMPILE WARNING: implementation may violate complexity bound
#[complexity(time = O(log N))]
fn suspicious_lookup(
tree: BTree<K, V>,
key: K
) -> Option<V>
{
// This scans linearly! Violates O(log N) contract
for entry in tree.entries() { // A46001 warning
if entry.key == key { return Some(entry.value) }
}
None
}
// Query planner uses complexity to choose strategy
fn choose_join_strategy(
left: Table,
right: Table,
join_key: Column
) -> JoinStrategy
{
let left_n = left.row_count
let right_n = right.row_count
if right.has_index(join_key) {
// Nested loop with index: O(left_n * log(right_n))
JoinStrategy.NestedLoopIndex
} else if left_n * right_n < HASH_JOIN_THRESHOLD {
// Hash join: O(left_n + right_n) but O(right_n) space
JoinStrategy.HashJoin
} else {
// Sort-merge: O(left_n*log(left_n) + right_n*log(right_n))
JoinStrategy.SortMerge
}
}
Verification Approach
Complexity bounds are verified by a combination of:
- Loop analysis: Count the number of iterations in terms of
input size. A
forloop overtree.entries()is O(N); a binary search loop withmid = (lo + hi) / 2is O(log N) - Recursion depth: A function that recurses with halving input is O(log N); with linear reduction is O(N)
- Callee composition: If f is O(log N) and g calls f inside an O(N) loop, g is O(N log N)
- IO counting: Reads and writes to pages count toward
io_readsandio_writesbounds
Verification is best-effort (Layer 2, 10s). The compiler warns
when it cannot verify the bound rather than rejecting the code.
The #[generate_tests] annotation can generate benchmarks that
empirically validate complexity claims.
Error Codes
| Code | Message | Cause |
|---|---|---|
| A46001 | Implementation may exceed O(X) bound | Loop structure suggests higher complexity |
| A46002 | Recursive call does not reduce input | Recursion without decreasing measure |
| A46003 | Callee F with O(X) inside O(Y) loop | Composition exceeds declared bound |
| A46004 | IO bound exceeded: O(X) declared but O(Y) observed | More reads/writes than promised |
Rust Codegen
Complexity annotations generate benchmark tests that verify the bound empirically:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod complexity_tests {
use criterion::*;
// AUTO-GENERATED from #[complexity(time = O(log N))]
fn btree_lookup_scaling(c: &mut Criterion) {
let mut group = c.benchmark_group("btree_lookup");
for size in [100, 1000, 10_000, 100_000, 1_000_000] {
let tree = build_btree(size);
let key = random_key(&tree);
group.throughput(Throughput::Elements(1));
group.bench_with_input(
BenchmarkId::from_parameter(size),
&size,
|b, _| b.iter(|| tree.lookup(&key)),
);
}
group.finish();
// Post-analysis: fit curve, verify O(log N) not O(N)
}
}
}
14.TEST: Testing and Verification Workflow
TEST.1 Test Generation from Contracts
Automatic generation of property-based tests and fuzz targets from contract specifications. When formal verification times out or reaches undecidable territory, the compiler generates executable tests that check contracts empirically.
Motivation
SQLite has 90 million lines of test code for 150,000 lines of source. These tests were written by hand over 25 years. With Assura, contracts already express what the tests should check. When SMT verification succeeds, no test is needed. When it times out (Layer 2 budget exceeded), the compiler can automatically generate tests that check the contract on concrete inputs, providing practical assurance where formal proof is infeasible.
Grammar
TestGenAnnotation = '#[generate_tests' [TestGenConfig] ']' ;
TestGenConfig = '(' { TestGenOption } ')' ;
TestGenOption = 'strategy' '=' StrategyExpr
| 'cases' '=' IntLit
| 'fuzz_target' '=' BoolLit
| 'shrink' '=' BoolLit
| 'corpus' '=' StringLit
| 'timeout_per_case' '=' DurationExpr ;
StrategyExpr = 'random'
| 'boundary' // focus on boundary values
| 'coverage' // coverage-guided
| 'mutation' // mutation-based fuzzing
| 'grammar' // grammar-aware (for parsers)
| CustomStrategy ;
CustomStrategy = Ident '(' [ExprList] ')' ;
Full Example: Generated Tests for B-Tree
// Contract on B-tree insert -- SMT may time out on the
// structural invariant (Layer 2, undecidable)
#[generate_tests(
strategy = boundary,
cases = 10000,
fuzz_target = true,
shrink = true
)]
structural_invariant BTreeValid<K, V, Level: Nat> {
// All 6 invariants from Section TYPE.2
// ...
}
// The compiler generates:
// 1. A proptest harness checking all 6 invariants
// 2. A libfuzzer/cargo-fuzz target for continuous fuzzing
// 3. Boundary value generators (empty tree, single element,
// ORDER-1 elements, ORDER elements, ORDER+1 elements)
// Custom strategy for SQLite-specific patterns
#[generate_tests(
strategy = grammar(sql_grammar),
cases = 100000,
fuzz_target = true,
corpus = "tests/fuzz_corpus/"
)]
fn execute_select(
db: Database,
stmt: PreparedStatement,
params: List<Value>
) -> List<Row>
requires { stmt.is_valid() }
ensures { ... }
What Gets Generated
For each contract with #[generate_tests], the compiler produces:
1. Property-Based Test (proptest/quickcheck)
#![allow(unused)]
fn main() {
// AUTO-GENERATED from BTreeValid structural_invariant
#[cfg(test)]
mod generated_tests {
use proptest::prelude::*;
// Strategy: generate valid B-trees, then apply operations
fn arb_btree(max_size: usize) -> impl Strategy<Value = BTree<i64, Vec<u8>>> {
prop::collection::vec(
(any::<i64>(), prop::collection::vec(any::<u8>(), 0..100)),
0..max_size,
)
.prop_map(|entries| {
let mut tree = BTree::new();
for (k, v) in entries {
let _ = tree.insert(k, v);
}
tree
})
}
proptest! {
#[test]
fn btree_insert_preserves_invariant(
tree in arb_btree(1000),
key: i64,
value in prop::collection::vec(any::<u8>(), 0..100),
) {
let mut tree = tree;
let _ = tree.insert(key, value);
// Invariant 1: Keys sorted within each node
assert!(tree.verify_sorted_keys());
// Invariant 2: Subtree ordering
assert!(tree.verify_subtree_ordering());
// Invariant 3: All leaves at same depth
assert!(tree.verify_balanced());
// Invariant 4: Minimum occupancy
assert!(tree.verify_min_occupancy());
// Invariant 5: Key count == value count
assert!(tree.verify_key_value_count());
// Invariant 6: Child count == key count + 1
assert!(tree.verify_child_count());
}
#[test]
fn btree_delete_preserves_invariant(
tree in arb_btree(1000),
key: i64,
) {
let mut tree = tree;
let _ = tree.delete(&key);
assert!(tree.verify_all_invariants());
}
// Boundary cases
#[test]
fn btree_boundary_order_minus_1(
entries in prop::collection::vec(any::<i64>(), ORDER - 1),
) {
let mut tree = BTree::new();
for k in entries { let _ = tree.insert(k, vec![]); }
assert!(tree.verify_all_invariants());
}
#[test]
fn btree_boundary_exact_order(
entries in prop::collection::vec(any::<i64>(), ORDER),
) {
let mut tree = BTree::new();
for k in entries { let _ = tree.insert(k, vec![]); }
// This forces the first split
assert!(tree.verify_all_invariants());
}
}
}
}
2. Fuzz Target (cargo-fuzz / libfuzzer)
#![allow(unused)]
fn main() {
// AUTO-GENERATED fuzz target from BTreeValid contracts
// File: fuzz/fuzz_targets/btree_invariant.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
use arbitrary::{Arbitrary, Unstructured};
#[derive(Arbitrary, Debug)]
enum BTreeOp {
Insert { key: i64, value: Vec<u8> },
Delete { key: i64 },
Search { key: i64 },
}
fuzz_target!(|ops: Vec<BTreeOp>| {
let mut tree = BTree::new();
for op in ops {
match op {
BTreeOp::Insert { key, value } => {
let _ = tree.insert(key, value);
}
BTreeOp::Delete { key } => {
let _ = tree.delete(&key);
}
BTreeOp::Search { key } => {
let _ = tree.search(&key);
}
}
// Check ALL structural invariants after every operation
assert!(tree.verify_all_invariants(),
"Invariant violated after {:?}", op);
}
});
}
3. Boundary Value Tests
#![allow(unused)]
fn main() {
// AUTO-GENERATED boundary tests from refinement types
#[cfg(test)]
mod boundary_tests {
#[test]
fn page_size_boundaries() {
// From: where page_size in {512, 1024, ..., 65536}
for &size in &[512, 1024, 2048, 4096, 8192, 16384, 32768, 65536] {
let header = DatabaseHeader::with_page_size(size);
assert!(header.validate().is_ok());
}
// Just outside valid values
assert!(DatabaseHeader::with_page_size(511).validate().is_err());
assert!(DatabaseHeader::with_page_size(513).validate().is_err());
assert!(DatabaseHeader::with_page_size(0).validate().is_err());
assert!(DatabaseHeader::with_page_size(65537).validate().is_err());
}
#[test]
fn fixed_width_narrowing_boundaries() {
// From: U64 -> U32 narrowing contract
assert!(narrow_u64_to_u32(0).is_ok());
assert!(narrow_u64_to_u32(u32::MAX as u64).is_ok());
assert!(narrow_u64_to_u32(u32::MAX as u64 + 1).is_err());
assert!(narrow_u64_to_u32(u64::MAX).is_err());
}
}
}
Verification Interaction
Test generation interacts with the verification layers:
| Verification Result | Test Generation Action |
|---|---|
| Layer 0 passes | No tests generated (structural check sufficient) |
| Layer 1 passes | No tests generated (SMT proved it) |
| Layer 2 passes | No tests generated (heavy SMT proved it) |
| Layer 2 times out | Generate proptest + fuzz target (practical assurance) |
| Layer 2 unknown | Generate proptest + fuzz target + warning |
#[generate_tests] explicit | Always generate regardless of verification result |
CLI Integration
$ assura test --generate
Generated 47 property-based tests from 12 contracts
Generated 8 fuzz targets from 4 structural invariants
Generated 156 boundary tests from 23 refinement types
$ assura fuzz btree_invariant --time 3600
Running fuzz target: btree_invariant
Corpus: 234 entries, 1,247 executions/sec
No crashes found in 3600s
$ assura test --run-generated
Running 47 property tests (10000 cases each)...
All passed.
Running 156 boundary tests...
All passed.
Error Codes
Test generation does not add new error codes. It uses existing verification warnings:
- A22001 (verification timeout) triggers test generation
- A22002 (unknown result) triggers test generation + warning
- Test failures at runtime produce standard Rust panics with the contract that was violated in the message
TEST.2 Behavioral Equivalence
Contracts that assert two implementations produce the same observable output for all valid inputs. Used when porting from one language/implementation to another.
Motivation
A Rust port of SQLite is worthless if it doesn’t produce the exact same results as the C version. “Same results” is precise: for any database file and any SQL statement, the Rust version must produce the same rows in the same order with the same types and the same error codes. Behavioral equivalence contracts let you express this at the module level and verify it via differential testing.
Grammar
EquivalenceDecl = 'equivalent' TypeIdent '~' TypeIdent '{'
{ EquivalenceMapping }
{ EquivalenceExclusion }
'}' ;
EquivalenceMapping = Ident '<->' Ident
[EquivalenceCondition] ;
EquivalenceCondition = 'when' Predicate ;
EquivalenceExclusion = 'except' Ident ':' StringLit ;
Full Example: C SQLite vs Rust SQLite
// Declare behavioral equivalence between C and Rust implementations
equivalent CSqlite ~ RustSqlite {
// Core API equivalence
sqlite3_open <-> Connection::open
sqlite3_close <-> Connection::close
sqlite3_exec <-> Connection::exec
sqlite3_prepare_v2 <-> Connection::prepare
sqlite3_step <-> VdbeExecution::step
sqlite3_finalize <-> VdbeExecution::finalize
sqlite3_column_int <-> Row::get_int
sqlite3_column_text <-> Row::get_text
sqlite3_column_blob <-> Row::get_blob
sqlite3_column_double <-> Row::get_double
sqlite3_column_type <-> Row::column_type
// For each mapping, the equivalence contract is:
// forall valid_inputs:
// C_function(inputs) == Rust_function(inputs)
// where == is defined on observable output (not internal state)
// Error code equivalence
sqlite3_errcode <-> Connection::error_code
when error_code in {
SQLITE_OK, SQLITE_ERROR, SQLITE_BUSY, SQLITE_LOCKED,
SQLITE_NOMEM, SQLITE_READONLY, SQLITE_INTERRUPT,
SQLITE_IOERR, SQLITE_CORRUPT, SQLITE_FULL,
SQLITE_CANTOPEN, SQLITE_CONSTRAINT, SQLITE_MISMATCH,
SQLITE_MISUSE, SQLITE_AUTH, SQLITE_RANGE, SQLITE_NOTADB
}
// Explicit exclusions (documented deviations)
except sqlite3_randomness:
"Rust uses a different CSPRNG; output differs but
security properties are equivalent"
except sqlite3_compileoption_get:
"Compile options differ between C and Rust builds"
except sqlite3_sourceid:
"Source identification differs by definition"
}
// Per-function equivalence with specific invariants
fn verify_select_equivalence(
db_bytes: Region<n>, // raw database file
sql: String
) -> Bool
#[equivalence_test]
{
let c_result = c_sqlite.open(db_bytes).exec(sql)
let r_result = rust_sqlite.open(db_bytes).exec(sql)
// Same number of rows
len(c_result.rows) == len(r_result.rows)
// Same values in same order
and forall i in 0..len(c_result.rows):
forall j in 0..len(c_result.rows[i].columns):
c_result.rows[i].columns[j] == r_result.rows[i].columns[j]
// Same error code if error
and c_result.error_code == r_result.error_code
}
// Equivalence for file format (round-trip)
fn verify_format_equivalence(
ops: List<SqlOperation>
) -> Bool
#[equivalence_test]
{
// Apply same operations to both implementations
let c_db = apply_operations(c_sqlite, ops)
let r_db = apply_operations(rust_sqlite, ops)
// Database files must be byte-identical
// (not just semantically equivalent)
c_db.to_bytes() == r_db.to_bytes()
}
Verification Approach
Behavioral equivalence cannot be proved by SMT (it requires running both implementations). Instead, the compiler generates:
- Differential test harness: Runs both implementations on the same inputs and compares outputs
- Differential fuzz target: Fuzzes both implementations simultaneously, flagging any divergence
- Golden file tests: Extracts expected outputs from the reference implementation’s test suite
- Migration tests: Verifies database files created by C SQLite can be read by Rust SQLite and vice versa
Error Codes
| Code | Message | Cause |
|---|---|---|
| A41001 | Output divergence detected | Implementations produce different results |
| A41002 | Error code mismatch | Different error for same invalid input |
| A41003 | Row ordering difference | Same rows but different order |
| A41004 | Type coercion difference | Same value, different SQLite type affinity |
| A41005 | Undocumented exclusion | Divergence found that isn’t in except list |
Rust Codegen
Equivalence declarations generate differential test infrastructure:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod equivalence_tests {
use libsqlite3_sys as csqlite; // C SQLite bindings
use crate as rsqlite; // Rust implementation
/// Differential fuzzing target
#[cfg(feature = "fuzz")]
pub fn differential_fuzz(data: &[u8]) {
let (db_bytes, sql) = match split_fuzz_input(data) {
Some(v) => v,
None => return,
};
let c_result = run_c_sqlite(db_bytes, sql);
let r_result = run_rust_sqlite(db_bytes, sql);
assert_eq!(
c_result.error_code, r_result.error_code,
"Error code mismatch: C={}, Rust={}",
c_result.error_code, r_result.error_code,
);
if c_result.error_code == SQLITE_OK {
assert_eq!(
c_result.rows, r_result.rows,
"Row divergence on SQL: {}",
std::str::from_utf8(sql).unwrap_or("<binary>"),
);
}
}
/// Run C SQLite's test suite against Rust implementation
#[test]
fn c_test_suite_compatibility() {
for test_case in load_sqlite_test_cases("tests/c_compat/") {
let r_result = run_rust_sqlite(
&test_case.db_bytes,
&test_case.sql,
);
assert_eq!(r_result, test_case.expected_output,
"Failed on test: {}", test_case.name);
}
}
}
}
TEST.3 Multi-Pass Refinement
Contracts for computations where each pass over the data refines the output, converging toward a final result. Each pass must improve (or at least not worsen) the output quality.
Motivation
MISC.1 (Incremental/Coroutine) handles producer/consumer
patterns where each step() produces independent output. But
some algorithms require multiple passes that REFINE the same
output:
- JPEG progressive mode: The first pass produces a coarse image (DC coefficients only). Each subsequent pass adds AC coefficient bands, refining detail. After all passes, the image matches the baseline decode.
- Interlaced PNG: Adam7 interlacing makes 7 passes. Each pass fills in more pixels. The partial image is viewable at every stage but incomplete until pass 7.
- Iterative solvers: Newton-Raphson, gradient descent, or any convergent algorithm where each iteration reduces error.
Without refinement contracts, there is no way to prove:
- Each pass produces a valid (if incomplete) result
- The quality monotonically improves (or at least does not degrade)
- After all passes, the output matches the non-progressive version
- A partial result (after N < total passes) is still usable
Grammar
RefinementDecl = 'refinement' Ident '{'
'passes' ':' Expr ','
'state' ':' TypeExpr ','
'quality' ':' QualityMetric ','
{ RefinementRule }
'}' ;
QualityMetric = 'metric' FnIdent
| 'monotonic_field' Ident ;
RefinementRule = 'after_each' ':' Predicate
| 'after_all' ':' Predicate
| 'convergence' ':' Predicate ;
Full Example: JPEG Progressive Decode
// Refinement contract for JPEG progressive decoding
refinement JpegProgressive {
passes: scan_count, // determined by SOS markers in file
state: PartialImage,
quality: metric image_psnr, // quality measured by PSNR
// After each pass, the image is valid (no garbage pixels)
after_each:
for_all(x in 0..state.width, y in 0..state.height,
state.pixel(x, y).is_valid()
)
// After all passes, output matches baseline decode
after_all:
for_all(x in 0..state.width, y in 0..state.height,
state.pixel(x, y) == baseline_decode(input).pixel(x, y)
)
// Each pass does not decrease quality (PSNR is non-decreasing)
convergence:
image_psnr(state, reference) >= image_psnr(old(state), reference)
}
type PartialImage {
pixels: Region<image_size>,
width: U32,
height: U32,
passes_completed: U32,
coefficients: [[I16; 64]], // accumulated DCT coefficients
invariant {
passes_completed <= max_passes,
pixels.len() == width as U64 * height as U64 * 3
}
}
fn progressive_decode_pass(
image: &mut PartialImage :refine,
scan: &ScanHeader,
data: &[U8] :tainted
) -> () | DecodeError
#[refinement(JpegProgressive)]
requires { image.passes_completed < max_passes }
ensures {
image.passes_completed == old(image.passes_completed) + 1
}
{
// Decode this scan's spectral selection (Ss..Se)
// and successive approximation (Ah, Al)
for mcu in 0..image.mcu_count() {
decode_mcu_progressive(image, scan, data, mcu)?
}
// Update pixels from refined coefficients
for block in 0..image.block_count() {
idct_and_update(image, block)
}
image.passes_completed += 1
}
// Adam7 interlaced PNG: 7 fixed passes
refinement PngAdam7 {
passes: 7,
state: InterlacedImage,
quality: monotonic_field pixels_filled,
after_each:
state.pixels_filled >= adam7_cumulative_pixels(
state.current_pass
)
after_all:
state.pixels_filled == state.width * state.height
}
Verification Rule
- Quality monotonicity: The verifier checks that the quality
metric is non-decreasing after each pass. For
metricfunctions, this is checked by SMT. Formonotonic_field, the monotonic state machinery (STOR.5) is reused - Convergence: The
after_allpredicate is checked as a postcondition of the final pass. The verifier confirms that whenpasses_completed == max_passes, the predicate holds - Partial validity: The
after_eachpredicate must hold after every pass, not just the final one. The verifier treats it as a loop invariant across the pass sequence - Pass count: For fixed-pass refinements (PNG Adam7: 7), the verifier can unroll and check each pass. For dynamic pass counts (JPEG progressive: determined by file), the verifier uses induction on pass number
Error Codes
| Code | Message | Cause |
|---|---|---|
| A53001 | Quality decreased after pass N | Refinement pass worsened output |
| A53002 | After-each invariant violated at pass N | Partial result is invalid |
| A53003 | After-all predicate not satisfied | Final output does not match expected |
| A53004 | Pass count exceeds declared maximum | More passes than passes specifies |
| A53005 | Refinement state not initialized before first pass | Missing initialization |
| A53006 | Quantifier missing trigger annotation | Quantifier has no triggers clause for verification performance |
Rust Codegen
Refinement contracts generate a pass-tracking wrapper with quality assertions:
#![allow(unused)]
fn main() {
pub struct RefinementState<T> {
inner: T,
passes_completed: u32,
max_passes: u32,
#[cfg(debug_assertions)]
last_quality: Option<f64>,
}
impl<T> RefinementState<T> {
pub fn apply_pass<F>(&mut self, pass_fn: F) -> Result<(), DecodeError>
where
F: FnOnce(&mut T) -> Result<(), DecodeError>,
{
assert!(self.passes_completed < self.max_passes,
"exceeded max passes");
pass_fn(&mut self.inner)?;
self.passes_completed += 1;
#[cfg(debug_assertions)]
{
let quality = self.measure_quality();
if let Some(prev) = self.last_quality {
debug_assert!(quality >= prev,
"quality decreased: {prev} -> {quality}");
}
self.last_quality = Some(quality);
}
Ok(())
}
pub fn is_complete(&self) -> bool {
self.passes_completed == self.max_passes
}
}
}
14.MISC: Specialized
MISC.1 Incremental and Coroutine Contracts
Contracts for functions that produce results incrementally (one at a time), suspending between yields and resuming on the next call.
Motivation
SQLite’s sqlite3_step() is a coroutine: each call advances the
VDBE virtual machine until it produces a row (SQLITE_ROW) or
finishes (SQLITE_DONE). Between calls, the VM is suspended with
its full state preserved. The caller must follow the protocol:
call step() until DONE, then finalize(). Calling step()
after DONE is undefined. Calling finalize() before DONE is
allowed (abort). This is a producer/consumer protocol that typestate
alone cannot fully express because the number of yields is
data-dependent.
Grammar
IncrementalDecl = 'incremental' TypeIdent [TypeParams] '{'
YieldType
FinalType
{ IncrementalInvariant }
{ IncrementalTransition }
'}' ;
YieldType = 'yields' ':' TypeExpr ;
FinalType = 'completes' ':' TypeExpr ;
IncrementalTransition = 'on' IncrementalEvent ':'
IncrementalAction ;
IncrementalEvent = 'step' | 'abort' | 'reset' | 'error' ;
IncrementalAction = '{' { OperationItem } '}' ;
Full Example: VDBE Step Protocol
incremental VdbeExecution<Row> {
// What each step() produces
yields: Row
// What finalize() returns after all rows
completes: StepResult // Done | Error
// States: Ready -> Stepping -> Done | Aborted
states { Ready, Stepping, Done, Aborted, Error }
// Valid transitions
transition Ready -> Stepping via step
transition Stepping -> Stepping via step // more rows
transition Stepping -> Done via step // no more rows
transition Stepping -> Aborted via abort
transition Ready -> Aborted via abort
transition Done -> Ready via reset
transition Aborted -> Ready via reset
transition Error -> Aborted via abort
transition Aborted -> Aborted via abort // idempotent
on step {
requires { self.state @ Ready or self.state @ Stepping }
ensures {
result is Yield(row) =>
self.state @ Stepping
and row satisfies stmt.column_contracts
}
ensures {
result is Complete(StepResult.Done) =>
self.state @ Done
}
ensures {
result is Complete(StepResult.Error(e)) =>
self.state @ Error
}
effects: database.read
}
on abort {
// Can abort at any time (releases resources)
requires {
self.state @ Ready
or self.state @ Stepping
or self.state @ Error
or self.state @ Aborted
}
ensures { self.state @ Aborted }
// All held locks released
ensures {
forall lock in old(self.held_locks): lock.released
}
effects: database.read
}
on reset {
// Re-execute from the beginning
requires {
self.state @ Done or self.state @ Aborted
}
ensures { self.state @ Ready }
ensures { self.cursor_position == 0 }
effects: pure
}
// Invariant: stepping must make progress or terminate
invariant {
self.state @ Stepping =>
self.instruction_pointer > old(self.instruction_pointer)
or self.state != old(self.state)
}
// Invariant: resource cleanup on terminal states
invariant {
(self.state @ Done or self.state @ Aborted) =>
self.held_locks == empty
and self.temp_tables == empty
}
}
// Usage
fn query_all_rows(
stmt: VdbeExecution<Row> :_1
) -> (VdbeExecution<Row> :_1, List<Row>)
requires { stmt.state @ Ready }
ensures { fst(result).state @ Done or fst(result).state @ Error }
effects: database.read
{
let mut rows = List.empty()
let mut s = stmt
loop {
match s.step() {
Yield(row) => {
rows = rows.append(row)
// s is now @ Stepping, loop continues
}
Complete(StepResult.Done) => {
// s is now @ Done
return (s, rows)
}
Complete(StepResult.Error(e)) => {
s = s.abort()
return (s, rows) // partial results
}
}
}
}
Verification Rule
- Protocol compliance: The compiler verifies that
step()is only called inReadyorSteppingstates (typestate check) - Termination: If the
invariantclause includes a progress measure, the compiler checks that the coroutine cannot step forever without producing a result or transitioning toDone - Resource cleanup: The
on abortcontract ensures all resources are released regardless of when the abort happens - Linear ownership: The incremental value is linear, preventing two threads from stepping the same coroutine
Error Codes
| Code | Message | Cause |
|---|---|---|
| A40001 | Step called in invalid state S | Stepping after Done or Aborted |
| A40002 | Incremental value not finalized | Value dropped without reaching Done or Aborted |
| A40003 | Incremental progress not guaranteed | Step may loop without yielding or completing |
| A40004 | Resources not released on terminal state | Held locks or temp tables survive abort |
Rust Codegen
Incremental contracts generate Rust iterators or async streams:
#![allow(unused)]
fn main() {
pub enum StepResult<T> {
Row(T),
Done,
Error(SqlError),
}
pub struct VdbeExecution<'conn> {
vm: VdbeVm,
conn: &'conn Connection,
state: VdbeState,
}
impl<'conn> VdbeExecution<'conn> {
pub fn step(&mut self) -> StepResult<Row> {
debug_assert!(
self.state == VdbeState::Ready
|| self.state == VdbeState::Stepping
);
match self.vm.execute_next() {
VmResult::Row(row) => {
self.state = VdbeState::Stepping;
StepResult::Row(row)
}
VmResult::Done => {
self.state = VdbeState::Done;
self.release_locks();
StepResult::Done
}
VmResult::Error(e) => {
self.state = VdbeState::Error;
StepResult::Error(e)
}
}
}
pub fn abort(&mut self) {
self.release_locks();
self.drop_temp_tables();
self.state = VdbeState::Aborted;
}
pub fn reset(&mut self) {
debug_assert!(
self.state == VdbeState::Done
|| self.state == VdbeState::Aborted
);
self.vm.reset();
self.state = VdbeState::Ready;
}
}
// Also implements Iterator for convenience
impl<'conn> Iterator for VdbeExecution<'conn> {
type Item = Result<Row, SqlError>;
fn next(&mut self) -> Option<Self::Item> {
match self.step() {
StepResult::Row(row) => Some(Ok(row)),
StepResult::Done => None,
StepResult::Error(e) => Some(Err(e)),
}
}
}
// Drop ensures cleanup even if user forgets to abort
impl<'conn> Drop for VdbeExecution<'conn> {
fn drop(&mut self) {
if self.state != VdbeState::Done
&& self.state != VdbeState::Aborted
{
self.abort();
}
}
}
}
MISC.2 Scoped Invariant Suspension
Temporary suspension of an invariant within a function, with a proof obligation that the invariant is restored before the function returns.
Motivation
SQLite’s B-tree balance() is the most complex function in the
codebase. It redistributes cells across sibling pages, temporarily
violating the B-tree ordering invariant. The invariant must be
restored before balance() returns.
Grammar
SuspendAnnotation = '#[suspend_invariant(' TypeIdent ')]' ;
RestoredClause = 'ensures' '{' TypeIdent 'restored' '}' ;
Usage
fn balance(
cursor: BtCursor :_1,
parent_index: Nat
) -> BtCursor :_1
#[suspend_invariant(BTreeValid)]
ensures { BTreeValid restored }
ensures { all_keys(old(cursor.tree)) == all_keys(cursor.tree) }
effects: database.write
{
// During this function:
// - BTreeValid is NOT checked on intermediate states
// - The cursor is linear (no other function can observe the tree)
// - The compiler verifies BTreeValid holds at every return point
let (left, middle, right) = split_siblings(cursor, parent_index)
// BTreeValid is violated here: keys may be unbalanced
let redistributed = redistribute_cells(left, middle, right)
// BTreeValid is being restored: cells are moved between siblings
merge_siblings(cursor, redistributed)
// BTreeValid must hold here (compiler checks at return)
}
Verification Rule
When #[suspend_invariant(I)] is present:
- The invariant
Iis NOT checked on intermediate states within the function body - The invariant
IIS checked at every return point (including early returns and error paths) - The suspended variable must be linear (to prevent observation during suspension)
- Calling any function that expects
Ito hold within the suspension scope is a compile error
Appendix A: Grammar Statistics
| Category | Production Rules | Keywords |
|---|---|---|
| Lexical | 8 | 53 |
| Top-level | 5 | - |
| Services | 3 | - |
| Types | 12 | - |
| Contracts | 10 | - |
| Predicates | 8 | - |
| Extended layers (8-27) | 14 | - |
| Extern/Bind | 4 | - |
| Effects | 3 | - |
| CORE (verification infrastructure) | 16 | 22 |
| MEM (memory safety) | 14 | 10 |
| TYPE (types and contracts) | 8 | 6 |
| SEC (trust and security) | 16 | 18 |
| CONC (concurrency) | 20 | 28 |
| STOR (storage and durability) | 15 | 20 |
| FMT (data formats and parsing) | 18 | 22 |
| NUM (numerical and precision) | 6 | 8 |
| PLAT (platform and configuration) | 8 | 10 |
| PERF (performance) | 6 | 6 |
| TEST (testing and verification) | 8 | 8 |
| MISC (specialized) | 4 | 4 |
| Total | 195 | 199 |
Keywords by category:
- CORE:
ghost,lemma,apply,induction,cases,modifies,reads,axiom,define,property,trigger,auto_trigger,opaque,reveal,prophecy,resolve,liveness,eventually,leads_to,eventually_within,eventually_always,fair - MEM:
region,allocator,shared_memory,layout,atomic,atomic_load,circular_buffer,write_pos,valid_count,slide - TYPE:
interface,impl,structural_invariant,error_policy,must_propagate,must_not_mask - SEC:
ffi,export,caller_guarantees,callee_guarantees,error_convention,constant_time,secret,secure_erase,erase,spec,conforms,axiom_spec,algorithm - CONC:
callback,must_not_call,must_not_reenter,may_call,must_be,deterministic,lock_rank,lock_order,deadline,timeout,ordering,relaxed,acquire,release,acq_rel,seq_cst,fence,view,stale_view,merge - STOR:
recovery,durable_state,crash_point,recovers_to,cache,pinned,snapshot,monotonic,storage_model,on_crash_during,erase_value,prog_idempotent - FMT:
format,bit_format,bits,codec_registry,codec,magic,integrity,encoding_matches,protocol,rfc,conforms,deviation,accepts,rejects - NUM:
precision,max_ulp_error,max_abs_error,table,precompute,verify_against - PLAT:
platform,variant,cfg,feature,limit,on_exceed - PERF:
unsafe_escape,bounds,complexity,amortized_time - TEST:
generate_tests,equivalent,refinement,passes,quality,convergence - MISC:
incremental,yields,frozen,extensible
Appendix B: Type System Summary
| Feature | Source | SMT Required | Decidable |
|---|---|---|---|
| Refinement types | Liquid Haskell, Flux | Yes (QF_UFLIA) | Yes (QF) |
| Dependent types (restricted) | Idris 2 | Yes (LIA) | Yes |
| Linear/graded types | Granule, QTT | No (algorithmic) | Yes |
| Typestate | Plaid | No (DFA) | Yes |
| Effect rows | Koka | No (set ops) | Yes |
| Information flow | Jif, FlowCaml | Yes (QF_DT) | Yes |
| Totality | Idris 2, Agda | Partial (fuel) | Semidecidable |
| Memory regions | ATS, Flux | Yes (QF_UFLIA) | Yes |
| Fixed-width integers | Ada, SPARK | Yes (QF_BV/LIA) | Yes |
| Untrusted data taint | Jif, Ur/Web | Yes (QF_DT) | Yes |
| Shared memory protocols | Session types, TLA+ | BMC | Bounded |
| Allocator contracts | Separation logic | Yes (HORN) | Semidecidable |
| Interface contracts | Eiffel, SPARK | Inherited | Inherited |
| Structural invariants | Dafny, Lean | Yes (AUFLIA) | No |
| Integrity contracts | Vest | Yes (AUFLIA) | No |
| Invariant suspension | Iris, RustBelt | Yes (AUFLIA) | No |
| Binary format contracts | Kaitai Struct, Daedalus | Yes (QF_BV) | Yes |
| Crash recovery | TLA+, Coyote | BMC | Bounded |
| Platform abstraction | Rust cfg, C #ifdef | Inherited | Inherited |
| Callback re-entrancy | Cyclone, RustBelt | No (call graph) | Yes |
| Determinism | Ur/Web | No (taint analysis) | Yes |
| Transactional rollback | STM, Haskell | Yes (pre/post) | Yes |
| FFI boundary | Prusti, Creusot | Yes (QF_UFLIA) | Yes |
| Test generation | QuickCheck, Hypothesis | N/A (meta) | N/A |
| Feature flags | Rust cfg, C #ifdef | No (structural) | Yes |
| Resource limits | Ada constraints | Yes (QF_LIA) | Yes |
| Incremental/coroutine | Session types | No (DFA) | Yes |
| Behavioral equivalence | QuickChick, Csmith | N/A (testing) | N/A |
| Unsafe escape | Prusti, Verus | Yes (QF_UFLIA) | Yes |
| String encoding | Ur/Web, ATS | Yes (QF_BV) | Yes |
| Page cache | Linear Haskell, ATS | No (ref counting) | Yes |
| MVCC/snapshot isolation | TLA+, Alloy | BMC | Bounded |
| Complexity bounds | RAML, AARA | Partial (heuristic) | Semidecidable |
| Monotonic state | TLA+, Coyote | No (temporal logic) | Yes |
| Error propagation | Elm, Midori | No (taint analysis) | Yes |
| Bit-level format | Kaitai Struct, Nom | Yes (QF_BV) | Yes |
| Precomputed tables | Coq, Lean | No (exhaustive eval) | Yes |
| Numerical precision | Fluctuat, Gappa | Partial (interval arith) | Bounded |
| Codec registry | Serde, nom | No (structural) | Yes |
| Multi-pass refinement | TLA+, Dafny | Yes (induction) | Semidecidable |
| Ghost code | SPARK, Dafny, Verus | No (erased) | N/A |
| Lemmas / proof functions | Dafny, Verus, Lean | Yes (SMT) | Semidecidable |
| Frame conditions | Frama-C, SPARK, Dafny | No (structural) | Yes |
| Axiomatic definitions | Frama-C, Dafny, Why3 | Yes (axiom) | Assumed |
| Quantifier triggers | Verus, Dafny, Z3 | N/A (SMT hint) | N/A |
| Opaque functions | Dafny, Verus | No (contract only) | Yes |
| Prophecy variables | Iris, TaDA, Abadi-Lamport | Yes (existential) | Semidecidable |
| Liveness contracts | TLA+, SPIN, nuXmv | BMC + k-induction | Bounded/Semidecidable |
| Constant-time execution | ct-verif, FaCT | No (info flow) | Yes |
| Secure erasure | Zeroize, SecureZeroMemory | No (erasure check) | Yes |
| Lock ordering | SPARK, ThreadSanitizer | No (rank tracking) | Yes |
| Temporal deadlines | TLA+, UPPAAL | BMC | Bounded |
| Storage failure model | TLA+, CrashMonkey | BMC | Bounded |
| Protocol grammar conformance | Everparse, Hammer | Yes (grammar check) | Yes |
| Circular buffer contracts | ATS, Dafny | Yes (QF_UFLIA) | Yes |
| Crypto spec conformance | HACL*, Fiat-Crypto, Jasmin | Yes (algebra) | Semidecidable |
| Weak memory ordering | GPS, RSL, Iris-Relaxed | Yes (view logic) | Decidable (QF_UFLIA) |
Appendix C: Rust Codegen Summary
| Assura Construct | Rust Representation |
|---|---|
| Refined type | Newtype + debug_assert |
| Typestate | PhantomData + state modules |
| Effects | Capability traits as parameters |
| Linear types | Move semantics (ownership) |
| Info flow labels | Erased (compile-time only) |
| Dependent indices | Erased (compile-time only) |
| Contracts | debug_assert in debug mode |
| Measures | cfg(debug_assertions) functions |
| Extern | Trait definition |
| Bind | Wrapper with assertions |
| Memory regions | &[u8] / &mut [u8] with bounds checks |
| Fixed-width integers | u8/u16/u32/u64/i32/i64 with checked casts |
| Taint labels | Erased; Unverified<T>/Verified<T> wrappers |
| Shared memory | mmap + AtomicU32 with Ordering annotations |
| Allocator contracts | std::alloc::Allocator trait impl |
| Interface contracts | Rust trait definitions |
| Structural invariants | Recursive debug_assert functions |
| Integrity contracts | Unverified<T>/Verified<T> wrappers |
| Invariant suspension | Scoped unsafe block with restore assert |
| Binary format contracts | Zero-copy parser structs over &[u8] |
| Crash recovery | Recovery function + savepoint structs |
| Platform abstraction | cfg-gated modules + unified trait |
| Callback re-entrancy | Marker traits (NoReenter<T>) + Send bounds |
| Determinism | Lint attrs + BTreeMap enforcement |
| Transactional rollback | Savepoint/rollback closures |
| FFI boundary | extern “C” + safety wrappers + debug_assert |
| Test generation | proptest harness + libfuzzer target + boundary tests |
| Feature flags | Cargo features + cfg-gated modules + const bounds |
| Resource limits | Config struct with clamped setter + const defaults |
| Incremental/coroutine | Iterator impl + state enum + Drop cleanup |
| Behavioral equivalence | Differential test harness + fuzz target |
| Unsafe escape | unsafe block + debug_assert proofs + #[inline(always)] |
| String encoding | PhantomData<E> + Encoding trait + validated constructors |
| Page cache | PinnedPage RAII guard + AtomicU32 pin count |
| MVCC/snapshot isolation | ReadTransaction/WriteTransaction + &mut exclusion |
| Complexity bounds | Criterion benchmarks (empirical verification) |
| Monotonic state | MonotonicU32 wrapper + debug_assert on advance |
| Error propagation | #[must_use] + CriticalError drop bomb + MustUse wrappers |
| Bit-level format | BitReader wrapper with bounds-checked read_bits |
| Precomputed tables | const arrays + build.rs generation + test verification |
| Numerical precision | Test harness with reference comparison + ULP checks |
| Codec registry | Dispatch function with magic-byte pattern matching |
| Multi-pass refinement | RefinementState wrapper with quality tracking |
| Ghost code | Completely erased; debug_assert in debug mode |
| Lemmas / proof functions | Completely erased (proof-only) |
| Frame conditions | Erased; debug_assert field equality in debug mode |
| Axiomatic definitions | Erased; simple axioms become debug_assert checks |
| Quantifier triggers | Erased (SMT directives only) |
| Opaque functions | Normal Rust code (opacity is verification-only) |
| Prophecy variables | Completely erased (verification-only) |
| Liveness contracts | Erased; optional debug_assert monitors in debug mode |
| Constant-time execution | Normal code + core::hint::black_box for secrets |
| Secure erasure | Drop impl with volatile zero write + compiler fence |
| Lock ordering | Erased; debug_assert witness system in debug mode |
| Temporal deadlines | Timer registration + handler dispatch |
| Storage failure model | Fault-injection test harness |
| Protocol grammar conformance | Erased; deviation comments + optional runtime logging |
| Circular buffer contracts | Struct with modular indexing + slide method + debug_assert |
| Crypto spec conformance | debug_assert(result == reference) in debug; erased in release |
| Weak memory ordering | Rust atomic operations with exact ordering preserved; debug view checks |
Appendix D: Error Code Summary (All Categories)
Core Language Errors
| Range | Category | Count | Layer |
|---|---|---|---|
| A-LANG-001-005 | Syntax | 5 | 0 |
| A-LANG-006-010 | Name resolution | 5 | 0 |
| A-LANG-011-016 | Type mismatch | 6 | 0 |
| A-LANG-017-023 | Refinement violation | 7 | 1 |
| A-LANG-024-028 | Linearity | 5 | 0 |
| A-LANG-029-033 | Typestate | 5 | 0 |
| A-LANG-034-038 | Effect violation | 5 | 0 |
| A-LANG-039-043 | Information flow | 5 | 1 |
| A-LANG-044-047 | Totality | 4 | 0-2 |
| A-LANG-048 | Pattern exhaustiveness | 1 | 0 |
| A-LANG-049-052 | Business invariant | 4 | 1-2 |
| A-LANG-053-055 | Concurrency | 3 | 0 |
| A-LANG-056-059 | Numerical precision | 4 | 0-1 |
| A-LANG-060 | Temporal ordering | 1 | 0 |
| A-LANG-061 | Idempotency | 1 | 0 |
| A-LANG-062-064 | Privacy | 3 | 0-1 |
| A-LANG-065-067 | Schema evolution | 3 | 0 |
| A-LANG-068 | Crash safety | 1 | 1 |
| A-LANG-069 | Audit trail | 1 | 0 |
| A-LANG-070 | Serialization | 1 | 2 |
| A-LANG-071-073 | API evolution | 3 | 0 |
| A-LANG-074-076 | Complexity bounds | 3 | 2 |
| A-LANG-077 | Protocol violation | 1 | 1-2 |
| A-LANG-078 | Observability | 1 | 0 |
| A-LANG-079 | Regulatory compliance | 1 | 1-2 |
| A-LANG-080 | i18n completeness | 1 | 0 |
| A-LANG-081 | Module / import | 1 | 0 |
Category Errors
| Range | Category | Count | Layer |
|---|---|---|---|
| A-CORE-xxx | Ghost code, lemmas, frames, axioms, triggers, opaque, prophecy, liveness | 39 | 0-3 |
| A-MEM-xxx | Regions, fixed-width, allocators, circular buffers | 12 | 0-1 |
| A-TYPE-xxx | Interface, structural invariants, error propagation | 14 | 0-1 |
| A-SEC-xxx | Taint, FFI, constant-time, secure erasure, crypto conformance | 20 | 0-2 |
| A-CONC-xxx | Shared memory, callback, determinism, lock ordering, deadlines, weak memory | 26 | 0-2 |
| A-STOR-xxx | Crash recovery, page cache, MVCC, rollback, monotonic, storage model | 25 | 0-2 |
| A-FMT-xxx | Binary format, bit-level, string encoding, codec, checksum, protocol | 27 | 0-1 |
| A-NUM-xxx | Numerical precision, precomputed tables | 10 | 0-2 |
| A-PLAT-xxx | Platform, feature flags, resource limits | 11 | 0-1 |
| A-PERF-xxx | Unsafe escape, complexity bounds | 9 | 1-2 |
| A-TEST-xxx | Test generation, behavioral equivalence, multi-pass | 10 | 0-2 |
| A-MISC-xxx | Incremental/coroutine, invariant suspension | 4 | 0 |
| Total | ~278 |
Error Code Index (quick lookup)
Purpose: Use this table to find the compiler phase and primary crate/files for any error code. Full catalog is in docs/SPECIFICATION.md §7.2 / Appendix D.
How to use
- Note the code prefix (
A01= parser,A02= resolve,A03= types, …). - Open the primary crate/files below (or
rg 'A0xxxx' crates --glob '*.rs'). - Do not fix a types error by changing the SMT backend unless the code is
A04/A11/A05100and the failure is genuinely solver-side. - For unknown codes not listed here:
rg 'A0xxxx' docs/SPECIFICATION.mdthenrg 'A0xxxx' crates.
By series (agent phase map)
| Prefix | Phase | Primary crate | Start here |
|---|---|---|---|
| A01xxx | parser | assura-parser | grammar/, lexer.rs, lower/ |
| A02xxx | resolve | assura-resolve | lib.rs, type_refs.rs, imports.rs |
| A03xxx | types | assura-types | inference.rs, clauses.rs, checks/ |
| A04xxx | smt+types | assura-smt / assura-types | entry/, z3_backend/, refinement paths |
| A05xxx | types | assura-types | checks/linear_typestate.rs, checkers/linear.rs |
| A06xxx | types | assura-types | checks/linear_typestate.rs, checkers/typestate.rs |
| A07xxx | types | assura-types | checks/effects.rs, checkers/effects.rs |
| A08xxx | types | assura-types | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A09xxx | types | assura-types | checks/meta.rs (match), checkers/totality.rs |
| A10xxx | types | assura-types | checks/meta.rs, match exhaustiveness |
| A11xxx | smt+types | assura-smt / assura-types | entry/, invariant checks |
| A12xxx | types | assura-types | checks/concurrency.rs, checkers/security/ |
| A13xxx | types | assura-types | checks/numeric.rs, domain/numeric.rs |
| A31xxx | types | assura-types | checks/core.rs (liveness prove/fairness) |
| A05 (impl) | smt+cli | assura-smt / assura-cli | A05100 SMT inconclusive / limitation |
Codes from SPEC §7.2 (plus a few high-traffic impl codes)
| Code | Phase | Primary crate | Message | Cause (spec) | SPEC subsection | Start in tree |
|---|---|---|---|---|---|---|
| A01001 | parser | assura-parser | Unexpected token | Parser error | Syntax (A01xxx) | grammar/, lexer.rs, lower/ |
| A01002 | parser | assura-parser | Unterminated string literal | Missing closing quote | Syntax (A01xxx) | grammar/, lexer.rs, lower/ |
| A01003 | parser | assura-parser | Invalid numeric literal | Malformed number | Syntax (A01xxx) | grammar/, lexer.rs, lower/ |
| A01004 | parser | assura-parser | Reserved keyword used as identifier | Naming conflict | Syntax (A01xxx) | grammar/, lexer.rs, lower/ |
| A01005 | parser | assura-parser | Mismatched braces | Unbalanced {} | Syntax (A01xxx) | grammar/, lexer.rs, lower/ |
| A02001 | resolve | assura-resolve | Undefined identifier X | Name not in scope | Name Resolution (A02xxx) | lib.rs, type_refs.rs, imports.rs |
| A02002 | resolve | assura-resolve | Undefined type X | Type not declared | Name Resolution (A02xxx) | lib.rs, type_refs.rs, imports.rs |
| A02003 | resolve | assura-resolve | Duplicate definition of X | Name collision | Name Resolution (A02xxx) | lib.rs, type_refs.rs, imports.rs |
| A02004 | resolve | assura-resolve | Ambiguous import X | Multiple modules export same name | Name Resolution (A02xxx) | lib.rs, type_refs.rs, imports.rs |
| A02005 | resolve | assura-resolve | Circular import | Module A imports B imports A | Name Resolution (A02xxx) | lib.rs, type_refs.rs, imports.rs |
| A03001 | types | assura-types | Expected T1, found T2 / empty tuple / pattern arity | Incompatible types; invalid (,); constructor/tuple pattern field count | Type Mismatch (A03xxx) | inference.rs, clauses.rs, checks/ |
| A03002 | types | assura-types | Type parameter count mismatch | Wrong number of generics | Type Mismatch (A03xxx) | inference.rs, clauses.rs, checks/ |
| A03003 | types | assura-types | Cannot unify T1 with T2 | Failed unification | Type Mismatch (A03xxx) | inference.rs, clauses.rs, checks/ |
| A03004 | types | assura-types | Missing field F in struct | Incomplete construction | Type Mismatch (A03xxx) | inference.rs, clauses.rs, checks/ |
| A03005 | types | assura-types | Unknown field F in type T | Field does not exist | Type Mismatch (A03xxx) | inference.rs, clauses.rs, checks/ |
| A03006 | types | assura-types | Clause not Bool / dependent index mismatch | Non-Bool clause body; or Vec<T,3> vs Vec<T,5> | Type Mismatch (A03xxx) | clauses.rs, checkers/info_flow.rs |
| A04001 | smt+types | assura-smt / assura-types | Precondition may not hold | requires clause violated | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04002 | smt+types | assura-smt / assura-types | Postcondition may not hold | ensures clause violated | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04003 | smt+types | assura-smt / assura-types | Refinement subtype check failed | `{v: T \ | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04004 | smt+types | assura-smt / assura-types | Division by zero possible | Divisor may be 0 | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04005 | smt+types | assura-smt / assura-types | Index out of bounds possible | Index may exceed length | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04006 | smt+types | assura-smt / assura-types | Arithmetic overflow possible | Result may exceed bounds | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A04007 | smt+types | assura-smt / assura-types | Refinement timeout | SMT solver timed out | Refinement Violation (A04xxx) | entry/, z3_backend/, refinement paths |
| A05001 | types | assura-types | Linear variable X used twice | Grade 1, used 2+ times | Linearity (A05xxx) | checks/linear_typestate.rs, checkers/linear.rs |
| A05002 | types | assura-types | Linear variable X not used | Grade 1, never consumed | Linearity (A05xxx) | checks/linear_typestate.rs, checkers/linear.rs |
| A05003 | types | assura-types | Grade mismatch: expected N, used M | Exact count violated | Linearity (A05xxx) | checks/linear_typestate.rs, checkers/linear.rs |
| A05004 | types | assura-types | Cannot copy linear value | Tried to duplicate | Linearity (A05xxx) | checks/linear_typestate.rs, checkers/linear.rs |
| A05005 | types | assura-types | Linear value dropped without consuming | Resource leak | Linearity (A05xxx) | checks/linear_typestate.rs, checkers/linear.rs |
| A06001 | types | assura-types | Invalid transition: S1 -> S2 | Not in state machine | Typestate (A06xxx) | checks/linear_typestate.rs, checkers/typestate.rs |
| A06002 | types | assura-types | Operation requires state S, found S' | Wrong current state | Typestate (A06xxx) | checks/linear_typestate.rs, checkers/typestate.rs |
| A06003 | types | assura-types | Object not in final state at end of scope | Protocol incomplete | Typestate (A06xxx) | checks/linear_typestate.rs, checkers/typestate.rs |
| A06004 | types | assura-types | Ambiguous state after branch | Different states in if/else | Typestate (A06xxx) | checks/linear_typestate.rs, checkers/typestate.rs |
| A06005 | types | assura-types | Missing transition guard | Required predicate missing | Typestate (A06xxx) | checks/linear_typestate.rs, checkers/typestate.rs |
| A07001 | types | assura-types | Undeclared effect E | Effect not in function signature | Effect Violation (A07xxx) | checks/effects.rs, checkers/effects.rs |
| A07002 | types | assura-types | Pure function performs effect E | Side effect in pure context | Effect Violation (A07xxx) | checks/effects.rs, checkers/effects.rs |
| A07003 | types | assura-types | Effect E in must-not list | Explicitly forbidden effect | Effect Violation (A07xxx) | checks/effects.rs, checkers/effects.rs |
| A07004 | types | assura-types | Effect handler missing for E | Unhandled effect | Effect Violation (A07xxx) | checks/effects.rs, checkers/effects.rs |
| A07005 | types | assura-types | Effect hierarchy violation | Sub-effect used but parent not declared | Effect Violation (A07xxx) | checks/effects.rs, checkers/effects.rs |
| A08001 | types | assura-types | Data flow violation: L1 to L2 | High to low flow | Information Flow (A08xxx) | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A08002 | types | assura-types | PII leaked to logs | Restricted data in Public sink | Information Flow (A08xxx) | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A08003 | types | assura-types | Implicit flow via branch | Secret in branch condition | Information Flow (A08xxx) | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A08004 | types | assura-types | Purpose violation | Data used for undeclared purpose | Information Flow (A08xxx) | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A08005 | types | assura-types | Missing declassification | Label downgrade without declassify | Information Flow (A08xxx) | checks/info_flow.rs, checkers/taint.rs, checkers/info_flow.rs |
| A09001 | types | assura-types | Non-exhaustive pattern match | Missing cases | Totality (A09xxx) | checks/meta.rs (match), checkers/totality.rs |
| A09002 | types | assura-types | Recursion may not terminate | No decreasing measure | Totality (A09xxx) | checks/meta.rs (match), checkers/totality.rs |
| A09003 | types | assura-types | Decreasing measure not well-founded | Measure does not decrease | Totality (A09xxx) | checks/meta.rs (match), checkers/totality.rs |
| A09004 | types | assura-types | Partial function called from total context | Missing trust | Totality (A09xxx) | checks/meta.rs (match), checkers/totality.rs |
| A11001 | smt+types | assura-smt / assura-types | Invariant violated | SMT found counterexample | Business Invariant (A11xxx) | entry/, invariant checks |
| A11002 | smt+types | assura-smt / assura-types | Invariant not preserved by operation | Mutation breaks invariant | Business Invariant (A11xxx) | entry/, invariant checks |
| A11003 | smt+types | assura-smt / assura-types | Invariant verification timeout | SMT solver timed out | Business Invariant (A11xxx) | entry/, invariant checks |
| A11004 | smt+types | assura-smt / assura-types | Rule clause violated | Business rule not satisfied | Business Invariant (A11xxx) | entry/, invariant checks |
| A12001 | types | assura-types | Exclusive resource accessed concurrently | Data race possible | Concurrency (A12xxx) | checks/concurrency.rs, checkers/security/ |
| A12002 | types | assura-types | Actor isolation violated | Cross-actor mutable access | Concurrency (A12xxx) | checks/concurrency.rs, checkers/security/ |
| A12003 | types | assura-types | Shared-read resource modified | Write in shared-read context | Concurrency (A12xxx) | checks/concurrency.rs, checkers/security/ |
| A13001 | types | assura-types | Unit mismatch: U1 vs U2 | e.g., USD + EUR | Numerical Precision (A13xxx) | checks/numeric.rs, domain/numeric.rs |
| A13002 | types | assura-types | Dimensionally invalid operation | e.g., Money * Money | Numerical Precision (A13xxx) | checks/numeric.rs, domain/numeric.rs |
| A13003 | types | assura-types | Float used where fixed-point required | Precision loss | Numerical Precision (A13xxx) | checks/numeric.rs, domain/numeric.rs |
| A13004 | types | assura-types | Integer overflow possible | Arithmetic exceeds bounds | Numerical Precision (A13xxx) | checks/numeric.rs, domain/numeric.rs |
| A16001 | ? | ? | Purpose violation | Data used outside declared purposes | Privacy (A16xxx) | rg code in crates |
| A16002 | ? | ? | Retention policy missing | No retention declared for PII | Privacy (A16xxx) | rg code in crates |
| A16003 | ? | ? | Anonymization required | Retention period expired | Privacy (A16xxx) | rg code in crates |
| A17001 | ? | ? | Breaking field removal | Required field removed | Schema Evolution (A17xxx) | rg code in crates |
| A17002 | ? | ? | Missing default for new field | Non-optional field added | Schema Evolution (A17xxx) | rg code in crates |
| A17003 | ? | ? | Type change without migration | Incompatible field type change | Schema Evolution (A17xxx) | rg code in crates |
| A21001 | ? | ? | Breaking response field removal | Client may depend on field | API Evolution (A21xxx) | rg code in crates |
| A21002 | ? | ? | New required request field | Existing clients will fail | API Evolution (A21xxx) | rg code in crates |
| A21003 | ? | ? | Error variant removed | Client handlers break | API Evolution (A21xxx) | rg code in crates |
| A22001 | ? | ? | Exceeds declared complexity | O(n^2) found, O(n) declared | Complexity Bounds (A22xxx) | rg code in crates |
| A22002 | ? | ? | Complexity analysis timeout | AARA solver timed out | Complexity Bounds (A22xxx) | rg code in crates |
| A22003 | ? | ? | Unbounded allocation detected | No allocation bound proved | Complexity Bounds (A22xxx) | rg code in crates |
| A05100 | smt+cli | assura-smt / assura-cli | SMT counterexample found (verification failed) | Fix the contract (real violation) | (impl) | check/report.rs |
| A05101 | cli | assura-cli | SMT solver timed out | Increase --timeout | (impl) | check/report.rs |
| A05102 | cli | assura-cli | Known compiler limitation (warning, exit 0) | No action needed | (impl) | check/report.rs |
| A05103 | cli | assura-cli | Solver inconclusive (error, exit 1) | Simplify the contract | (impl) | check/report.rs |
| A10002 | types | assura-types | Match on unknown scrutinee without wildcard | (implementation; see CLI/SMT Unknown policy) | (impl) | checks/meta.rs (match exhaustiveness) |
High-traffic implementation codes (not always in SPEC §7.2 table above)
Agents often hit these in tests/checkers before finding them in Appendix D. Prefer this table over guessing the phase.
| Code | Phase | Primary crate | Typical meaning | Start in tree |
|---|---|---|---|---|
| A02006 | resolve | assura-resolve | Duplicate import | imports.rs |
| A02007 | resolve | assura-resolve | Unused import | unused.rs |
| A02008 | resolve | assura-resolve | Invalid import path segment | imports.rs |
| A02010 | resolve | assura-resolve | Cannot resolve import (module not found) | imports.rs, lib.rs |
| A03006 | types | assura-types | Clause body not Bool where required | clauses.rs |
| A03007 | types | assura-types | Numeric / refinement constraint failure | checks/numeric.rs, domain/numeric.rs |
| A03010 | types | assura-types | Type / annotation mismatch (impl) | inference.rs, clauses.rs, checks/ |
| A07003 | types | assura-types | Unknown / denied effect | checks/effects.rs (known effect names only) |
| A08102 | types | assura-types | Info-flow / taint violation (impl) | checks/info_flow.rs, checkers/taint.rs |
| A10001 | types | assura-types | Non-exhaustive match | checks/meta.rs |
| A10101 | types | assura-types | Numeric / match interaction (impl) | checks/numeric.rs, checks/meta.rs |
| A11005 | types | assura-types | Invariant / FFI-related type issue | checks/ffi_error.rs, entry/invariant paths |
| A14001 | types | assura-types | Frame / modifies violation | checks/frame_totality.rs |
| A23016 | types | assura-types | Domain / feature checker (impl) | domain/, checks/ |
| A24001 | types | assura-types | Domain / feature checker (impl) | domain/, checks/ |
| A27003 | types | assura-types | Domain / feature checker (impl) | domain/, checks/ |
| A28001 | types | assura-types | Domain / feature checker (impl) | domain/, checks/ |
| A33001 | types | assura-types | Storage / resource checker | checks/storage.rs |
| A37003 | types | assura-types | Storage / resource checker | checks/storage.rs |
| A38001 | types | assura-types | Storage / resource checker | checks/storage.rs |
| A42003 | types | assura-types | Numeric precision / bounds | checks/numeric.rs |
| A43001 | types | assura-types | Numeric precision / bounds | checks/numeric.rs |
| A43002 | types | assura-types | Numeric precision / bounds | checks/numeric.rs |
| A44001 | types | assura-types | Platform / target checker | checks/platform.rs |
| A45001 | types | assura-types | Platform / target checker | checks/platform.rs |
| A47001 | types | assura-types | Safety / CVE pattern checker | checks/safety.rs |
| A48002 | types | assura-types | Meta / match / totality (impl) | checks/meta.rs |
| A49001 | types | assura-types | Meta / match / totality (impl) | checks/meta.rs |
| A49002 | types | assura-types | Meta / match / totality (impl) | checks/meta.rs |
| A50001 | types | assura-types | Meta / feature checker (impl) | checks/meta.rs, domain/ |
| A52001 | types | assura-types | Meta / feature checker (impl) | checks/meta.rs |
| A54001 | types | assura-types | Meta / feature checker (impl) | checks/meta.rs |
| A55001 | types | assura-types | Meta / feature checker (impl) | checks/meta.rs, domain/ |
| A64001 | types | assura-types | FFI / error propagation (impl) | checks/ffi_error.rs |
| A31006 | types | assura-types | Liveness block missing prove | checks/core.rs (run_liveness_checks) |
| A31007 | types | assura-types | leads_to without assume fair | checks/core.rs (run_liveness_checks); colon form splits prove/leads_to clauses |
If a code is still missing: rg 'A0xxxx' crates --glob '*.rs' then add a row here
in the same PR when agents are likely to hit it again.
Agent decision shortcuts
| Symptom | First action |
|---|---|
A01xxx | Parser/grammar/lower; minimal reproduction in tests/fixtures/ |
A02xxx | assura-resolve; symbol table / imports / type_refs |
A03xxx | assura-types inference/clauses; check Type::is_indeterminate() footgun |
A04xxx / counterexample | assura-smt; unconstrained result/outputs; verify_typed |
A05xxx linearity | checks/linear_typestate.rs / checkers/linear.rs |
A06xxx typestate | checkers/typestate.rs |
A07xxx effects | checks/effects.rs; known effect names only (see AGENTS pipeline trap) |
A08xxx taint/flow | checks/info_flow.rs / checkers/taint.rs |
A09xxx / A10xxx match/totality | checks/meta.rs / checkers/totality.rs; parser arm trivia footgun |
A14xxx frame/modifies | checks/frame_totality.rs |
A31xxx liveness | checks/core.rs; parser may split prove: leads_to(...) into two clauses |
A05100 counterexample / A05101 timeout / A05102 limitation / A05103 inconclusive | check/report.rs; limitation (A05102) = warning, else error |
A52xxx / A54xxx / high A-series | domain/meta features: checks/meta.rs, domain/, then rg 'Axxxxx' crates |
| Wrong phase suspicion | bash scripts/guards.sh then re-read AGENTS decision tree |
Maintenance
- Source of truth for meanings:
docs/SPECIFICATION.md§7.2. - When adding a new
Axxxxxin code, add a row here (or in “High-traffic implementation codes”) in the same PR if agents are likely to hit it. - Do not try to generate all of Appendix D unless agents repeatedly miss phase; curated + high-traffic is enough.
- Full phase/wiring rules:
AGENTS.md,crates/assura-types/src/CHECKER-LAYERS.md.
Assura Compiler Internals
This document covers the architecture and internal design of the Assura compiler. It is intended for contributors who want to understand the codebase, add new features, or fix bugs.
Pipeline Overview
The compiler processes .assura source files through a linear pipeline:
Source (.assura)
|
v
Lexer (logos 0.16) crates/assura-parser/src/lexer.rs
| produces tokens via logos derive
v
Parser (rowan 0.16 CST) crates/assura-parser/src/cst.rs
| hand-written recursive descent + Pratt expression parsing
| produces GreenNode (lossless concrete syntax tree)
v
CST -> AST Lowering crates/assura-parser/src/lower.rs
| produces SourceFile (AST)
| uses helpers (spanned, missing_expr, lower_expr_children, etc.)
| to avoid boilerplate (see AGENTS.md "Lowering Helpers")
v
Name Resolution crates/assura-resolve/src/lib.rs
| produces ResolvedFile + SymbolTable
v
Type Checking crates/assura-types/src/lib.rs
| produces TypedFile + Vec<TypeError>
v
SMT Verification (Z3/CVC5) crates/assura-smt/src/lib.rs
| produces Vec<VerificationResult>
v
Code Generation crates/assura-codegen/src/lib.rs
| produces GeneratedProject (Rust source files)
v
rustc (external)
The CLI (assura check) runs the pipeline through SMT verification.
The CLI (assura build) runs the full pipeline including codegen and
optionally invokes cargo check on the generated Rust project.
Crate Map
| Crate | LOC | Tests | Purpose |
|---|---|---|---|
assura-parser | 8,100 | 149 | Lexer (logos 0.16), CST (rowan 0.16), recursive descent parser, Pratt expressions, CST-to-AST lowering |
assura-resolve | 4,300 | 91 | Name resolution, scope analysis, symbol table |
assura-types | 33,800 | 1,081 | Type checking, 50+ domain-specific checkers |
assura-smt | 13,800 | 397 | Z3/CVC5 SMT solver integration, verification |
assura-codegen | 7,200 | 159 | Rust code generation via prettyplease |
assura-diagnostics | 2,000 | 23 | Unified Diagnostic type, error catalog with O(1) lookup |
assura-cli | 6,200 | 74 | CLI binary (check, build, init, fmt, explain, infer, audit, diff, REPL) |
assura-fmt | 1,300 | 56 | Source code formatter |
assura-config | 900 | 34 | assura.toml configuration parsing |
assura-pipeline | 400 | 7 | Orchestrates multi-file compilation pipeline |
assura-macros | 300 | 20 | #[contract] and #[trust] proc macros for Rust interop |
assura-stdlib | 300 | 13 | Standard library type and contract definitions |
assura-lsp | 2,000 | 55 | Language Server Protocol (tower-lsp 0.20) |
assura-mcp | 600 | 24 | Model Context Protocol server (rmcp 1.7) |
assura-server | 800 | 27 | gRPC (tonic 0.14) + HTTP (axum 0.8) API server |
assura-rust-analyzer | 1,600 | 40 | Rust source analysis for assura infer and assura audit |
assura-bench | 2 | - | Criterion benchmarks for all pipeline stages |
| Total | ~85,000 | 2,334 |
Crate Details
assura-parser
Entry point: assura_parser::parse(source: &str) -> SourceFile
Note on layering (Phase 11): The canonical AST types live in assura-ast (the compiler IR crate). assura-parser re-exports them for convenience (assura_parser::ast). Downstream crates (assura-codegen, assura-smt) depend only on assura-ast (plus assura-types/assura-resolve) to avoid parser layering violations. expr_to_string and the Feature registry were moved to assura-ast as part of this.
Also: parse_unwrap(source) (panics on error, for tests) and
parse_cst(source) -> (GreenNode, Vec<ParseError>) (raw CST access).
The parser uses a three-stage architecture:
- Lexing (
lexer.rs):logos0.16 derive macro tokenizes ~200 token types (keywords, operators, literals). - CST construction (
cst.rs): Hand-written recursive descent parser builds a lossless rowanGreenNodetree using an events/markers pattern (Open/Close/Advance). Expressions use Pratt parsing with 8 binding power levels. - Lowering (
lower.rs): Converts the CSTSyntaxNodetree into a typed AST (SourceFile).
Key types:
lexer::Token: All ~200 token types (keywords, operators, literals)syntax_kind::SyntaxKind: Rowan node/token kinds, withFrom<&Token>ast::SourceFile: Top-level AST node containingVec<Spanned<Decl>>ast::Decl: Declaration variants (Contract, Service, TypeDef, EnumDef, Extern, FnDef, Block, Import, Module, Bind, Trait)ast::Clause: Contract clause with kind and bodyast::Expr: Expression AST (22 variants: literals, binary ops, calls, quantifiers, match, let, field access, index, etc.)ast::Literal: Literal values (Int, Float, Str, Bool)
Source files:
lexer.rs: Token enum with#[derive(Logos)], ~200 keyword mappingssyntax_kind.rs:SyntaxKindenum for rowan,AssuraLanguagetrait implcst.rs: Parser engine with events/markers,GreenNodeBuildergrammar/mod.rs: Top-level grammar (source_file, project, module, import)grammar/items.rs: Declaration grammar (contract, type, enum, fn, service, extern, bind, trait)grammar/clauses.rs: Clause grammar (requires, ensures, invariant, effects, etc.)grammar/expressions.rs: Pratt expression parser (8 precedence levels)grammar/params.rs: Parameter lists, return types, type parametersast.rs: All AST node types,Spanned<T>wrapperlower.rs: CST-to-AST loweringdisplay.rs: Human-readable Display impls for AST nodeslib.rs:parse()entry point wiring lex + CST + lower
Important patterns:
- All AST nodes carry
Span = Range<usize>(byte offsets) - The CST is lossless (whitespace, comments preserved for formatting)
Expr::Raw(Vec<String>)only appears in non-expression clause bodies (input, output, effects); expression clauses (requires, ensures, invariant, decreases) always produce structuredExpr
assura-resolve
Entry point: assura_resolve::resolve(source: &SourceFile) -> Result<ResolvedFile, Vec<ResolutionError>>
Builds a symbol table and resolves all name references.
Key types:
SymbolTable: Collection ofSymbolentries with scope hierarchySymbol: Name, kind, span, scope index, and optional type infoScope: Contains symbols and a parent scope indexResolvedFile: OriginalSourceFile+SymbolTable+ resolved importsResolutionError: Error with code (A02xxx), message, and span
Multi-file support: resolve_with_modules() accepts a ModuleMap
for cross-file resolution.
assura-types
Entry point: assura_types::type_check(resolved: &ResolvedFile) -> Result<TypedFile, Vec<TypeError>>
The largest crate (33,800 lines). Runs 50+ checkers organized into phases:
Source files:
lib.rs: Entry point,Typeenum,TypeEnv, core checker wiringcheckers.rs: 20+ analysis pass checkers (linearity, typestate, effects, taint, totality, etc.)checkers/interface.rs: Interface conformance checkingdomain.rs: 34 domain-specific checkers (allocators, crypto, concurrency, formats, storage, etc.)inference.rs: Expression type inferenceclauses.rs: Clause body type checkingtests.rs: Unit tests
Key types:
Type: All type variants (Int, Nat, Float, Bool, String, Generic, Refined, Linear, Typestate, Effect, etc.) withis_indeterminate()forUnknown/ErrorhandlingTypeEnv: Type environment mapping names toTypeTypedFile: Type-checked output withresolved,typed_bindings,pending_decrease_checksTypeError: Error with code, severity, message, primary span, secondary spans
Checker phases (executed in order):
- Expression type inference
- Contract clause checking
- Generic instantiation
- Pattern exhaustiveness
- Linearity (linear type usage tracking, context splitting)
- Typestate (DFA state transition checking)
- Effects (effect set containment, call-graph inference)
- Information flow (security label propagation)
- Totality (termination checking with SMT-backed decrease verification)
- Domain-specific checkers (30+ specialized analyzers)
assura-smt
Entry point: assura_smt::verify(typed: &TypedFile) -> Vec<VerificationResult>
Callers outside this crate should prefer assura_pipeline::verify_typed /
compile_full (CLI, LSP, MCP, tests via assura_test_support::verify_ok).
Z3 integration behind the z3-verify feature flag (enabled by default).
CVC5 fallback via external binary in portfolio mode.
Key types:
VerificationResult: Enum withVerified,Counterexample,Timeout,Unknown,Skipped,ErrorvariantsCounterexampleModel: Structured counterexample with variable assignmentsMeasureDefinition: Termination measure for recursion checkingEncoder: TranslatesExprinto Z3 AST (arithmetic, comparisons, quantifiers, field access, function calls)
Module map (agent edit surface — encode here / verify here / result here):
| Area | Path | Edit here for… |
|---|---|---|
| Public verify API | entry/ (verify.rs, jobs.rs, advanced_passes.rs, helpers.rs, evolution.rs) | verify(), job collection, advanced passes, decrease dispatch |
| CLI check command | assura-cli/src/check/ (run.rs, report.rs, watch.rs, project.rs, check_rust.rs) | assura check / verify reporting / watch / project |
| Results / limitation marker | result.rs | VerificationResult, KNOWN_SMT_LIMITATION_MARKER |
| Managers (prophecy, trigger, weak memory) | advanced.rs | New manager methods (must call from entry/encoder, not tests only) |
| Z3 solve loop | z3_backend/verify.rs | Per-clause solve, timeouts, portfolio |
| Z3 encoding | z3_backend/encoder/ (value, core_impl, methods, unmodelable, bitvector) | Expr → Z3 AST; edit methods for encode_expr/raw/binop, core_impl for ADT/call/field |
| CVC5 | cvc5_backend.rs (+ cvc5_*) | CVC5 parity / shell-out |
| IR / layer 2 | ir_*.rs, layer2.rs | Intermediate IR, quantifier layer |
| SMT-LIB dump | smt_dump.rs | --dump-smt offline scripts |
| Display / stats | display.rs | Contract name collection (DeclVisitor), verify stats |
| Measures / termination | measures.rs | Decrease / measure definitions |
Agent rule: add SMT behavior in the row above, then wire from entry/mod.rs or
z3_backend/encoder. scripts/guards.sh section 7 fails if high-signal
methods exist only in advanced.rs / tests.
Source files (legacy list, still accurate):
lib.rs: Crate root, re-exports, feature-gated backendsz3_backend/: Z3-specific encoding and solving (split modules)cvc5_backend.rs: CVC5 SMT-LIB output and external binary invocationlayer2.rs: Layer 2 quantifier verification (10s timeout)advanced.rs: Advanced verification (prophecy variables, triggers)display.rs: Contract name collection and stats
Verification layers:
- Layer 1 (1s timeout): Quantifier-free (QF_UFLIA, QF_UFLRA)
- Layer 2 (10s timeout): With quantifiers (AUFLIA)
Graceful fallback: When compiled without z3-verify, all verification
functions return VerificationResult::Skipped with a message.
assura-types layering (checks vs checkers vs domain): see
crates/assura-types/src/CHECKER-LAYERS.md and the AGENTS ergonomics map.
assura-codegen
Entry point: assura_codegen::codegen(typed: &TypedFile) -> GeneratedProject
Generates a Cargo project with valid Rust source code.
Key types:
GeneratedProject:Cargo.tomlcontent + list ofGeneratedFileBackendConfig: Target, output directory, feature flagsCodegenBackend: Native or Wasm target
Source files:
lib.rs: Main codegen logic, declaration iteration, Cargo.toml generationblock.rs: Block-level code generation,format_rust()via prettypleasetype_map.rs: Reverse type mapping (Rust -> Assura) forassura infer
What gets generated:
Cargo.tomlwith dependencies (proptest in dev-deps for tests)src/lib.rs(single-contract files) or multi-file layout- Struct/enum definitions from AST
TypeDef,EnumDef - Function stubs with
todo!()bodies debug_assert!fromrequiresclauses- Typestate-encoded services (
PhantomData<State>pattern) - Proptest property-based tests from
ensuresclauses - Checked wrappers for
binddeclarations
assura-diagnostics
Unified Diagnostic type used by all compiler passes:
#![allow(unused)]
fn main() {
pub struct Diagnostic {
pub code: String, // e.g., "A03001"
pub severity: Severity, // Error, Warning, Info
pub message: String,
pub primary: Range<usize>, // primary span
pub secondary: Vec<(Range<usize>, String)>,
pub suggestion: Option<Suggestion>,
}
}
From conversions exist for ResolutionError and TypeError.
The error catalog provides explain(code) with O(1) HashMap lookup for
all ~278 error codes defined in the spec.
assura-cli
The CLI binary (assura) with subcommands:
assura check <file>: Parse, resolve, type-check, verify (exit 1 on errors)assura build <file>: Full pipeline + codegen + optionalcargo checkassura init: Scaffold a new.assuraprojectassura fmt <file>: Format source with consistent styleassura explain <code>: Explain an error codeassura infer <file.rs>: Generate skeleton Assura bind contracts from a Rust source fileassura audit <path>: Scan a Cargo crate, discover public functions, generate skeleton contracts, and verify themassura diff <a> <b>: Compare two.assurafiles structurally- REPL mode: Interactive contract evaluation
Flags: --verbose (-v), --quiet (-q), --watch (-w),
--output <dir>, --no-check, --ast, --tokens, --json
assura-fmt
Source code formatter for .assura files. Produces consistently
styled output with proper indentation, clause alignment, and
whitespace normalization.
assura-config
Parses assura.toml project configuration files. Handles solver
timeouts, codegen backend selection, and project-level settings.
assura-pipeline
Orchestrates multi-file compilation. Discovers .assura files,
resolves cross-module imports, and runs the pipeline on each file
in dependency order.
verify_ir / multi-contract selection (#853):
assura_pipeline::verify_ir(source, ir, config) validates IR against the
first Decl::Contract in the source (historical default). For files
with multiple contracts, use
verify_ir_for_contract(source, ir, config, Some("ContractName")) so
structural validation and IR extras target that contract; SMT results are
filtered to its clauses. Auto-implement historically worked around this by
building single-contract source via build_single_contract_source().
Fixed-width signedness (Z3 and CVC5 native): Parameters and results
registered as U8/U16/… use unsigned BV order; I8/I32/… use signed
order for comparisons (bvslt / BitvectorSlt, …). Modular BV add/sub/mul
is the same for both. CVC5 consults enc_state.bv_signed and walks free
constant symbols in BV terms so derived expressions (e.g. x + 0) inherit
signedness (#858).
assura-macros
Procedural macros for Rust interop:
#[contract]: Generatesdebug_assert!from@requires/@ensuresannotations on Rust functions#[trust]: Marks a function as trusted (skips verification)
assura-stdlib
Standard library definitions. Provides built-in type and contract
definitions (numeric types, collections, Option, Result) that are
implicitly available in all .assura files.
assura-lsp
Language Server Protocol server built with tower-lsp 0.20:
textDocument/diagnostic: Parse/type errors as diagnosticstextDocument/hover: Type info on hovertextDocument/definition: Go to symbol definitiontextDocument/completion: Keyword and type completionstextDocument/documentSymbol: Document outline
assura-mcp
Model Context Protocol server built with rmcp 1.7:
check: Run the compiler pipeline on source textexplain: Look up error code descriptionslist_declarations: List contracts and types in a file
assura-server
gRPC (tonic 0.14) + HTTP (axum 0.8) API server:
CheckRPC: Type-check source and return diagnosticsBuildRPC: Generate Rust code from sourceExplainRPC: Look up error code descriptionsHealthRPC: Server health check- HTTP endpoints mirror gRPC RPCs for REST clients
assura-rust-analyzer
Rust source file analysis for assura infer and assura audit.
Parses Rust files with syn, extracts function signatures, and
converts Rust types to Assura types via the reverse type mapping
in assura-codegen/src/type_map.rs.
How to Add a New Checker to assura-types
-
Choose the right file:
- Core analysis (linearity, effects, typestate):
checkers.rs - Domain-specific (memory, security, etc.):
domain.rs
- Core analysis (linearity, effects, typestate):
-
Define the checker struct:
#![allow(unused)] fn main() { pub struct MyNewChecker { pub errors: Vec<TypeError>, } impl MyNewChecker { pub fn new() -> Self { Self { errors: Vec::new() } } pub fn check(&mut self, file: &ResolvedFile) { for decl in &file.source.decls { // Analyze declarations, emit errors via self.errors.push(...) } } } } -
Define error codes following the spec’s scheme (Appendix D):
#![allow(unused)] fn main() { self.errors.push(TypeError { code: "AXXXXX".to_string(), severity: "error".to_string(), message: "description".to_string(), primary_label: "what went wrong here".to_string(), span: some_span.clone(), secondary: vec![], }); } -
Wire into the pipeline in
lib.rs:#![allow(unused)] fn main() { // In type_check() function, after existing checkers: let mut my_checker = MyNewChecker::new(); my_checker.check(&resolved); errors.extend(my_checker.errors); } -
Add tests in
tests.rs:#![allow(unused)] fn main() { #[test] fn my_checker_detects_violation() { let source = r#" contract Bad { // ... contract that should trigger the error } "#; let errors = type_check_source(source); assert!(errors.iter().any(|e| e.code == "AXXXXX")); } } -
Add MUST REJECT fixtures in
tests/fixtures/must_reject/: Create a.assurafile with// MUST REJECT AXXXXXannotation.
How to Add a New SMT Encoding to assura-smt
-
Add a public verification function:
#![allow(unused)] fn main() { pub fn verify_my_property(/* inputs */) -> VerificationResult { #[cfg(feature = "z3-verify")] { verify_my_property_impl(/* inputs */) } #[cfg(not(feature = "z3-verify"))] { VerificationResult::Skipped("Z3 not available".into()) } } } -
Implement the Z3 encoding:
#![allow(unused)] fn main() { #[cfg(feature = "z3-verify")] fn verify_my_property_impl(/* inputs */) -> VerificationResult { let cfg = z3::Config::new(); let ctx = z3::Context::new(&cfg); let solver = z3::Solver::new(&ctx); // Set timeout let params = z3::Params::new(&ctx); params.set_u32("timeout", 1000); solver.set_params(¶ms); // Encode the property let x = z3::ast::Int::new_const(&ctx, "x"); // ... build Z3 AST ... // Check satisfiability of the negation (to prove validity) solver.assert(&negated_property); match solver.check() { z3::SatResult::Unsat => VerificationResult::Verified, z3::SatResult::Sat => { let model = solver.get_model().unwrap(); let ce = extract_counter_model(&model, &ctx); VerificationResult::Counterexample(ce) } z3::SatResult::Unknown => VerificationResult::Unknown, } } } -
Wire into the verification pipeline (in
verify()orverify_contract()), or call directly from the type checker. -
Add tests that cover: verified (property holds), counterexample (property fails with concrete values), and timeout/unknown cases.
-
Use the existing
Encoderstruct (inassura-smt/src/lib.rs) for translatingExprinto Z3 AST. It handles arithmetic, comparisons, quantifiers, field access, and function calls.
How to Add a New Codegen Pass
-
Identify what to generate (new Rust construct, new file, new dependency in Cargo.toml).
-
Modify
codegen()orcodegen_with_config()inassura-codegen/src/lib.rs. The function iterates over declarations in theTypedFile. -
Generate Rust source as a string, then validate with
syn::parse_file()to ensure syntactic correctness. -
For new files, add entries to the
GeneratedProject.filesvector. -
For new dependencies, modify the
cargo_tomlgeneration section. -
Format the output using
prettyplease::unparse()for consistent Rust formatting. -
Add tests using the
codegen_ok()helper that runs the full pipeline (parse -> resolve -> typecheck -> codegen) and validates the generated Rust is syntactically valid.
Error Code Scheme
| Range | Category | Crate |
|---|---|---|
| A01xxx | Syntax errors | assura-parser |
| A02xxx | Name resolution | assura-resolve |
| A03xxx | Type checking | assura-types |
| A05xxx | Linearity | assura-types (checkers.rs) |
| A06xxx | Typestate | assura-types (checkers.rs) |
| A07xxx | Effects | assura-types (checkers.rs) |
| A08xxx | Information flow | assura-types (checkers.rs) |
| A09xxx | Totality | assura-types (checkers.rs) |
| A10xxx | Pattern exhaustiveness | assura-types (lib.rs) |
| A13xxx | Interface conformance | assura-types (checkers/interface.rs) |
| A22xxx-A55xxx | Domain-specific | assura-types (domain.rs) |
Building and Testing
# Build all crates
cargo build --workspace
# Run all tests (2,334)
cargo test --workspace
# Run clippy (required to pass before commit)
cargo clippy --workspace -- -D warnings
# Format check
cargo fmt --check --all
# Run benchmarks
cargo bench -p assura-bench
# Run the CLI
cargo run --bin assura -- check demos/libwebp-huffman.assura
cargo run --bin assura -- build demos/libwebp-huffman.assura
cargo run --bin assura -- fmt demos/libwebp-huffman.assura --check
# Full pre-commit gate
cargo fmt --all && cargo clippy --workspace -- -D warnings && cargo test --workspace
Key Libraries
| Library | Version | Used For |
|---|---|---|
| logos | 0.16 | Lexer (derive macro) |
| rowan | 0.16 | Lossless concrete syntax tree |
| ariadne | 0.6 | Error display |
| z3 | 0.20 | SMT solver bindings (optional, behind z3-verify feature) |
| prettyplease | 0.2 | Rust source formatting in codegen |
| syn | 2 | Rust AST validation and source analysis |
| tower-lsp | 0.20 | LSP server framework |
| rmcp | 1.7 | MCP server framework |
| tonic | 0.14 | gRPC server |
| axum | 0.8 | HTTP server |
| criterion | 0.8 | Benchmarks |
| notify | 8 | Filesystem watching (–watch) |
Key crate versions: ariadne 0.6 uses Report::build(kind, span) with
a 2-arg API; z3 0.20 uses pre-generated FFI bindings (no bindgen at build
time) and removes lifetime parameters from AST types.
crates.io packaging and release-please
Operational guide for publishing Assura libraries to
crates.io and shipping CLI installers via cargo-dist.
Versioning and GitHub Releases are driven by release-please, not by
manually pushing v* tags.
Do not merge a release-please PR without explicit maintainer approval. Merging it creates the tag, GitHub Release, cargo-dist artifacts, and crates.io publish for the version in that PR.
Current status (CLI co-publish, #838)
| Channel | What | Notes |
|---|---|---|
| crates.io libraries | Core graph ending at assura-pipeline | Embed API for apps |
| crates.io frontends | assura-rust-analyzer, assura-llm, assura-lsp, assura-mcp | Shared by the CLI |
| crates.io CLI | assura binary package | cargo install assura --locked |
| GitHub Releases / cargo-dist | Same assura binary | Prebuilt multi-platform installers ([package.metadata.dist] dist = true) |
Versions share [workspace.package] version. After a release, co-publish
uploads any package whose version is not yet on crates.io (idempotent skips
for already-published members).
What ships on crates.io
Publish order is graph-derived by scripts/publish-crates.sh (all path deps
including dev). Typical order:
assura-ast → assura-config → assura-diagnostics → assura-runtime →
assura-rust-analyzer → assura-parser → assura-macros → assura-llm →
assura-fmt → assura-stdlib → assura-resolve → assura-types →
assura-codegen → assura-smt → assura-pipeline → assura-lsp →
assura-mcp → assura (CLI).
Path dependencies pin version = "…" so packaging resolves from crates.io;
scripts/sync-path-dep-versions.sh keeps pins aligned on release PRs.
What does not ship on crates.io
| Package | Reason |
|---|---|
assura-test-support | Internal monorepo test helpers only (publish = false). |
assura-server / assura-bench / editor tooling | Not co-published; use monorepo or separate install docs. |
How the release pipeline works
push to main
└─ release-please.yml
├─ opens/updates release PR (label: autorelease: pending)
│ └─ sync-path-dep-versions (align path dep pins on that branch)
└─ when release PR is merged (release_created=true):
tag + GitHub Release (App token)
├─ tag-push usually starts release.yml (preferred path)
└─ dispatch-release: only if no healthy tag-push Release exists
plan → build-local/global (cargo-dist) → host (upload assets)
→ publish-crates (scripts/publish-crates.sh) → announce
Important details:
- Auto-merge uses the
assura-auto-approveGitHub App (vars.AUTO_APPROVE_CLIENT_ID+secrets.AUTO_APPROVE_PRIVATE_KEY) so merge pushes are not suppressed. Historical note: Auto-merge withGITHUB_TOKENdoes not start push workflows (includingrelease-please.yml). Prefer a human merge for release-related PRs, use the hourly cron catch-up, orgh workflow run "Release Please". See issue #785. - Release tag + single pipeline (#1380): release-please creates tags with
the App token, which does fire
release.ymlon.push.tags. PureGITHUB_TOKENtags would not.dispatch-releasestill exists as a fallback, but it skipsworkflow_dispatchwhen a healthy tag-push Release already exists (queued / in progress / success). Concurrency is keyed by the release tag (release-${{ inputs.tag || github.ref_name }}) so a race cannot run two full cargo-dist matrices in parallel. Manual re-run:gh workflow run Release -f tag=vX.Y.Z(still works alone). - Auto-approve skips PRs labeled
autorelease: pending. Release PRs must be merged by a human. - Virtual workspace +
release-type: simple: release-please only rewritesworkspace.package.version(viaextra-files). That does not updateCargo.lockthe wayrelease-type: rustdoes for a single root package (e.g. patchloom). Thesync-release-pr-versionsjob runssync-path-dep-versions.shandsync-cargo-lock-workspace-versions.shon the release PR branch so CI with--lockedstays green. Do not omit the lock step when copying this layout to another monorepo. - Use
chore:for CI/docs/refactor. Reservefix:/feat:for user-visible changes so version bumps stay intentional. - Optional: commit
RELEASE_NOTES.mdon main (not on the release-please branch) before merging the release PR to override the GitHub Release body. Remove it after the release lands (see prior issue #813).
Config files:
| Path | Role |
|---|---|
release-please-config.json | release-type: simple (workspace root has no [package]), updates workspace.package.version, CHANGELOG.md, bump-minor-pre-major |
.release-please-manifest.json | Last released version per package (root ".") |
scripts/publish-crates.sh | Fail-closed graph filter + topo publish (pre-check, 429 handling, skip already-uploaded) |
scripts/sync-path-dep-versions.sh | Path-dep version= pins = workspace version |
scripts/sync-cargo-lock-workspace-versions.sh | Align Cargo.lock workspace member versions after a version bump (required for CI --locked) |
scripts/check-publish-plan.sh | Assert publish order matches the 13-crate library stack |
scripts/check-cargo-package.sh | cargo package gate; full verify when version is on crates.io, --list on co-publish version-bump PRs (#814, co-publish skew) |
.github/workflows/release-please.yml | Opens release PR on main push; syncs path-deps + lock on that PR; dispatches Release only if tag-push did not (#1380) |
.github/workflows/release.yml | cargo-dist installers + publish-crates (tag / dispatch); concurrency by tag name |
dist-workspace.toml | cargo-dist targets / installers |
Preflight (before merging a release PR)
git status # clean working tree preferred
bash scripts/check-publish-plan.sh
bash scripts/check-cargo-package.sh # full package+verify if version on crates.io;
# auto --list on pre-publish version-bump PRs
# Optional dry-run of the publish script (no token required):
bash scripts/publish-crates.sh --dry-run
On a release-please PR that bumps to a version not yet on crates.io, full
cargo package cannot resolve path+version deps against the registry
(only the old version exists). The script falls back to --list so CI still
checks tarball membership; co-publish (publish-crates.sh) is the ordered
full verify at release time.
check-cargo-package.sh is the gate that would have caught the v0.1.0
assura-smt failure (monorepo include_str! paths outside the package
tarball). CI runs the same script on every rust-touching change (job
Cargo package (publishable)).
Fast listing only (no verify build):
bash scripts/check-cargo-package.sh --list-only
Fail-closed real publish normally runs only in CI after a release-please merge:
CARGO_REGISTRY_TOKEN=… bash scripts/publish-crates.sh
Cutting a new release (normal path)
- Land normal work on
mainwith conventional commits (feat:,fix:,chore:as appropriate). - release-please updates the open release PR (or opens a new one). An open PR such as “release 0.1.1” after 0.1.0 is expected.
- Review version bump,
CHANGELOG.md, and path-dep pins (sync-path-dep-versionson the release workflow run). - Optionally land curated
RELEASE_NOTES.mdon main, wait for release-please to refresh the PR. The host job applies it to the GitHub Release body;cleanup-release-notesthen opens an auto-merge PR to remove the file so the next release does not reuse stale notes. - Merge the release PR with an explicit human decision (never auto-merge
autorelease: pending). - Watch the Release workflow; verify crates.io + GitHub Release assets.
gh run list --workflow=release.yml --limit 5
gh run watch <RUN_ID>
| Job | Meaning |
|---|---|
release-please (in release-please workflow) | Tag + GitHub Release created; dispatches Release with tag |
plan | cargo-dist plan for that tag |
build-local-artifacts / build-global-artifacts | CLI installers |
host | Upload assets (idempotent if the Release already exists); applies RELEASE_NOTES.md if present |
publish-crates | Libraries via scripts/publish-crates.sh (skips versions already on crates.io) |
announce | Final confirmation |
cleanup-release-notes | PR to delete RELEASE_NOTES.md from main after apply (non-fatal) |
If publish-crates fails mid-graph, fix on main and re-dispatch the same
tag (idempotent for already-uploaded crates). You cannot overwrite a version
on crates.io.
# Re-dispatch Release for an existing tag after a script/CI fix
gh workflow run release.yml -f tag=v0.1.0
# or re-run failed jobs on an existing run:
gh run rerun <RUN_ID> --failed
Post-release verification
crates.io
for c in assura-ast assura-config assura-diagnostics assura-macros \
assura-runtime assura-parser assura-fmt assura-stdlib assura-resolve \
assura-types assura-codegen assura-smt assura-pipeline; do
echo -n "$c: "
curl -fsS "https://crates.io/api/v1/crates/$c" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d['crate']['max_version'])"
done
GitHub Release
gh release view vX.Y.Z
gh release view vX.Y.Z --json assets --jq '.assets[].name'
- Co-published crates (libraries + frontends +
assuraCLI) show the released version on crates.io (bash scripts/publish-crates.sh --plan-only). - Release page has installers for configured targets.
- Notes state experimental status and CLI install path
(
cargo install assura --lockedand/or GitHub Release installers).
IR templates and packaging pitfalls
IR prompt markdown used by include_str! must live under
crates/assura-smt/templates/ir/ (crate-local). Monorepo
templates/ir/ is a pointer README only (#812). scripts/guards.sh
section 13 fails if pattern bodies reappear at the monorepo root.
scripts/check-cargo-package.sh fails if any publishable crate cannot
package/verify.
Manual / break-glass
Only if CI cannot run and a maintainer approves a laptop publish:
git checkout vX.Y.Z
CARGO_REGISTRY_TOKEN=… bash scripts/publish-crates.sh
Prefer re-running GitHub Actions so logs and token handling stay in one place.
Historical: first release (v0.1.0)
The first cut used a temporary release-as: 0.1.0 pin (otherwise historical
commits computed 1.0.0), multiple Release re-dispatches, and script fixes
for graph order and “already exists” pre-checks. That pin is gone. Do not
re-apply release-as unless intentionally forcing a version for a future
cut. Details live in session notes / assura-contrib skill, not in day-to-day
procedure above.
Related skill notes
Canonical patterns live in ci-build-release (release-please same-workflow,
cargo-dist host vs release-please body ownership, RELEASE_NOTES.md override,
cargo package preflight for publishable members) and ci-branch-protection
(never auto-merge autorelease: pending).
Assura Implementation Roadmap
For implementers deciding whether to build Assura, and in what order. Based on the SPECIFICATION.md (195 EBNF productions, 50 verification features, ~278 error codes) and INVESTIGATION.md (tech stack, competitive analysis, stress-tested demo projects).
Scope Summary
Assura is a contract-first language that transpiles to Rust. The compiler
is written in Rust, performs 3-layer verification (structural, decidable
SMT, heavy SMT) using Z3/CVC5, and generates Rust source code that
rustc compiles to native or WASM binaries.
The full specification defines:
- 50 verification features across 12 categories + 8 CORE
- 195 EBNF grammar productions, ~199 keywords
- 6-feature type system (refinement, dependent, linear, typestate, effect rows, information flow)
- 3-layer verification: Layer 0 (<10ms), Layer 1 (<200ms), Layer 2 (<10s), Layer 3 (BMC/k-induction)
- ~278 error codes with structured JSON output
- CLI, AI Agent API (gRPC), LSP server
This roadmap sequences the work from “nothing exists” to “all 50 features verified and tested.”
Current Status
The compiler is functional with all core pipeline stages implemented: parser (195 EBNF productions), name resolution, type checker (50+ domain-specific checkers across 12 categories), SMT verification (Z3 primary, CVC5 fallback, portfolio mode), and Rust code generation. The CLI, LSP server, formatter, and MCP server are operational. Over 4,500 tests pass across 19 crates.
See MASTER-PLAN.md for the detailed task-level status of each phase.
Phase 0: Foundation (Months 1-3)
Goal: A working compiler that parses contracts, builds an AST, performs structural checks, and emits valid Rust source code for trivial contracts. Prove the transpile-to-Rust architecture works.
Month 1: Lexer, Parser, AST
Deliverable: assura check file.assura parses a contract file
and reports syntax errors with error codes.
| Task | Effort | Details |
|---|---|---|
| Lexer | 1 week | ~199 keywords (Section 1.1). Tokenize identifiers, literals, operators, comments. Decision: hand-rolled lexer in Rust vs logos crate. Recommend logos for speed and simplicity; it handles keyword disambiguation well. |
| Parser | 2-3 weeks | Recursive descent for the core EBNF (Sections 1.2-1.11). Start with a subset: SourceFile, ServiceDecl, ContractDecl, TypeDecl, EnumDecl, OperationDecl, QueryDecl, RequiresClause, EnsuresClause, EffectsClause, Predicate, Expr. Skip Layers 8-21 (extended contract layers) initially. |
| AST types | 1 week | Define Rust structs for every AST node. Use Span annotations for source locations on every node. Derive Debug, Clone, PartialEq. |
| Error reporting | 1 week | Implement error codes A01001-A01005 (syntax errors). JSON output format from Section 7.3. Human-readable mode with ariadne or miette crate. |
Key decisions:
- Parser generator vs hand-rolled: Hand-rolled recursive descent.
The grammar has enough context sensitivity (refinement types,
effect rows, where clauses) that parser generators add friction.
Gleam, Rust, and Swift all use hand-rolled parsers. A PEG/parser
combinator (
chumsky,winnow) is a reasonable middle ground. - tree-sitter: Build a tree-sitter grammar in parallel for editor support, but do NOT use it as the compiler’s parser. tree-sitter is error-tolerant (good for editors, bad for a verification compiler that needs exact parses).
Risk: The grammar is large (195 productions). Prioritize the contract language (what humans write) before the IR grammar (what AI generates). The IR grammar (Section 4) can be deferred to Phase 1.
Month 2: Name Resolution, Type Checking (Layer 0)
Deliverable: The compiler rejects contracts with undefined names, type mismatches, and missing fields. Error codes A02001-A03006.
| Task | Effort | Details |
|---|---|---|
| Scope analysis | 1 week | Build a symbol table. Resolve QualifiedName references. Detect duplicate definitions (A02003), circular imports (A02005). Module paths match file paths (Section 8.1). |
| Core type checker | 2 weeks | Check base types (Int, Nat, Float, Bool, String, Bytes, Unit, Never). Check generic types (List<T>, Map<K,V>, Set<T>, Option<T>). Check field access, function calls, pattern matches. Emit A03001-A03005. |
| Pattern exhaustiveness | 1 week | Coverage checker for match expressions over enum variants. Emit A10001 (non-exhaustive). This is a well-known algorithm (Maranget’s approach). |
What NOT to build yet: Refinement types, dependent types, linear types, typestate, effect system, information flow. All of those are Layer 0 features but involve significant complexity. Get basic types working first.
Month 3: Rust Codegen + End-to-End
Deliverable: assura build produces a Cargo project with
generated Rust code that compiles and runs. Contract pre/post
conditions become debug_assert! calls.
| Task | Effort | Details |
|---|---|---|
| Codegen framework | 1 week | Generate Rust source code as strings (or use quote/prettyplease for formatting). Create Cargo.toml with workspace setup per Section 10.3. Output to generated/ directory. |
| Type mapping | 1 week | Implement Section 6.1: Int -> i64, Nat -> u64, List<T> -> Vec<T>, Map<K,V> -> BTreeMap<K,V>, etc. Generate newtype wrappers for refinement types (Section 6.2). |
| Contract codegen | 1 week | requires -> debug_assert! at function entry. ensures -> debug_assert! before return. old() expressions -> save values before body executes (Section 6.7). |
| CLI | 1 week | Implement assura check, assura build, assura init, assura explain (Section 10). Wire up --json / --human output modes. assura build invokes cargo build on the generated project. |
End-of-Phase 0 milestone: This contract compiles and runs:
contract SafeDivision {
input(a: Int, b: Int)
output(result: Int)
requires { b != 0 }
ensures { result * b + (a mod b) == a }
effects { pure }
}
It generates Rust with debug_assert!(b != 0) and
debug_assert!(result * b + (a % b) == a). The contract is
not SMT-verified yet; that comes in Phase 1.
Phase 0 deliverables: A working parser for the core grammar, structural type checker for base types, Rust codegen with contract assertions, and a functional CLI. The parser and codegen can be developed in parallel.
Phase 1: v0.1 Alpha (Months 4-8)
Goal: Minimum viable verification pipeline. Z3 integration, CORE infrastructure features, MEM.1 + SEC.1 (the two features that catch the most CVEs), and the libwebp demo working end-to-end.
Month 4: Layer 0 Completion (Linearity, Typestate, Effects)
Deliverable: The compiler checks linearity, typestate, and effect containment without invoking Z3.
| Task | Effort | Details |
|---|---|---|
| Linear type checker | 2 weeks | Implement context splitting (Section 2.5). Track usage grades: 0 (erased), 1 (linear), n (exact), omega (unlimited). Emit A05001-A05005. The key insight: refinement predicates are ghost (grade 0), not computational. See Test Case 1 in Section 13. |
| Typestate checker | 1 week | Finite state machine DFA per typestate variable (Section 2.6). Track state through branches; reject ambiguous states after diverging branches (A06004). Typestate variables must be linear. |
| Effect checker | 1 week | Set inclusion check. Each function’s body effects must be a subset of its declared effect row (Section 3.5). Implement effect hierarchy (io = union of all IO sub-effects, Section 3.6). Emit A07001-A07005. |
Interaction priority: Implement Linear + Typestate first (typestate requires linearity), then Linear + Effect (resource-scoped effects). See Section 13 implementation priority.
Month 5: Z3 Integration + Layer 1
Deliverable: Refinement type checking via Z3. The compiler proves
or disproves requires/ensures clauses using SMT.
| Task | Effort | Details |
|---|---|---|
| Z3 bindings | 1 week | Use the z3 crate (Rust bindings to libz3). Set up the solver context, declare sorts, define functions. Implement the timeout mechanism (1s default for Layer 1, configurable via assura.toml). |
| Refinement type encoding | 2 weeks | Translate {v: T | P} <: {v: T | Q} into SMT queries per Section 5.2. Encode P => Q as (assert P) (assert (not Q)) (check-sat). UNSAT means the subtyping holds. SAT means counterexample exists. |
| Counterexample extraction | 1 week | When Z3 returns SAT, extract the model (concrete variable values) and format as structured JSON (Section 5.3). This is critical for AI iteration: the counterexample tells the AI exactly what input breaks the contract. |
SMT theories used in Layer 1 (all decidable):
- QF_UFLIA: quantifier-free uninterpreted functions + linear integer arithmetic (refinement types)
- QF_UFLRA: same with real arithmetic (float contracts)
- QF_DT: datatypes (information flow labels, typestate guards)
- QF_LIA: linear integer arithmetic (grade arithmetic)
Key risk: Z3 binding complexity. The z3 crate is well-maintained
but the API is low-level. Expect 1-2 weeks of wrestling with lifetimes
and sort declarations before the first query works.
Month 6: CORE Features (Ghost Code, Lemmas, Frame Conditions)
Deliverable: Ghost variables, lemma functions, and frame conditions work. These are the connective tissue that makes domain-specific features composable.
| Task | Effort | Details |
|---|---|---|
| CORE.1 Ghost code | 1.5 weeks | Ghost variables, functions, and blocks (Section 14.CORE.1). Enforce erasure: ghost code cannot affect runtime. Ghost functions must be pure. Ghost assertions become SMT obligations. Codegen: completely erased (or debug_assert in debug mode). Error codes A54001-A54005. |
| CORE.2 Lemmas | 1.5 weeks | Proof functions that generate no runtime code (Section 14.CORE.2). apply lemma_name(args) adds the lemma’s ensures as an assumption. induction var generates base/inductive cases. Error codes A55001-A55005. |
| CORE.3 Frame conditions | 1 week | modifies clauses declaring what a function changes (Section 14.CORE.3). Everything else is implicitly unchanged. This is critical for modular verification: without frame conditions, the verifier must re-prove all invariants after every call. |
Why these matter: In the stress-testing rounds (INVESTIGATION.md), CORE features appeared in 57% of gap collapses (16 of 28 in Round 6). Ghost code alone simplifies MEM.1, FMT.2, STOR.5, and TEST.3.
Month 7: MEM.1 + SEC.1 (The CVE Killers)
Deliverable: Memory region contracts and taint tracking. These two features together catch 5 of 6 CVSS 9.8 CVEs analyzed in the investigation.
| Task | Effort | Details |
|---|---|---|
| MEM.1 Memory regions | 2 weeks | Buffer bounds contracts. requires offset + len <= buf.capacity. Ghost regions tracking valid index ranges. SMT encoding of region containment (region_a ⊆ region_b). Error codes: buffer overread, overwrite, out-of-bounds access. This is the single most impactful safety feature. |
| SEC.1 Untrusted data taint | 2 weeks | Taint labels on data from external sources (network, file, user input). Taint propagation through operations. Taint must be explicitly validated before use in sensitive positions (array indices, allocation sizes, SQL queries). Information flow lattice (Section 2.7) handles the label hierarchy. |
Why these two first: The CVE prevention matrix (INVESTIGATION.md) shows SEC.1 + MEM.1 as the common denominator in 5 of 6 CVSS 9.8 vulnerabilities across libwebp, zlib, and mbedTLS. Getting these two right is the minimum viable safety story.
Month 8: Error Reporting + Integration Testing
Deliverable: Complete error reporting with all Phase 1 error codes. The libwebp demo contract compiles, verifies, and generates correct Rust.
| Task | Effort | Details |
|---|---|---|
| Error catalog | 1 week | Implement all error codes for Phase 1 features: A01xxx-A08xxx, A10xxx, A11xxx (from Section 7.2). Each error includes: location, secondary locations, contract reference, counterexample (when SMT), suggested fixes with confidence scores. |
assura explain | 0.5 weeks | Implement the explain command for all Phase 1 error codes (Section 10.2). Each explanation includes: description, example code, fix guidance. |
| libwebp demo | 2 weeks | Write ~200-300 lines of Assura contracts for the libwebp Huffman table parsing path (the CVE-2023-4863 attack surface). Verify that MEM.1 + SEC.1 catch the buffer overflow. Generate Rust code. Compile and run. |
| Integration tests | 0.5 weeks | Test suite exercising all 11 type interaction test cases from Section 13 (those applicable to Phase 1 features). Each test case is both a specification and a regression test. |
End-of-Phase 1 milestone: A developer can write this contract and get a verified Rust implementation:
service HuffmanDecoder {
type BitReader {
data: Bytes,
pos: Nat,
ghost remaining: Nat
}
operation decode_table {
input(reader: BitReader, code_lengths: List<Nat>)
output(table: HuffmanTable)
requires { reader.pos < reader.data.len() }
requires { forall cl in code_lengths: cl <= 15 }
ensures { table.entries.len() <= MAX_TABLE_SIZE }
effects { pure }
}
}
Phase 1 deliverables: Z3-backed refinement type verification, linear and effect type checking, ghost code and lemma infrastructure, MEM.1 and SEC.1 domain checkers, the libwebp CVE demo verified end-to-end, and complete error reporting for all Phase 1 features. SMT solver experience (or study of the z3 crate docs and Dafny’s Z3 encoding) is essential for this phase.
Phase 2: v0.2 Beta (Months 9-14)
Goal: Feature completeness for primary use cases. Remaining MEM, SEC, TYPE, CONC, and FMT features. Layer 2 verification. LSP server. Test generation.
Months 9-10: Remaining Type System Features
| Task | Effort | Details |
|---|---|---|
| Information flow checker | 2 weeks | Full security lattice (Public < Internal < Confidential < Restricted). Declassification points tracked and auditable (Section 2.7). Purpose labels for GDPR (Section 2.7). Error codes A08001-A08005. |
| Dependent types (restricted) | 2 weeks | Types depending on Nat, Bool, and finite enums (Section 2.4). Vec<T, n> with index arithmetic. Index erasure at runtime. Error code A03006 (dependent index mismatch). Full value-level dependency deferred to v2. |
| Totality checker | 1 week | Exhaustive pattern matches (already done in Phase 0). Termination checking via decreases measures (Section 2.8). partial escape hatch. Error codes A09001-A09004. |
| Measures | 1 week | Structurally recursive functions lifted into the logic (Section 2.3). len, elems, keys, values, size. Encode as uninterpreted functions in SMT with definitional axioms. |
Months 10-11: Feature Categories (MEM, SEC, TYPE, CONC)
| Feature | Effort | Priority | Rationale |
|---|---|---|---|
| MEM.2 Fixed-width integers | 1 week | High | Overflow detection. Already partially covered by refinement types; this adds first-class checked_add/checked_mul and width-aware arithmetic. |
| MEM.3 Allocator contracts | 1.5 weeks | Medium | Allocation/deallocation pairing, size tracking, arena lifetime. Needed for jemalloc demo. |
| MEM.4 Circular buffer contracts | 1 week | Medium | Wrap-around indexing, logical-to-physical mapping. Found from zlib stress test. |
| SEC.2 FFI boundary contracts | 1.5 weeks | High | extern/bind declarations (Section 1.10). Trust boundaries at Rust interop points. Runtime assertion wrappers in debug mode. |
| SEC.3 Constant-time execution | 1 week | Medium | Reject branches and memory accesses dependent on secret data. Found from WireGuard stress test. |
| SEC.4 Secure erasure | 1 week | Medium | Guarantee sensitive data is zeroed after use. Linear type consumed via zeroize. |
| SEC.5 Cryptographic conformance | 1.5 weeks | Low | Top-level theorem connecting code to math spec. Found from mbedTLS stress test. Deferred if team is small. |
| TYPE.1 Interface contracts | 1 week | High | Trait-like contracts for abstract interfaces. Callback re-entrancy restrictions. |
| TYPE.2 Recursive structural invariants | 1 week | High | Tree balance, list sortedness, graph acyclicity as type-level properties. |
| TYPE.3 Error propagation | 1 week | High | must_propagate on error types. Detect silently swallowed errors. |
| CONC.1 Shared memory protocols | 2 weeks | High | Per-object access modes (exclusive, shared-read, actor-isolated). Detect data races at compile time. |
| CONC.2 Callback re-entrancy | 1 week | Medium | Prevent re-entrant calls through callback chains. |
| CONC.3 Determinism contracts | 1 week | Medium | Guarantee reproducible output. Ban HashMap (use BTreeMap), ban random, enforce ordering. |
| CONC.4 Lock ordering | 1 week | Medium | Static lock hierarchy. Prevent deadlocks by enforcing acquisition order. Found from jemalloc stress test. |
| CONC.5 Temporal deadlines | 1 week | Medium | Bounded response time. must_complete_within(ticks: N). Found from WireGuard stress test. |
Months 12-13: FMT Features + Layer 2
| Feature | Effort | Priority | Rationale |
|---|---|---|---|
| FMT.1 Binary format | 1.5 weeks | High | Byte-aligned format contracts. Offset, length, magic bytes. Critical for parser demos (libwebp, zlib). |
| FMT.2 Bit-level format | 1.5 weeks | High | Sub-byte parsing (Huffman codes, JPEG, H.264). Ghost bit cursor. Found from stb_image stress test. |
| FMT.3 String encoding | 1 week | Medium | UTF-8/UTF-16 safety. Encoding-aware string operations. |
| FMT.4 Codec dispatch | 1 week | Medium | Magic-byte routing to per-format contract sets. Found from stb_image. |
| FMT.5 Checksum integrity | 0.5 weeks | Medium | CRC32, Adler-32, SHA verification. |
| FMT.6 Protocol grammar | 1.5 weeks | High | RFC conformance. State machine for protocol parsing (HTTP, TLS, DNS). |
| Layer 2 verification | 2 weeks | High | Quantified invariants (AUFLIA), functional correctness (AUFLIA + UF), termination, serialization roundtrip. Timeout strategy (Section 12.3): emit warning, generate property-based test, flag for --deep. |
| CORE.4 Axiomatic definitions | 1 week | Medium | Abstract mathematical concepts (hash functions, cryptographic primitives). |
| CORE.5 Quantifier triggers | 1 week | Medium | E-matching hints for the SMT solver. Critical for preventing solver instability on quantified formulas. |
| CORE.6 Opaque functions | 1 week | Medium | Hide implementation from verifier. Reason only about the contract. Needed for scalability. |
Month 14: LSP + Test Generation
| Task | Effort | Details |
|---|---|---|
| LSP server | 3 weeks | Implement Language Server Protocol for the contract language. Completions for keywords, types, effects. Go-to-definition. Hover for type info. Inline diagnostics. Error squiggles. VS Code extension (syntax highlighting via TextMate grammar, LSP client). |
| TEST.1 Test generation | 2 weeks | Generate property-based tests from contracts (Section 14.TEST.1). Each requires/ensures pair produces a test case. Use proptest or quickcheck in generated Rust. Also generate boundary value tests from refinement predicates. |
End-of-Phase 2 milestone: All MEM, SEC, TYPE, CONC, and FMT features work. Layer 2 verification handles quantified invariants. Developers have editor support. The zlib and mbedTLS demos work end-to-end.
Phase 2 deliverables: All MEM, SEC, TYPE, CONC, and FMT feature categories complete; Layer 2 quantified verification operational; LSP server with VS Code extension; property-based test generation; and the zlib and mbedTLS demos verified end-to-end. Layer 2 SMT encoding is significantly harder than Layer 1 and should be treated as its own workstream.
Phase 3: v0.3 (Months 15-20)
Goal: Advanced features, production readiness. Storage features, liveness proofs, weak memory ordering, prophecy variables, AI Agent API, and performance optimization.
Months 15-16: STOR Features
| Feature | Effort | Priority | Rationale |
|---|---|---|---|
| STOR.1 Crash recovery | 2 weeks | High | Write-ahead log contracts, crash point annotations, recovery proof obligations. Critical for database and filesystem demos (SQLite, littlefs). |
| STOR.2 Page cache contracts | 1.5 weeks | Medium | Pin/unpin protocols, eviction invariants, dirty page tracking. SQLite B-tree demo. |
| STOR.3 MVCC / snapshot isolation | 1.5 weeks | Medium | Read snapshots, write set isolation, serializability. |
| STOR.4 Transactional rollback | 1 week | Medium | Compensation actions on failure. Already partially covered by typestate. |
| STOR.5 Monotonic state | 1 week | High | Values that only increase. Epoch counters, sequence numbers, wear counters. |
| STOR.6 Storage failure model | 1 week | Medium | Flash wear, partial writes, bad sector modeling. Found from littlefs stress test. |
Months 17-18: Advanced Verification (CORE.7, CORE.8, CONC.6)
These are the hardest features in the entire specification. They extend Assura beyond what Dafny, F*, or SPARK offer.
| Feature | Effort | Difficulty | Details |
|---|---|---|---|
| CONC.6 Weak memory ordering | 3 weeks | Very hard | Per-thread ghost views (GPS/RSL approach). Model all 5 C++ memory orderings (SeqCst, AcqRel, Acquire, Release, Relaxed). The SMT encoding is complex: each thread has its own view of shared state, and synchronization operations merge views. |
| CORE.7 Prophecy variables | 2 weeks | Hard | Ghost state with deferred resolution. Needed for linearizability proofs of lock-free data structures (Michael-Scott queue, Treiber stack). The variable’s value is determined by a future event but constrained now. SMT encoding uses Skolemization. |
| CORE.8 Liveness contracts | 3 weeks | Hard | eventually, leads_to, eventually_within. Verification via liveness-to-safety reduction (Biere et al.). BMC with lasso detection at Layer 2. K-induction for unbounded proofs at Layer 3. Fairness encoding (compassion, justice). |
Technical note: CONC.6 and CORE.7 are research-adjacent. The techniques exist individually (GPS, RSL, Iris, prophecy variables in Verus) but have not been integrated into a single tool targeting Rust codegen. These features carry higher technical risk than the rest of the roadmap and may require iteration on the encoding strategy.
Months 19-20: NUM, PLAT, PERF, MISC + AI Agent API
| Feature | Effort | Details |
|---|---|---|
| NUM.1 Numerical precision | 1 week | Per-operation precision contracts. Fixed-point types. Unit-aware arithmetic. |
| NUM.2 Precomputed table verification | 1 week | table[i] == f(i) for all valid i. CRC tables, Huffman tables, zigzag tables. |
| PLAT.1 Platform abstraction | 1 week | OS-specific behavior behind trait boundaries. |
| PLAT.2 Feature flags | 1 week | Compile-time configuration. Combinatorial flag interactions. The mbedTLS CVEs showed why this matters. |
| PLAT.3 Resource limits | 1 week | Memory, stack, allocation bounds. |
| PERF.1 Unsafe escape | 1 week | unsafe blocks with proof obligations that the safety invariant is maintained. |
| PERF.2 Complexity bounds | 1.5 weeks | AARA (automatic amortized resource analysis). O(n), O(log n), O(n log n) bounds verified via LP solver. |
| TEST.2 Behavioral equivalence | 1.5 weeks | N-way equivalence testing (C vs Rust, SIMD vs scalar). Differential testing harness generation. |
| TEST.3 Multi-pass refinement | 1 week | Progressive JPEG, iterative solvers. Each pass improves on the previous. |
| MISC.1 Incremental contracts | 1 week | Stateful parsers, streaming decoders. Resume-from-any-point guarantees. |
| MISC.2 Scoped invariant suspension | 1 week | Temporarily break an invariant within a transaction, restore at boundary. |
| AI Agent API | 2 weeks | gRPC service (Section 11.2). Check, Build, Explain, Health RPCs. CheckStream for incremental AI iteration. JSON-over-HTTP fallback. |
End-of-Phase 3 milestone: All 50 features implemented. Layer 3 (BMC/k-induction) operational for liveness proofs. AI agents can submit code via gRPC and get streaming verification results. The full demo portfolio (libwebp, zlib, mbedTLS, FreeRTOS, sudo, PX4) has working contract files.
Phase 3 deliverables: All 50 verification features implemented; Layer 3 (BMC/k-induction) operational for liveness proofs; AI Agent gRPC API; and the full demo portfolio with working contract files. CONC.6, CORE.7, and CORE.8 each require deep formal methods expertise.
Phase 4: v1.0 (Months 21+)
Goal: Production release. All 50 features verified and tested. Comprehensive standard library. CI/CD integrations. Documentation.
Months 21-23: Hardening and Standard Library
| Task | Effort | Details |
|---|---|---|
| Standard library | 3 weeks | Implement Section 9: core types (Pos, NonNeg, Email, Uuid), collection contracts (ListOps, sort, filter), numerical types (Money<C>, FixedDecimal), CRUD patterns, auth contracts. |
| Module system completion | 2 weeks | Contract composition with extends (Section 8.3). Service dependencies (Section 8.4). Contract libraries as publishable packages (Section 8.5). |
| Configuration | 1 week | Full assura.toml support (Section 10.3). Project profiles (Section 1.2): minimal, parser, database, embedded, crypto, tls, systems. |
| IR format | 2 weeks | Implement the Implementation IR (Section 4). Text format parser, binary (MessagePack) serializer. Canonical serialization. IR metadata. This is what AI agents generate; the compiler verifies it against contracts. |
| Performance tuning | 3 weeks | Verification caching (hash contract + implementation, skip re-verification if unchanged). Parallel SMT queries across independent contracts. Incremental compilation (only re-check changed modules). Target: Layer 0+1 in <1s for 10K-line projects. |
Months 24-26: Ecosystem and Documentation
| Task | Effort | Details |
|---|---|---|
| CI/CD integration | 2 weeks | GitHub Action (assura-lang/verify-action). Docker image for CI pipelines. GitLab CI template. |
| Documentation | 3 weeks | Language tutorial (contract writing guide). Compiler internals guide. API reference. Error code reference (auto-generated from error catalog). Migration guides for Dafny/SPARK users. |
| Showcase builds | 4 weeks | Complete the demo portfolio: libwebp (CVE-2023-4863), zlib (CVE-2022-37434), mbedTLS (4 CVSS 9.8 CVEs). Full differential testing. CVE replay demonstrations. |
| Cranelift backend | 3 weeks | Dev-mode fast compilation. Cranelift for assura build (10x faster than rustc). Keep rustc for assura build --release. This is the v2 backend from the INVESTIGATION.md roadmap. |
Phase 4 deliverables: Production-quality standard library, full module system with publishable contract packages, CI/CD integrations (GitHub Action, Docker), comprehensive documentation, verified showcase builds for all target CVEs, and optional Cranelift backend for fast dev-mode compilation.
Critical Path
What blocks what. This is the dependency chain that determines the minimum calendar time.
Parser ─────────────────────────────────────────────────────────────►
│
├─► AST ──► Name Resolution ──► Core Type Checker
│ │
│ ┌───────────────────────┘
│ │
│ ├─► Linearity ──► Typestate ──► Effect Checker
│ │ │ │ │
│ │ │ ┌─────────┘ │
│ │ │ │ │
│ ├─► Z3 Integration ──► Layer 1 ──► Layer 2 ──► Layer 3
│ │ │ │
│ │ ├─► MEM.1 ◄──────┘
│ │ │
│ │ ├─► SEC.1
│ │ │
│ │ └─► CORE.1-3 (ghost, lemmas, frames)
│ │
│ └─► Rust Codegen ──► Cargo Integration
│ │
│ └─► End-to-End Pipeline
│
└─► tree-sitter Grammar ──► LSP Server ──► VS Code Extension
The bottleneck is Z3 integration. Everything before it is standard compiler engineering. Everything after it depends on having a working SMT solver connection. The Z3 encoding strategy (Section 5.2) is the make-or-break technical challenge of the project.
Parallelizable work:
- tree-sitter grammar and LSP (independent of verification engine)
- CLI and build system (independent of type checker)
- Standard library contracts (independent of compiler internals)
- Documentation (ongoing)
- Demo project contracts (can be written before the compiler verifies them)
Risk Areas
1. Z3 Encoding Complexity (High Risk)
What: Translating Assura’s 6-feature type system into SMT queries that Z3 can solve reliably.
Why it’s hard: Each feature alone has a well-known encoding. The composition of all six features generates SMT queries that combine quantifiers, uninterpreted functions, datatypes, and linear arithmetic in ways that can cause solver instability. The “dangerous combinations” in Section 12.2 are real:
- Quantified refinements + recursive measures trigger unbounded MBQI
- Nonlinear integer arithmetic (NIA) is undecidable
- Array theory + quantifiers cause exponential blowup
Mitigation: Start with decidable fragments only (Layer 1: QF_UFLIA, QF_DT). Add quantifiers cautiously in Layer 2 with strict timeouts. Study Dafny’s and Verus’s Z3 encoding strategies; both have solved similar problems. Use CVC5 as a fallback solver.
2. Layer 3 BMC Scalability (High Risk)
What: Bounded model checking for liveness contracts (CORE.8) scales exponentially with state space.
Why it’s hard: BMC unrolls the transition relation K times. For a system with N state variables, each step doubles the formula size. K=1000 (the default) on a system with even modest state space can produce SMT queries that take hours.
Mitigation: Modular verification (verify each component in isolation). Compositional reasoning (verify component A’s liveness assuming component B’s safety). Reduce K for development; increase for pre-release. K-induction can prove properties unboundedly if the invariant is inductive, but finding the right inductive invariant is often the hard part.
3. Type Interaction Soundness (Medium Risk)
What: The 6 type features must compose correctly. Section 13 identifies 15 pairwise interactions and 4 three-way interactions.
Why it’s hard: Each pairwise interaction has subtle corner cases. Test Case 1 (refinement + linear): should a refinement predicate count as a linear use? Test Case 5 (typestate + info flow): how does declassification interact with state transitions? Getting even one interaction wrong produces unsoundness (the compiler accepts code that violates a contract).
Mitigation: Implement the 11 test cases from Section 13 as regression tests. Add each pairwise interaction incrementally, with a test before the code. Fuzz the type checker with randomly generated contracts.
4. Rust Codegen Semantic Gap (Medium Risk)
What: Assura’s memory model may diverge from Rust’s ownership
model, forcing generated code to use Rc/Arc heavily.
Why it’s hard: Assura has linear types (use exactly once) which
map cleanly to Rust ownership. But graded types (use exactly N times)
and shared-read access patterns may require reference counting. If
too much generated code uses Arc<Mutex<T>>, performance suffers.
Mitigation: Design the codegen to prefer ownership transfer
(move semantics) wherever possible. Use Rc only for omega-graded
values that are genuinely shared. Profile early; if Arc overhead
is significant, investigate Cranelift direct codegen (Phase 4) as
an alternative to transpiling through Rust.
5. Solver Performance at Scale (Medium Risk)
What: Verification time for a real project (10K+ lines of contracts) may be unacceptable.
Why it’s hard: Each contract clause generates one or more SMT queries. A 10K-line project might have 500+ contracts, each with 3-5 clauses, generating 2,000+ SMT queries. If each query takes 100ms, total verification takes 3+ minutes.
Mitigation: Verification caching (skip unchanged contracts). Parallel SMT queries. Modular verification (verify each module independently; only re-verify modules that changed or whose dependencies changed). The 3-layer architecture helps: most iteration happens at Layer 0+1 (<200ms), with Layer 2 reserved for pre-commit.
MVP Definition
The absolute minimum that’s useful:
Parser + Layer 0 type checking + MEM.1 + SEC.1 + Rust codegen
This corresponds to Phase 0 plus the first half of Phase 1.
What it can do: Parse contracts, check basic types and linearity, check buffer bounds (MEM.1) and taint propagation (SEC.1) via Z3, generate Rust code with debug assertions.
What it proves: The libwebp CVE-2023-4863 (CVSS 9.8, the single worst image codec CVE of the decade) is mathematically impossible. This is the minimum viable demo.
What it can’t do yet: Ghost code, lemmas, frame conditions, typestate, effects, information flow, dependent types, Layer 2/3, LSP, test generation, AI Agent API.
Why this matters: A tool that catches the most common class of CVEs (tainted input driving buffer overflows) is useful even without the other 48 features. Ship early, iterate.
Technology Decisions
Compiler Implementation Language: Rust
Decided. The compiler is written in Rust. It generates Rust. The INVESTIGATION.md evaluated 8 approaches (Section “Architecture Decision”) and concluded transpile-to-Rust is the best path:
- All effort on novel parts (type system, effect system, verification)
rustcas a second safety net (borrow checker catches codegen bugs)- Cargo ecosystem access for generated code
- Proven model (Gleam transpiles to Erlang, TypeScript to JavaScript)
- Fastest prototype timeline (3-6 months)
Parser Strategy: Hand-Rolled Recursive Descent
The grammar has 195 productions with context-sensitive elements (refinement type syntax, effect rows, where clauses, extended contract layers). Parser generators (LALR, PEG) struggle with:
'requires' ['{'] Predicate ['}'](optional braces)- Refinement types
{v: T | P}(ambiguous with set literals) - Effect rows
<e1, e2 | tail>(pipe as separator vs row variable)
A recursive descent parser handles these naturally with lookahead.
Use logos for lexing (fast, minimal).
tree-sitter: Build separately for editor support. tree-sitter grammars are error-tolerant by design, which is the opposite of what a verification compiler needs. They share the grammar specification but not implementation.
SMT Strategy: Z3 Primary, CVC5 Fallback
- Z3 (primary): 15+ years mature. Best Rust bindings (
z3crate). Handles QF_UFLIA, QF_DT, AUFLIA well. Default solver. - CVC5 (fallback): Competitive on quantified formulas. Better at
some datatype theories. Use when Z3 times out. The
cvc5crate exists but is less mature. - Portfolio mode (future): Run both solvers in parallel, take the first result. Verus does this and reports significant reliability improvements.
Encoding strategy: Study Dafny’s Boogie-to-Z3 encoding (open source, well-documented) and Verus’s direct Z3 encoding (also open source). Both have solved the same class of problems.
Codegen Strategy: String Templates with prettyplease
Generate Rust source code as formatted strings. Use prettyplease
for consistent formatting. Do NOT use syn/quote for generation
(they are designed for proc macros, not full program generation).
The generated code should be human-readable (with comments linking
back to contract clauses) for debugging, but NOT intended for human
editing. The generated/ directory is a build artifact.
Build System: Standard Cargo Workspace
Generated projects are standard Cargo workspaces:
project/
Cargo.toml # workspace
contracts/ # human-written .assura files
generated/ # compiler output (Rust source)
Cargo.toml
src/lib.rs
app/ # hand-written Rust (optional)
Cargo.toml
src/main.rs
This is the interop model from INVESTIGATION.md. The generated/
crate is a dependency of app/. Normal Cargo semantics.
Error Reporting: ariadne for Human, JSON for AI
- Human mode (
--human): Useariadnecrate for rich terminal diagnostics with source snippets, underlines, and suggested fixes. - AI mode (
--json, default): Structured JSON per Section 7.3. Error code, location, counterexample, suggested fixes with confidence scores.
Testing Strategy for the Compiler Itself
- Unit tests: Each parser production, each type checker rule, each
SMT encoding. Use Rust’s
#[test]framework. - Snapshot tests: Parse a
.assurafile, serialize the AST, compare to a golden file. Useinstacrate for snapshot testing. - Integration tests: Each of the 11 type interaction test cases from
Section 13. Each test is a
.assurafile with// MUST COMPILEor// MUST REJECT <error code>annotations. - Fuzzing: Fuzz the parser with
cargo-fuzz. Fuzz the type checker with randomly generated ASTs. Fuzz the Z3 encoding by generating random contracts and checking that verification terminates.
What to Read Before Starting
-
Gleam compiler source (github.com/gleam-lang/gleam): The closest architectural precedent. Rust compiler that transpiles to another language. Study its parser, type checker, and codegen structure.
-
Verus source (github.com/verus-lang/verus): SMT-based Rust verification. Study its Z3 encoding strategy, especially how it handles quantifiers and triggers.
-
Dafny source (github.com/dafny-lang/dafny): The most mature verification language. Study its Boogie IR and Z3 encoding. The OOPSLA papers on Dafny’s verification methodology are essential.
-
Liquid Haskell papers: The foundational work on refinement type checking via SMT. The encoding strategy (subtyping -> SMT validity) is exactly what Assura needs.
-
Koka effect system: Row-polymorphic effects. Study how Koka encodes effect rows and how it handles effect handlers.
-
Section 13 of SPECIFICATION.md: The 11 type interaction test cases. These are the specification for the hardest part of the type checker. Implement them as tests before writing the code.
Known Challenges
Type system composition (Section 13): The 6-feature type system composition is novel. No existing tool combines all six features. Each feature alone is well-understood; the interactions are where complexity lives.
Research-adjacent features: CONC.6 (weak memory ordering), CORE.7 (prophecy variables), and CORE.8 (liveness via BMC) extend Assura beyond what comparable tools offer. They are also what differentiates Assura from Dafny/Verus/SPARK.
SMT scalability: Z3 encoding must work beyond small examples. Mitigations already in place: modular verification, caching, parallel queries, portfolio solver mode (Z3 + CVC5).