SMT Integration
verum_smt is the bridge between the type checker and the SMT
subsystem. It runs during Phase 3a (contract verification) and
the refinement / dependent-verifier sub-step of Phase 4
(semantic analysis) — see the verification pipeline
for the subsystem-internal stages (5.1–5.7 below are the solver's
own numbering, not public compilation phases).
Verum's verification layer is backend-agnostic at the language level. The current release bundles Z3 and CVC5 behind the capability router; a Verum-native SMT solver is on the roadmap and will slot into the same interface. Anywhere specific backends are named below, read them as the current implementation — the subsystem's contract with the rest of the compiler is what is load-bearing, not the specific solver.
Architecture
Translation
expr_to_smtlib.rs walks a refinement / contract expression and
emits SMT-LIB:
Verum: x > 0 && x < 100
SMT: (and (> x 0) (< x 100))
Verum: forall i in 0..xs.len(). xs[i] < key
SMT: (forall ((i Int)) (=> (and (>= i 0) (< i (List.len xs))) (< (List.get xs i) key)))
Datatypes, generics, and refinement types are encoded in the solver's native datatype / sort system.
Refinement reflection
User @logic functions become define-fun-rec in SMT-LIB:
(define-fun-rec is_sorted ((xs (List Int))) Bool
(match xs
((nil) true)
((cons x rest) (match rest
((nil) true)
((cons y _) (and (<= x y) (is_sorted rest)))))))
Capability routing
capability_router.rs classifies each obligation by theory usage:
- LIA, bitvector, array → Z3.
- Strings, nonlinear, SyGuS, FMF → CVC5.
- Mixed that both support → Z3 (faster on average).
- Mixed only CVC5 supports → CVC5.
Classification is by AST walk: tag nodes with theories, union, look up in capability table.
Backend switcher
backend_switcher.rs implements four strategies:
- Manual — fixed backend.
- Auto — router-driven per obligation.
- Fallback — try primary, fall back to other on timeout.
- Portfolio — both solvers in parallel.
Portfolio
portfolio_executor.rs:
- Spawn every available SMT backend on the same obligation.
- Wait for both or timeout.
- Cross-validate:
- both unsat → accepted.
- both sat (counter-example) → rejected, user sees one.
- one unsat, one sat → disagreement; flagged.
- timeouts handled per policy.
Used for @verify(thorough) and @verify(certified).
Caching
Every obligation has an SMT-LIB fingerprint (SHA-256). Proof results
are cached per project (target/smt-cache/). Invalidation:
- Cache hit: verify the saved result against the current obligation.
- Solver upgrade: fingerprints include solver version; upgrades invalidate partial.
Telemetry & routing statistics
Every solver check() call records routing choice, outcome (SAT/UNSAT/
unknown), elapsed time, and theory class into a shared
Arc<RoutingStats> on the Session. The CLI exposes this data:
verum build --smt-stats # persist stats to .verum/state/smt-stats.json
verum smt-stats # print human-readable report
verum smt-stats --json # machine-readable JSON
verum smt-stats --reset # clear after printing
The Context object (verum_smt/context.rs) auto-records on every
check() call when a routing-stats collector is installed via
context.with_routing_stats(arc). Both the contract-verification
phase (api.rs) and the refinement verifier (pipeline.rs phase_verify)
wire the session's collector automatically.
Proof search
proof_search.rs (230 K LOC) implements tactics:
auto— call solver with default configuration.omega— linear integer fragment.ring— ring-axiom rewriting.simp [rules]— simplification rewriting.induction— structural induction.cases— case split.
Tactics can compose via the tactics.rs combinator language.
Proof extraction
proof_extraction.rs (135 K LOC) extracts a proof term from an SMT
unsat response. Each bundled backend emits a proof log; the translator
normalises them to Verum's proof-term representation for machine
checking.
Cubical tactic
cubical_tactic.rs (1058 lines) handles cubical / HoTT obligations
that general SMT cannot discharge:
- Path reduction.
- HIT coherence.
- Transport normalisation.
- Glue / unglue simplification.
- Category-theoretic rewrites (associativity, identity laws, etc.).
It decomposes obligations into smaller fragments, dispatches the decidable ones to SMT, and applies rewrites for the rest.
Performance
Typical obligation mix (measured on a 50 KLOC Verum project):
| Theory | Count | Avg time (ms) | p95 |
|---|---|---|---|
| LIA only | 2,100 | 8 | 35 |
| LIA + bv | 940 | 14 | 60 |
| LIA + string | 110 | 45 | 180 |
| Nonlinear | 42 | 320 | 1,800 |
| Cubical | 18 | 120 | 400 |
Overall: ~92% of obligations discharge in under 50 ms.
See also
- Verification → SMT routing — user-facing policy.
- Verification → refinement reflection
— how
@logicfunctions reach the solver. - Verification → proofs — the tactic DSL.