Skip to main content

Solver Tuning

This page is the operator's manual for the verification solver: every configurable knob, what it does, and which combinations to reach for under specific workflow goals.

If you only want to set a per-function timeout, use @verify(thorough) and stop reading. If you need to bound memory in CI, configure the SMT backend's quantifier strategy, or debug a stuck proof, this page is the one to bookmark.


How to read this page

Each subsection covers one configuration struct in the verification stack. For each, you see:

  • Owner — which subsystem owns the struct.
  • Where it's set — manifest section, CLI flag, or @verify attribute.
  • Default — the value you get from Default::default().
  • Effect — what changing the value actually does.
  • Wiring scope — which multiple SMT backends parameter scope (global, Config, or Solver) the field reaches; see scope discipline for why this matters.

Every field listed is load-bearing: an audit run on 2026-04-29 confirmed that toggling each one has an observable effect on the corresponding solver invocation. Fields are not "best-effort hints" — Verum treats every documented knob as a contract.


Three layers of tuning

When you tune the solver, the value flows through three nested layers:

verum.toml [verify.*] ← persistent project policy

CLI flag (--solver, --timeout) ← per-invocation override

@verify(strategy) attribute ← per-function override

The most-specific layer wins. The full precedence order (highest → lowest):

  1. CLI flag (verum verify --timeout 60s)
  2. Active manifest profile ([verify.profiles.<name>] when --verify-profile <name>)
  3. Per-module manifest override ([verify.modules."<path>"])
  4. Top-level [verify] / [verify.solver]
  5. @verify(strategy) attribute on the function
  6. Built-in default

Within layer 4 (manifest), the structs below are the authoritative schema.


RefinementConfig — refinement-type subtyping

Owner: verum_types::refinement::RefinementChecker.

Where set: [verify] / [verify.solver] (top-level timeout knobs); the structured form lives in [verify.solver.refinement] (planned for v0.4).

Use when: every time the type checker proves a refinement subtyping T{φ1} <: T{φ2} — which is everywhere refinements appear, including function args, return types, and let bindings.

FieldDefaultEffect
enable_smttrueWhen false, fall back to syntactic-only subsumption — fast but conservative (rejects valid programs that need SMT).
timeout_ms100Per-query the SMT backend budget. The spec mandates 10–500 ms for refinement checks; values outside that range trigger a warning at type-check time.
enable_cachetrueMemoise verification conditions by their SHA-256 fingerprint. Disabling adds ~30% to total verification time.
max_cache_size10 000LRU bound. Each entry is ~512 B, so 10 k entries ≈ 5 MB.

Recipe — fast IDE feedback:

[verify.solver.refinement]
timeout_ms = 50 # 100 ms is too slow for on-type
enable_cache = true
max_cache_size = 50_000 # bigger cache = more reuse

Recipe — strict CI:

[verify.solver.refinement]
timeout_ms = 5_000 # generous; CI cares about correctness, not latency

QEConfig — quantifier elimination

Owner: verum_smt::quantifier_elim::QuantifierEliminator.

Used by: invariant synthesis, weakest-precondition computation, refinement projection.

FieldDefaultEffect
timeout_ms5 000Per-QE-call the SMT backend budget.
max_iterations10Reserved for iterative QE (planned).
use_qe_litetrueTry the SMT backend's qe-light first — fast for linear arithmetic.
use_qe_sattrueSAT-preprocess Boolean-heavy formulas before QE.
use_model_projectiontrueModel-based projection for non-linear cases.
use_skolemizationtrueSkolem-functions fallback when QE-lite + projection fail.
simplify_level2See chain table below.

simplify_level maps to escalating the SMT backend tactic chains:

LevelChain
0skip (identity — no rewriting)
1simplify
2 (default)simplifypropagate-values
3+simplifypropagate-valuesctx-simplify

Higher levels produce smaller / more readable invariants but spend more time. 0 is useful when you want to feed the QE result directly to a downstream rewriter that has its own simplifier.


InterpolationConfig — Craig interpolation

Owner: verum_smt::interpolation::InterpolationEngine.

Used by: compositional verification (split a hard goal into A ⇒ I, I ∧ B ⇒ ⊥), inductive-invariant synthesis (use interpolant as candidate invariant).

FieldDefaultEffect
algorithmMBIPick: McMillan (proof-based, strongest), Pudlak (dual, weakest), Dual (combines), Symmetric (avoids McMillan/Pudlak bias), MBI (model-based, the SMT backend-native), PingPong / Pogo (specialised).
strengthBalancedBias toward stronger or weaker interpolant.
simplifytrueRun simplify on the result before returning.
timeout_msSome(5 000)Per-interpolation the SMT backend budget; None = unbounded (only for offline runs).
proof_basedfalseReserved for proof-based fallback.
model_basedtrueReserved for MBI fallback hint.
quantifier_eliminationtrueWhen false, MBI's project_onto_shared skips QE and returns the original formula. McMillan correctness (A ⇒ I) is preserved; the I ∧ B ⇒ ⊥ half degrades.
max_projection_vars100Reject MBI projection when the elimination set is larger — exponential in some theories.

Recipe — large heaps:

[verify.solver.interpolation]
algorithm = "MBI"
quantifier_elimination = false # ~30% encoder reduction
max_projection_vars = 50 # bail earlier on blowup

StaticVerificationConfig — bounds / safety

Owner: verum_smt::static_verification::StaticVerifier.

Used by: compile-time elimination of runtime checks (verum build with enable_bounds_elimination = true).

FieldDefaultEffect
timeout_ms30 000Global wall-clock for the entire verifier pass.
constraint_timeout_ms100Per-constraint the SMT backend budget — triggers graceful degradation (constraint stays in runtime CFG instead of being eliminated).
enable_proofstrueRequest the SMT backend proof generation.
enable_unsat_corestrueExtract minimal unsat cores for diagnostics.
minimize_corestrueIterate to find a minimal core (more expensive).
enable_cachingtrueProof-cache lookups across runs.
max_cache_size10 000LRU bound on the proof cache.
enable_parallelfalseReserved — the SMT backend Context is not Send/Sync in 0.19.
num_workerscpus()Reserved.
auto_tacticstrueUse the SMT backend's tactic auto-selection when constructing solvers.
memory_limit_mbSome(4096)Process-wide memory ceiling — see parameter-scope discipline for why this is global, not per-solver.

SmtBackendConfig — the SMT backend-specific tuning

Owner: verum_smt::backend::SmtContextManager.

Where set: [verify.solver.smt-backend].

FieldDefaultEffectWiring scope
enable_proofstrueProof-log generation.Config
minimize_corestruePass to unsat_core extraction.Solver Params
enable_interpolationfalseEnable the SMT backend's MBI tactic when interpolating.tactic dispatch
global_timeout_msSome(30 000)Context-level timeout.Config (set_timeout_msec)
memory_limit_mbSome(8192)Process-wide memory ceiling.Global (memory_max_size)
enable_mbqitrueModel-based quantifier instantiation.Solver Params (per-query)
enable_patternstruePattern-based quantifier instantiation.Solver Params (per-query)
random_seedNoneReproducibility seed.Global (smt.random_seed)
num_workerscpus().max(4)(Forwarded to ParallelConfig.)
auto_tacticstrueAuto-tactic selection.Config (auto_config)

Recipe — reproducible CI:

[verify.solver.smt-backend]
random_seed = 42
auto_tactics = false # remove heuristic non-determinism

SmtBackendConfig — the SMT backend-specific tuning

Owner: verum_smt::backend::SmtBackend.

Where set: [verify.solver.smt-backend].

FieldDefaultthe SMT backend optionEffect
logicALL:logicSMT-LIB logic name.
timeout_msSome(30 000)tlimit-perPer-query timeout.
incrementaltrueincrementalPush/pop support.
produce_modelstrueproduce-modelsSAT result + model.
produce_proofstrueproduce-proofsUNSAT result + proof log.
produce_unsat_corestrueproduce-unsat-coresUNSAT + core.
preprocessingtruepreprocess-only=falseWhen false, the SMT backend stops after preprocessing — useful for instrumentation, never for production.
quantifier_modeAutoquant-modeNone / EMatching / CEGQI / MBQI / Auto.
random_seedNoneseedReproducibility.
verbosity0verbosity0–5; saturates at 5.

Quantifier-mode tuning: the SMT backend's Auto heuristic picks per goal. Pin a mode when you have ground truth about goal shape:

ModeBest for
EMatchingGround-instance proofs (most refinement obligations).
CEGQICounterexample-guided synthesis.
MBQIModel-based; good when patterns are sparse.
NoneQuantifier-free fragments only — fastest.

SubsumptionConfig — refinement subtyping internals

Owner: verum_smt::subsumption::SubsumptionChecker.

FieldDefaultEffect
cache_size10 000LRU bound on result cache.
smt_timeout_ms100Per-query the SMT backend budget. Updated dynamically by RefinementSmtBackend::set_timeout_ms so each RefinementConfig.timeout_ms change takes effect immediately.

This is an internal struct; you don't normally configure it directly — RefinementConfig.timeout_ms propagates here.


BisimulationConfig — coinductive equivalence

Owner: verum_smt::coinductive::BisimulationChecker.

Used by: behavioural equivalence checks for session types, π-calculus processes, lazy data structures.

FieldDefaultEffect
max_depth100Hard cap on recursive-destructor unfolding.
timeout_ms30 000Per-query the SMT backend budget.
generate_counterexamplestrueWhen false, leave the counterexample slot as None to save formatting work — useful when you only need the boolean answer.
infinite_strategyBoundedUnfoldingCoinduction / UpToBisimulation / BoundedUnfolding.

SepLogicConfig — separation logic

Owner: verum_smt::separation_logic::SepLogicEncoder.

Used by: heap-shape verification (linked lists, trees, graph predicates).

FieldDefaultEffect
entailment_timeout_ms5 000Per-entailment the SMT backend budget.
max_unfolding_depth10Bound on recursive-predicate unfolding (ListSeg, TreePred).
enable_frame_inferencetrueGates infer_frame; when false, returns typed failure so callers that only need entailment validity skip the residual computation (~30% encoder reduction on large heaps).
enable_symbolic_executiontrueReserved — feature not yet enabled by default in encoder.
enable_cachingtrueReserved — encoding-cache infrastructure exists but isn't yet read.

UnsatCoreConfig — minimal-core extraction

Owner: verum_smt::unsat_core::UnsatCoreExtractor.

Used by: refinement-type error reporting (which clauses contradict?), proof-search debugging.

FieldDefaultEffect
minimizetrueIterate to find a minimal core.
quick_extractionfalseTrade minimality for speed — return the first unsat-core the SMT backend finds.
max_iterations100Bound on minimization iteration count.
timeout_msSome(10 000)Per-extraction the SMT backend budget — folded into the same Params that sets unsat_core: true.
proof_basedfalseUse the SMT backend's proof API instead of assumption-tracking — slower but more precise.

ParallelConfig — portfolio + cube-and-conquer

Owner: verum_smt::parallel::ParallelSolver.

Used by: @verify(thorough) and @verify(certified) modes — both multiple SMT backends race to discharge the same goal.

FieldDefaultEffect
num_workerscpus()Worker thread count.
strategiesdefault_strategies()Per-worker strategy list (the SMT backend logic, tactics, seed).
timeout_msSome(30 000)Global timeout.
enable_sharingtrueBroader gate — must be true for any cross-worker exchange.
enable_lemma_exchangetruePer-feature gate; effective only when enable_sharing is also true.
race_modetrueFirst-to-finish terminates others.
lemma_exchange_interval_ms500How often workers swap learned clauses.
max_lemmas_per_exchange10Bound on payload size per round.
enable_cube_and_conquerfalseSearch-space partitioning.
cubes_per_worker4Partition target.

Recipe — deterministic verification:

[verify.solver.parallel]
enable_sharing = false # no cross-talk
race_mode = false # all workers finish; stable result
num_workers = 1 # single-thread for full reproducibility

OptimizerConfig — MaxSAT / Pareto

Owner: verum_smt::optimizer::SmtOptimizer.

Used by: optimization-modulo-theories (best-effort refinement, weighted-soft-constraint solving).

FieldDefaultEffect
incrementaltrueGates push / pop scope manipulation. When false, push/pop are no-ops (paired so the stack stays balanced).
max_solutionsSome(usize::MAX)Cap for Pareto-front enumeration.
timeout_msSome(30 000)Per-query the SMT backend budget.
enable_corestrueExtract unsat cores for soft-constraint debugging.
methodLexicographicLexicographic / Pareto / Box / WeightedSum.

CacheConfig — verification-result cache

Owner: verum_smt::verification_cache::VerificationCache.

Used by: cross-build SMT result reuse (target/smt-cache/).

FieldDefaultEffect
max_size2 000Entry cap.
max_size_bytes500 MBMemory cap.
ttl30 daysResult expiry.
statistics_driventrueWhen false, cache everything. When true, gate inserts on the SMT backend stats.
min_decisions_to_cache1 000≥ this many SMT decisions → cache.
min_conflicts_to_cache100≥ this many conflicts → cache.
min_solve_time_ms100≥ this elapsed → cache.
distributed_cacheNoneOpt-in distributed cache via S3/Redis URL.

Statistics-driven caching only fires for callers that route through VerificationCache::insert_with_stats. The default get_or_verify path uses unconditional insert, so the stats thresholds above only apply when callers explicitly provide stats.


Parameter-scope discipline

The SMT backend has three distinct parameter scopes that are not interchangeable — even for the same param key:

ScopeAPIPersistence
Globalsmt-backend::set_global_param(k, v)Process-wide; most-recent call wins.
ConfigConfig::set_param_value(k, v)Applied at context construction.
SolverParams::set_u32 / set_bool + Solver::set_params(&p)Per-solver.

The empirically verified rule per key:

KeyGlobalConfigSolver
memory_max_size❌ silent ignore + help dump❌ mis-routes queries
smt.random_seedpartial
auto_config
proof✅ (set_proof_generation)
timeoutpartial✅ (set_timeout_msec)✅ (set_u32)
unsat_core✅ — must fold into the same Params value as the timeout
quant-mode (the SMT backend)n/an/a✅ via smt_solver_set_option
Don't trust your intuition

A parameter that looks settable at any scope frequently isn't. the SMT backend's silent-ignore behaviour means an incorrect choice produces a "config seems to work" failure mode that only shows up under stress (memory_max_size is the canonical example: setting it on Config lets the build succeed but the SMT backend ignores the limit; setting it on Solver makes the SMT backend mis-route easy queries).

If you're adding a new param to the verifier, empirically verify at each scope before committing. The verifier's audit suite includes regression tests for every parameter that has been wired this way.

Solver::set_params is destructive

A subtle gotcha: Solver::set_params(&params) replaces the entire param set. Two separate calls do not accumulate:

// WRONG — second call erases the first
let mut p1 = Params::new();
p1.set_bool("unsat_core", true);
solver.set_params(&p1);

let mut p2 = Params::new();
p2.set_u32("timeout", 5000);
solver.set_params(&p2); // unsat_core is now back to default

// RIGHT — fold both keys into a single Params value
let mut p = Params::new();
p.set_bool("unsat_core", true);
p.set_u32("timeout", 5000);
solver.set_params(&p);

Every per-solver call site in the verifier follows this pattern. The audit suite has a regression test for it.


Common workflows

Latency-sensitive (IDE / on-type)

[verify.solver.refinement]
timeout_ms = 50

[verify.solver.qe]
timeout_ms = 500
simplify_level = 1 # cheaper

[verify.solver.cache]
max_size = 100_000 # bigger reuse window
ttl = "7d"

CI / production builds

[verify.solver.refinement]
timeout_ms = 5_000

[verify.solver.smt-backend]
random_seed = 42 # reproducible failures
auto_tactics = false
memory_limit_mb = 16_384

[verify.solver.parallel]
enable_sharing = false # determinism
race_mode = false
num_workers = 1

Deep proof debugging

[verify.solver.smt-backend]
verbosity = 3 # the SMT backend trace output

[verify.solver.unsat_core]
minimize = true
proof_based = true # slow but precise
timeout_ms = 60_000

[verify.solver.optimizer]
incremental = false # no scope reuse — clean state per query

Ultra-conservative (research / paper artefacts)

[verify]
default_strategy = "certified"
fail_on_divergence = true

[verify.solver.refinement]
timeout_ms = 30_000

[verify.solver.smt-backend]
enable_proofs = true
random_seed = 42

[verify.solver.parallel]
enable_sharing = false
race_mode = false

[verify.solver.unsat_core]
proof_based = true

Telemetry & inspection

After a build with --smt-stats, inspect what each setting actually accomplished:

verum smt-stats # human-readable
verum smt-stats --json # machine-readable
verum smt-stats --reset

The output includes per-theory routing counts, p50/p95 latency, cache hit rate, and per-strategy breakdown. Use it to validate that a tuning change had the expected effect before committing the manifest.


See also