[ Bounded · Invariant · Logic ]
Write the specification.
Check it.
bil is a statically typed language for writing a specification next to its implementation and checking that they agree. It has guarded commands (if … fi, do … od), refinement, first-order logic, and system models of concurrent state machines. Checks run through weakest preconditions, the built-in bil-smt solver or Z3 (with proof certificate checking), and an explicit-state model checker. It can also emit Verus code.
do x = a, y = b invariant x > 0 && y > 0 && gcd_val(x, y) == gcd_val(a, b) variant x + y : x > y -> x := x - y [] y > x -> y := y - x od
gcd_spec[BOUNDED (6561 samples)] invariant preservation
What the name means
Each word in the name maps to a part of the language.
Proved or sampled, and it says which
Each proof obligation is reported as PROVED (Z3 with a checked proof certificate, or built-in bil-smt) or BOUNDED, with the number of samples tried. bil verify exits 1 when a result is only BOUNDED, unless you pass --allow-bounded.
Conditions that must keep holding
A loop has an invariant and a variant. A system has invariants that must hold in the initial state and after every transition. A violation is reported with the shortest trace that reaches it.
First-order logic and types
Conditions use forall and exists. concept hierarchies (<:) are checked for cycles and used as types, with Hindley–Milner inference. Either ASCII or Unicode works: => or ⇒, forall or ∀.
How a program is processed
A program is converted to three intermediate representations (IRs) in turn. Specification code (spec, requires, ensures, invariant, variant) is removed in the last one, so it is not in the code that runs. bil dump-ir prints each level.
Concepts, axioms, specs, fn, proof, system.
Resolved types, flattened concept inheritance, desugared |> pipelines.
Marks each node as spec, proof or exec. Holds the verification conditions and system transitions.
Example: source and output
The source is from examples/. The output is copied from the CLI, with lines cut where marked.
spec gcd_spec(a: Int, b: Int) -> Int requires a > 0 && b > 0 ensures result > 0 && (a % result == 0) && (b % result == 0) && … fn gcd(a: Int, b: Int) -> Int refines gcd_spec requires a > 0 && b > 0 ensures … = do x = a, y = b invariant x > 0 && y > 0 && gcd_val(x, y) == gcd_val(a, b) variant x + y : x > y -> x := x - y [] y > x -> y := y - x od then x
[PROVED (by z3)] fn `gcd_val` — if..fi Guard Totality [PROVED (by z3)] fn `gcd` — Refinement Weakened Precondition [PROVED (by z3)] fn `gcd` — Refinement Strengthened Postcondition [BOUNDED (81 samples)] fn `gcd` — do..od Invariant Initialization (Pre => I) [BOUNDED (6561 samples)] fn `gcd` — do..od Invariant Preservation Branch #1 (I ∧ G1 => wp(S1, I))
system PetersonMutex { state p0: Phase = Idle state p1: Phase = Idle state turn: Int = 0 invariant MutualExclusion: !(p0 == Critical && p1 == Critical) temporal StarvationFreeP0: (p0 == Trying) ~> (p0 == Critical) transition p0 == Idle -> p0 := Trying, turn := 1 [] p0 == Trying && (p1 == Idle || turn == 0) -> p0 := Critical [] … }
States explored: 10 | Transitions fired: 16 Max depth: 3 | Max nondeterministic branching: 2 [LTL PROVED] temporal `StarvationFreeP0` [LTL PROVED] temporal `StarvationFreeP1` [VERIFIED] All safety invariants, LTL temporal properties, deadlock-freedom (BB), and variant bounds hold.
Commands
One binary. Run any of these from a checkout with cargo run --.
| Command | What it does |
|---|---|
| check | OntoLogic DAG, contradiction linter and Hindley–Milner type check. |
| solve | Z3 / CVC5 / bil-smt ontology consistency and subsumption (--subsumes C P). |
| verify | Dijkstra weakest preconditions, refinement and proof lemmas. Bounded, Z3 (proof-checked) and built-in bil-smt. |
| model-check | Explicit-state search with LTL (~>, eventually), deadlock freedom and timed deadlines. --bmc for symbolic. |
| run · vm · build | Tree-walk interpreter, bytecode VM on ghost-erased LIR, standalone native binary. |
| emit-verus | Verus Rust. A system becomes a step_<i> function per transition plus a next relation. |
| dump-ir | Inspect HIR → VIR → LIR. |
| all · repl | check + verify + model-check + run in one go; interactive REPL. |
What emit-verus proves, and what it does not
For each system, emit-verus writes one function per transition. Verus then checks each function.
Proved
- Every
step_<i>function keeps the system'sinvariants true: if they hold before the step, they hold after it. - All 14
examples/domainssystems emit Verus that verifies. - Verus rejects the 6 seeded safety bugs with “postcondition not satisfied”.
Not covered
- Liveness (“this eventually happens”, written
~>oreventually). Onlymodel-checkchecks these. - Wrong axioms. An
axiomis assumed true and never proved. - That your own Rust code matches the
nextrelation. You prove that yourself.
Of the 11 seeded bugs, Verus is expected to reject 6. The other 5 are listed with reasons in tests/verus_emit.rs: 2 break a liveness property and 3 are wrong-axiom bugs that the model checker does not see. Verus version: 0.2026.10.26.d17fbd0. Numbers inside the example models are illustrative.
Example systems
Models in examples/domains/. The seeded-bug files are copies with one fault added.
These are models, not certified safety cases. Sources for domain details are in CITATIONS.md.