7. Solver
Similar to terms, solvers come in typed and untyped flavors, though they do not differ by much. The main differences are
-
typed
Solver.assertrequires aTerm Bool, instead of aTermfor the untyped version, and -
typed model/value-retrieval as we will see below.
The rest of this section discusses typed solvers.
Solvers are tied to cvc5's notion of term manager, which means they are manager-scoped with a Cvc.Ω instance just like sorts and terms.
open Cvc Typed
variable [Ω]
7.1. Basics
Solver creation and virtually all solver functions run in the Env monad.Marginal noteSome run in stronger monads than Env, as we will see very soon.
Documentation
Creates a new solver.
Some functions such as setLogic are actually defined on Untyped.Solver, but since Typed.Solver is just an alias for the untyped version we can use them on both types.
Documentation
Set logic.
-
logic: The logic to set.
Cvc.Untyped.Solver.setOption [Cvc.Ω] (s : Cvc.Untyped.Solver) (option value : String) : Cvc.Env UnitCvc.Untyped.Solver.setOption [Cvc.Ω] (s : Cvc.Untyped.Solver) (option value : String) : Cvc.Env Unit
Setting options also has a typed version that takes a Solver.Option, an enumeration of most of cvc5's options, and a value of the appropriate type for that option.
Cvc.Untyped.Solver.setOpt [Cvc.Ω] (s : Cvc.Untyped.Solver) (opt : Cvc.Solver.Option) (val : opt.argType) : Cvc.Env UnitCvc.Untyped.Solver.setOpt [Cvc.Ω] (s : Cvc.Untyped.Solver) (opt : Cvc.Solver.Option) (val : opt.argType) : Cvc.Env Unit
Documentation
Sets an option given as data, its value typed: s.setOpt .smt_produceModels true.
#eval Env.runIO do
let solver ← Solver.new
solver.setLogic .qf_lia
solver.setOption "produce-models" "true"
solver.setOpt .smt_produceUnsatCores true
println! "logic: {← solver.getLogic}"
let val ← solver.getOption "produce-models"
println! "produce-models: {val}"
While it is possible to declare function symbols in a solver, it is strictly equivalent to creating a solver-agnostic symbol with Term.mkSymbol/Term.mkSymbolAs. Both simply create a new symbol in the underlying term manager.
#eval Env.runIO do
let b ← Term.mkSymbol "b"
let solver ← Solver.new
let i ← solver.declareConst "i"
let f ← Term.mkSymbolAs (Bool → Int → Int) "f"
let g ← solver.declareFunAs (Int → Int → Int) "g"
let t ← smt! 0 ≤ g (f b i) i
solver.assert t
solver.assert b
println! "assertions:"
for a in ← solver.getAssertions do
-- note that `a : Term Bool`
println! "- {a}"
As shown above Solver.assert adds a Term Bool assertion in the solver.
Documentation
Asserts a formula.
7.2. Check-sat and modal environments
The check-sat function is significantly more complex, to see why let us look at the get-value function first.
Cvc.Typed.Solver.getValue [Cvc.Ω] (s : Cvc.Typed.Solver) {α : Type} [Cvc.Typed.TermToValue α] (term : Cvc.Typed.Term α) : s.EnvSat αCvc.Typed.Solver.getValue [Cvc.Ω] (s : Cvc.Typed.Solver) {α : Type} [Cvc.Typed.TermToValue α] (term : Cvc.Typed.Term α) : s.EnvSat α
Documentation
The value the model gives a term, as the Lean value its index describes.
Since retrieving the value of a term is only legal (directly) after a check-sat that returned sat, cvc.lean strongly-types this fact with a modal environment, i.e. a monad specific to a solver being in sat-mode.
Documentation and 1 equation
Code running where solver last answered sat: the model queries (getValue, …) live here.
Entered only through checkSat's ifSat branch.
Equations
s.EnvSat = Cvc.Untyped.Solver.EnvSat sCvc.lean has three of these, the other two being unknown-modeMarginal noteUsually triggered by a timeout or when in an undecidable theory. and unsat-mode.
Documentation
An unsat core: assertions, and assumptions of the check if any, that are unsatisfiable together.
Needs produce-unsat-cores. Unlike SMT-LIB, named and unnamed assertions are not told apart.
(get-unsat-core)
Documentation and 1 equation
Code running where solver last answered unsat: unsat cores and proofs live here.
Entered only through checkSat's ifUnsat branch.
Equations
s.EnvUnsat = Cvc.Untyped.Solver.EnvUnsat sDocumentation
A subset of the assertions that makes the solver time out, with the result of checking it.
Each internal check is limited by the timeout-core-timeout option. On an unknown result caused by
a timeout, the assertions are a timeout core; on an unsat result, an unsat core; on a sat result,
the assertions are satisfiable and the core is empty. Experimental in cvc5.
(get-timeout-core)
Documentation and 1 equation
Code running where solver last answered unknown: timeout cores live here.
Entered only through checkSat's ifUnknown branch.
Equations
s.EnvUnknown = Cvc.Untyped.Solver.EnvUnknown sTo guarantee modal code is safe to run, cvc.lean does not allow users to directly run it: it must be passed alongside a check-sat query to guarantee that only the appropriate sat/unsat/unknown code runs. Hence the signature of the solver check-sat function:
Cvc.Typed.Solver.checkSat {m : Type → Type} [Cvc.Ω] [Monad m] [MonadLiftT BaseIO m] (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) {α : Type} (ifSat : s.EnvSatT m α := liftM (Cvc.Untyped.Solver.unexpectedSat false)) (ifUnsat : s.EnvUnsatT m α := liftM (Cvc.Untyped.Solver.unexpectedUnsat false)) (ifUnknown : Cvc.Unknown.Explanation → s.EnvUnknownT m α := liftM ∘ fun e => Cvc.Untyped.Solver.unexpectedUnknown e false) : Cvc.EnvT m αCvc.Typed.Solver.checkSat {m : Type → Type} [Cvc.Ω] [Monad m] [MonadLiftT BaseIO m] (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) {α : Type} (ifSat : s.EnvSatT m α := liftM (Cvc.Untyped.Solver.unexpectedSat false)) (ifUnsat : s.EnvUnsatT m α := liftM (Cvc.Untyped.Solver.unexpectedUnsat false)) (ifUnknown : Cvc.Unknown.Explanation → s.EnvUnknownT m α := liftM ∘ fun e => Cvc.Untyped.Solver.unexpectedUnknown e false) : Cvc.EnvT m α
The only argument left to explain is assuming which is an optional array of Term Bool providing additional assumptions for this check-sat, called check-sat-assuming in SMT-LIB 2.7.
#eval Env.runIO do
let solver ← Solver.new
solver.setOpt .smt_produceModels true
solver.setOpt .smt_produceUnsatCores true
let b ← Term.mkSymbol "b"
let i1 ← Term.mkSymbol "i1"
let i2 ← Term.mkSymbol "i2"
let not_b ← smt! ¬ b
smt! i2 = 7 >>= solver.assert
smt! 0 ≤ i1 >>= solver.assert
smt! b ∨ i1 < 0 >>= solver.assert
let core ← solver.checkSat (assuming := #[not_b])
(ifUnsat := solver.getUnsatCore)
println! "unsat core, not necessarily minimal:"
for t in core do println! "- {t}"
-- `Solver.getValue` extracts lean values
let (bVal, i1Val, i2Val) ← solver.checkSat
(ifSat := return (
← solver.getValue b,
-- there's also a `getValue` function on terms
← i1.getValue,
← solver.getValue i2))
println! "model as Lean values:\n\
- b := {bVal}\n- i1 := {i1Val}\n- i2 := {i2Val}"
-- `Solver.getValueTerm` extracts (typed) cvc terms
let (bVal, i1Val, i2Val) ← solver.checkSat
(ifSat := return (
← solver.getValueTerm b,
-- there's also a `getValueTerm` function on terms
← i1.getValueTerm,
← solver.getValueTerm i2))
println! "model as cvc terms:\n\
- b := {bVal}\n- i1 := {i1Val}\n- i2 := {i2Val}"
-- a typed term can always be erased if needed
let vals ← solver.checkSat
(ifSat := do
let bVal ← solver.getValueTerm b
let i1Val ← solver.getValueTerm i1
let i2Val ← solver.getValueTerm i2
return #[ bVal.erase, i1Val.erase, i2Val.erase ])
println! "untyped model: {vals}"
7.3. More check-sat functions
For simple check-sat use cases where the only thing that matters is whether the current assertions are satisfiable or not one can simply use Solver.checkIsSat.
Cvc.Typed.Solver.checkIsSat [Cvc.Ω] (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) : Cvc.Env BoolCvc.Typed.Solver.checkIsSat [Cvc.Ω] (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) : Cvc.Env Bool
Documentation
Checks the satisfiability of the assertions, and of assuming if given, as a raw result.
Oftentimes we want to return a result specific to a sat/unsat result, and nothing otherwise, for instance a model on a sat result and nothing on unsat. In such cases it can be easier to use Solver.checkSat? which lets the caller specify which outcome produces a result and how, the unexpected result producing none by default.Marginal noteOn an unknown outcome, the default throws an error instead of returning none as unknown is considered to never be expected by default.
Cvc.Typed.Solver.checkSat? {m : Type → Type} [Cvc.Ω] [Monad m] [MonadLiftT BaseIO m] {α : Type} (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) (ifSat : s.EnvSatT m (Option α) := pure none) (ifUnsat : s.EnvUnsatT m (Option α) := pure none) (ifUnknown : Cvc.Unknown.Explanation → s.EnvUnknownT m (Option α) := liftM ∘ fun e => Cvc.Untyped.Solver.unexpectedUnknown e false) : Cvc.EnvT m (Option α)Cvc.Typed.Solver.checkSat? {m : Type → Type} [Cvc.Ω] [Monad m] [MonadLiftT BaseIO m] {α : Type} (s : Cvc.Typed.Solver) (assuming : Option (Cvc.Typed.Terms Bool) := none) (ifSat : s.EnvSatT m (Option α) := pure none) (ifUnsat : s.EnvUnsatT m (Option α) := pure none) (ifUnknown : Cvc.Unknown.Explanation → s.EnvUnknownT m (Option α) := liftM ∘ fun e => Cvc.Untyped.Solver.unexpectedUnknown e false) : Cvc.EnvT m (Option α)
Documentation
checkSat, whose sat and unsat branches default to none.
#eval Env.runIO do
let b ← Term.mkSymbol "b"
let i1 ← Term.mkSymbol "i1"
let i2 ← Term.mkSymbol "i2"
let t1 ← smt! b ∧ 0 < i1
let t2 ← smt! i1 % i2 = i1
let solver ← Solver.new
solver.setOpt .smt_produceModels true
if ← solver.checkIsSat then
println! "sat, adding constraint"
let values? ← solver.checkSat? (assuming := #[t2])
(ifSat := return some (← i1.getValue, ← i2.getValue))
if let some (i1Val, i2Val) := values?
then println! " i1 := {i1Val}, i2 := {i2Val}"
else println! " unsat with additional constraints"
else println! "unsat"
7.4. Defining (recursive) function symbols
Unlike declaring a function symbol, defining one is a solver-specific action as it implies a solver-level constraint to tie the symbol and its definition together.
Defining a function symbol in a solver is very similar to building a lambda: the resulting term must be typed from its bound variables.
Cvc.Typed.Solver.defineFun [Cvc.Ω] (s : Cvc.Typed.Solver) {β : Type} [Cvc.ToTyp β] (symbol : String) (bvs : Cvc.Typed.BVars) (body : Cvc.Typed.Term β) : Cvc.Env (Cvc.Typed.Term (Cvc.Typed.BVars.signatureTo β bvs))Cvc.Typed.Solver.defineFun [Cvc.Ω] (s : Cvc.Typed.Solver) {β : Type} [Cvc.ToTyp β] (symbol : String) (bvs : Cvc.Typed.BVars) (body : Cvc.Typed.Term β) : Cvc.Env (Cvc.Typed.Term (Cvc.Typed.BVars.signatureTo β bvs))
Documentation
Defines a function symbol over bound variables bvs defined by body.
#eval Env.runIO do
let ub ← Term.mkSymbolAs Int "upperBound"
let solver ← Solver.new
solver.setOpt .smt_produceModels true
let (i, j) := (← BVar.mk "i", ← BVar.mk "j")
let bVars := BVars.empty.push i |>.push j
let saturatingMaxDef ← smt!
let max ← if j ≤ i then i else j;
if max < ub then max else ub
println! "saturatingMaxDef := {saturatingMaxDef}"
let sMax ← solver.defineFun
"saturatingMax" bVars saturatingMaxDef
smt! sMax 17 21 < ub >>= solver.assert
solver.checkSat (ifSat := do
println! "ub := {← ub.getValue}")
7.4.1. Mutually recursive function symbols
Cvc.lean does not, currently, expose special functions for defining mutually recursive functions in its typed API. The untyped API does, which is what we present below.
open Cvc Untyped
variable [Ω]
#eval Env.runIO do
let int ← Srt.int
let bool ← Srt.bool
let solver ← Solver.new
solver.setOption "produce-models" "true"
solver.setOption "fmf-fun" "true"
let even ← solver.declareFun "even" #[int] bool
let odd ← solver.declareFun "odd" #[int] bool
let n ← BVar.mk int "n"
let m ← BVar.mk int "m"
solver.defineFunsRec #[even, odd] #[#[n], #[m]] #[
← smt! if n ≤ 0 then n = 0 else odd (n - 1),
← smt! if m ≤ 0 then false else even (m - 1)]
let i ← Term.mkSymbol int "i"
smt! 3 ≤ i >>= solver.assert
println! "forcing even"
solver.checkSat (assuming := #[← smt! even i])
(ifSat := do println! " sat, i := {← i.getValue}")
println! "forcing odd"
solver.checkSat (assuming := #[← smt! odd i])
(ifSat := do println! " sat, i := {← i.getValue}")