Skip to main content

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 SMT subsystem's own numbering, not public compilation phases).

On the choice of solver

Verum's verification layer is backend-agnostic at the language level. The current release ships a portfolio of solver adapters behind a capability router; an in-tree Verum-native SMT solver is on the roadmap and will plug into the same adapter trait. Anywhere a specific external solver is alluded to below, read it as the current implementation — the subsystem's contract with the rest of the compiler is what is load-bearing, not the specific solver.

Architecture

Every box upstream of CACHE lives in verum_smt. The boundary with the rest of the compiler is the SmtBackend adapter trait (solver_adapters.rs + solver_capability.rs); concrete adapters live behind feature flags so the same compilation pipeline can be linked against a different solver set without touching the verifier.

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 through the abstract adapter API — the verifier never embeds adapter-specific syntax in its own code.

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)))))))

This means @logic definitions are first-class in the obligation language — refinement predicates can call them, and the SMT adapter sees the same recursion structure the verifier sees.

Capability routing

capability_router.rs classifies each obligation by the theories it touches (linear / nonlinear arithmetic, bitvector, array, string, finite model, recursive datatypes, quantifier alternation, …). Classification is mechanical: the router walks the SMT-LIB tree, tags each node with its theory contribution, unions the tags up to the obligation's signature, and looks the signature up in a capability table that knows which adapters can prove what.

The router's output is a ranked adapter list per obligation, not a single choice — the executor (below) decides whether to run the top adapter alone, race two adapters, or run a portfolio.

Backend switcher

backend_switcher.rs implements four executor strategies on top of the router's ranking:

  • Manual — a fixed adapter, ignoring the ranking.
  • Auto — top-of-ranking adapter; the default for cheap obligations.
  • Fallback — try the top adapter; on timeout / unknown, fall back through the ranking.
  • Portfolio — race several adapters in parallel.

Portfolio

portfolio_executor.rs:

  1. Spawn every available adapter on the same obligation.
  2. Wait for the first conclusive result, or for the timeout.
  3. Cross-validate:
    • all adapters return unsat → accepted.
    • all return sat (counter-example) → rejected, the user is shown one counter-example.
    • adapters disagree (one unsat, another sat) → disagreement is flagged as a hard error, never silently resolved.
    • timeouts handled per policy (see Configuration below).

Used for @verify(thorough) and @verify(certified). A disagreement is treated as an SMT-layer bug, not a verification verdict — the obligation does not advance.

Caching

Every obligation has an SMT-LIB-canonicalised fingerprint (SHA-256). Proof results are cached per project under target/smt-cache/. Invalidation rules:

  • Cache hit: re-validate the saved result against the current obligation fingerprint; if it matches, the obligation is short-circuited and the result is reused.
  • Adapter upgrade: fingerprints include the adapter version, so an adapter upgrade invalidates partial entries while keeping any obligation whose adapter wasn't bumped.
  • Compiler upgrade: config_hash participates in the fingerprint, so a compiler version bump cleans the cache as a side effect.

Telemetry & routing statistics

Every adapter 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 session's Context (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 and the refinement verifier wire the session's collector automatically.

proof_search.rs implements the tactic primitives:

  • auto — adapter call with default configuration.
  • omega — linear integer fragment (Presburger).
  • ring — ring-axiom rewriting.
  • simp [rules] — simplification rewriting.
  • induction — structural induction.
  • cases — case split.

Tactics compose via the tactics.rs combinator language; users script them through the @verify-attribute and proof-DSL surface documented in Verification → proofs.

Proof extraction

proof_extraction.rs extracts a proof term from an unsat response. Each adapter emits a proof log; the translator normalises adapter-native logs into Verum's proof-term representation, which the trusted kernel can then re-check independently. This is what removes the SMT solver from the trusted computing base.

Cubical tactic

cubical_tactic.rs handles cubical / HoTT obligations that ordinary SMT cannot discharge:

  • Path reduction.
  • HIT coherence.
  • Transport normalisation.
  • Glue / unglue simplification.
  • Category-theoretic rewrites (associativity, identity laws, …).

It decomposes obligations into smaller fragments, dispatches the decidable ones to the SMT layer, and applies the cubical rewriting system for the rest.

Performance

Typical obligation mix, measured on a 50 KLOC Verum project:

TheoryCountAvg time (ms)p95
LIA only2 100835
LIA + bitvector9401460
LIA + string11045180
Nonlinear423201800
Cubical18120400

Overall: roughly 92 % of obligations discharge in under 50 ms.

Configuration knobs

The verifier exposes its tunables through typed configuration structs, each with a Default matching the spec defaults. The fields described below are the language-level knobs — they describe behaviour that a Verum project author can reasonably configure, not adapter-internal solver parameters. Adapter- specific tuning belongs in the adapter's own configuration surface.

RefinementConfig — refinement-type verification

Used by RefinementChecker::check_with_smt and verify_refinement_with_assumptions for refinement-subtyping queries (T{φ1} <: T{φ2} iff φ1 ⇒ φ2).

FieldDefaultEffect
enable_smttrueGate the SMT path; when false, fall back to syntactic-only subsumption.
timeout_ms100Per-query budget.
enable_cachetrueCache verification conditions by SHA-256 fingerprint.
max_cache_size10 000Bound on the cache map size; oldest entries evicted on overflow.

The timeout reaches the SMT layer through the adapter trait's typed timeout setter, not by writing solver-specific parameter keys from the verifier.

QEConfig — quantifier elimination

Used by QuantifierEliminator for invariant synthesis, weakest-precondition computation, and refinement projection.

FieldDefaultEffect
timeout_ms5 000Per-query budget.
use_qe_litetrueFast-path linear-arithmetic QE.
use_qe_sattrueSAT-preprocessed QE for Boolean-heavy formulas.
use_model_projectiontrueModel-based projection for non-linear cases.
use_skolemizationtrueSkolemisation fallback.
simplify_level2Escalating simplification chain (0 skip, 1 simplify, 2 simplify + propagate, 3+ plus context simplification).

InterpolationConfig — Craig interpolation

Used by InterpolationEngine for compositional verification and inductive-invariant synthesis.

FieldDefaultEffect
algorithmMBIMcMillan / Pudlak / Dual / Symmetric / MBI / PingPong / Pogo.
strengthBalancedBias toward stronger (McMillan) or weaker (Pudlak) interpolant.
simplifytrueRun an interpolant simplifier on the result.
timeout_ms5 000Per-query budget.
quantifier_eliminationtrueWhen false, projection skips QE — McMillan's A ⇒ I half stays sound; the I ∧ B ⇒ ⊥ half degrades in precision.
max_projection_vars100Reject MBI projection when the elimination set exceeds this — exponential in the number of vars for some theories.

StaticVerificationConfig — bounds / safety verification

Used by StaticVerifier for compile-time elimination of runtime checks.

FieldDefaultEffect
timeout_ms30 000Global wall-clock for the verifier.
constraint_timeout_ms100Per-constraint budget.
enable_proofstrueRequest proof generation.
enable_unsat_corestrueExtract minimal unsat cores.
minimize_corestrueIterate to find a minimal core.
enable_cachingtrueProof-cache lookups.
max_cache_size10 000Bound on proof-cache entries.
auto_tacticstrueUse the adapter's automatic tactic selection.
memory_limit_mb4096Process-wide memory ceiling.

SubsumptionConfig — refinement subtyping

Used internally by SubsumptionChecker.

FieldDefaultEffect
cache_size10 000LRU bound on the subsumption-result cache.
smt_timeout_ms100Per-query budget; updated dynamically so each RefinementConfig.timeout_ms change takes effect immediately.

BisimulationConfig — coinductive bisimulation

Used by BisimulationChecker for behavioural equivalence.

FieldDefaultEffect
max_depth100Hard cap on recursive-destructor unfolding.
timeout_ms30 000Per-query budget.
generate_counterexamplestrueWhen false, leaves the counterexample slot empty to save formatting work.
infinite_strategyBoundedUnfoldingOne of Coinduction / Up-to-bisimulation / BoundedUnfolding.

SepLogicConfig — separation logic

Used by SepLogicEncoder for heap-shape verification.

FieldDefaultEffect
entailment_timeout_ms5 000Per-entailment budget.
max_unfolding_depth10Bound on recursive-predicate unfolding.
enable_frame_inferencetrueGate infer_frame; when false, returns typed failure so callers that only need entailment validity skip the residual computation.

UnsatCoreConfig — minimal unsat-core extraction

Used by UnsatCoreExtractor.

FieldDefaultEffect
minimizetrueIterate to find a minimal core.
quick_extractionfalseTrade minimality for speed.
max_iterations100Bound on minimisation iteration count.
timeout_ms10 000Per-extraction budget.
proof_basedfalseUse the adapter's proof API instead of assumption-tracking.

ParallelConfig — portfolio + cube-and-conquer

Used by ParallelSolver for multi-strategy / multi-thread solving.

FieldDefaultEffect
num_workerscpus()Thread count.
strategiesdefault_strategies()Per-worker strategy list.
timeout_ms30 000Global timeout.
enable_sharingtrueMaster gate for any cross-worker exchange.
enable_lemma_exchangetruePer-feature gate; effective only when enable_sharing is also true.
race_modetrueFirst conclusive worker terminates the others.
lemma_exchange_interval_ms500How often workers swap learned clauses.
max_lemmas_per_exchange10Bound on payload size per exchange round.
enable_cube_and_conquerfalseSearch-space partitioning.
cubes_per_worker4Partition target.

OptimizerConfig — MaxSAT / Pareto optimisation

Used by SmtOptimizer for soft-constraint optimisation.

FieldDefaultEffect
incrementaltrueGate push / pop scope manipulation. When false, push / pop are no-ops (paired so the stack stays balanced).
max_solutionsusize::MAXCap for Pareto-front enumeration.
timeout_ms30 000Per-query budget.
enable_corestrueExtract unsat cores for soft-constraint debugging.
methodLexicographicOne of Lexicographic / Pareto / Box / WeightedSum.

CacheConfig — verification-result cache

Used by VerificationCache for cross-build SMT-result reuse.

FieldDefaultEffect
max_size2 000LRU entry cap.
max_size_bytes500 MBMemory cap.
ttl30 daysResult expiry.
statistics_driventrueWhen false, cache everything; when true, gate inserts on per-query work-statistics (decisions / conflicts / elapsed) so trivial queries don't pollute the cache.
min_decisions_to_cache1 000Threshold for the statistics-driven gate.
min_conflicts_to_cache100Threshold for the statistics-driven gate.
min_solve_time_ms100Threshold for the statistics-driven gate.

Wiring layers

The same conceptual setting may need to be wired at several layers because callers can opt out independently. The verifier follows this hierarchy when threading a tunable through:

caller intent (CLI flag / verum.toml / @verify attribute)

Session-level options (CompilerOptions)

Phase config (e.g. VerificationPhaseConfig)

Subsystem config (e.g. RefinementConfig)

Adapter-trait setters (typed; never raw solver param keys)

When a knob is documented but a layer in this chain is missing, the field is inert — it appears configurable but the lower layers ignore the change. The verifier's CI pins every configuration field with paired no-fire / fire branches so incomplete wiring is a hard test failure, not a quietly ignored option.

See also