Satifiability Modulo Theories
UNDER CONSTRUCTION.
At the end of the last chapter, we saw first-hand the magnificence of automated satisfiability and validity checking for propositional logic. Most recently we met model-finding algorithms. As usual lately, this chapter starts by presenting an updated and compressed version of our specifications for proposition logic, properties of expressions, and model finding. We'll briefly review this material at the start of class. We'll then turn to our main new topic: satisfiability modulo theories.
Review and Extensions
Higher-order functions in lists
@List.mapList.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β@List.foldrList.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β@List.filterList.filter: {α : Type u_1} → (α → Bool) → List α → List α
Language of Propositional Logic
structure var: Type
var : Type: Type 1
Type := (n: var → Nat
n: Nat: Type
Nat)
inductive unary_op: Type
unary_op : Type: Type 1
Type | not: unary_op
not
inductive binary_op: Type
binary_op : Type: Type 1
Type
| and: binary_op
and
| or: binary_op
or
| imp: binary_op
imp
| iff: binary_op
iff
inductive Expr: Type
Expr : Type: Type 1
Type
| true_exp: Expr
true_exp
| false_exp: Expr
false_exp
| var_exp: var → Expr
var_exp (v: var
v : var: Type
var)
| un_exp: unary_op → Expr → Expr
un_exp (op: unary_op
op : unary_op: Type
unary_op) (e: Expr
e : Expr: Type
Expr)
| bin_exp: binary_op → Expr → Expr → Expr
bin_exp (op: binary_op
op : binary_op: Type
binary_op) (e1: Expr
e1 e2: Expr
e2 : Expr: Type
Expr)
notation "{"v: Lean.TSyntax `term
v"}" => Expr.var_exp: var → Expr
Expr.var_exp v: Lean.TSyntax `term
v
prefix:max "¬" => Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp unary_op.not: unary_op
unary_op.not
infixr:35 " ∧ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.and: binary_op
binary_op.and
infixr:30 " ∨ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.or: binary_op
binary_op.or
infixr:25 " ⇒ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.imp: binary_op
binary_op.imp
infixr:20 " ⇔ " => Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp binary_op.iff: binary_op
binary_op.iff
notation " ⊤ " => Expr.top_exp: Type
Expr.top_exp
notation " ⊥ " => Expr.bot_exp: Type
Expr.bot_exp
def implies: Bool → Bool → Bool
implies : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, false: Bool
false => false: Bool
false
| _, _ => true: Bool
true
def iff: Bool → Bool → Bool
iff : Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool
| true: Bool
true, true: Bool
true => true: Bool
true
| false: Bool
false, false: Bool
false => true: Bool
true
| _, _ => false: Bool
false
def eval_un_op: unary_op → Bool → Bool
eval_un_op : unary_op: Type
unary_op → (Bool: Type
Bool → Bool: Type
Bool)
| unary_op.not: unary_op
unary_op.not => not: Bool → Bool
not
def eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op : binary_op: Type
binary_op → (Bool: Type
Bool → Bool: Type
Bool → Bool: Type
Bool)
| binary_op.and: binary_op
binary_op.and => and: Bool → Bool → Bool
and
| binary_op.or: binary_op
binary_op.or => or: Bool → Bool → Bool
or
| binary_op.imp: binary_op
binary_op.imp => implies: Bool → Bool → Bool
implies
| binary_op.iff: binary_op
binary_op.iff => iff: Bool → Bool → Bool
iff
def Interp: Type
Interp := var: Type
var → Bool: Type
Bool
def eval_expr: Expr → Interp → Bool
eval_expr : Expr: Type
Expr → Interp: Type
Interp → Bool: Type
Bool
| Expr.true_exp: Expr
Expr.true_exp, _ => true: Bool
true
| Expr.false_exp: Expr
Expr.false_exp, _ => false: Bool
false
| (Expr.var_exp: var → Expr
Expr.var_exp v: var
v), i: Interp
i => i: Interp
i v: var
v
| (Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp op: unary_op
op e: Expr
e), i: Interp
i => (eval_un_op: unary_op → Bool → Bool
eval_un_op op: unary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i)
| (Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp op: binary_op
op e1: Expr
e1 e2: Expr
e2), i: Interp
i => (eval_bin_op: binary_op → Bool → Bool → Bool
eval_bin_op op: binary_op
op) (eval_expr: Expr → Interp → Bool
eval_expr e1: Expr
e1 i: Interp
i) (eval_expr: Expr → Interp → Bool
eval_expr e2: Expr
e2 i: Interp
i)
Interpretations and Truth Tables
def reduce_or: List Bool → Bool
reduce_or := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr or: Bool → Bool → Bool
or false: Bool
false
def reduce_and: List Bool → Bool
reduce_and := List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr and: Bool → Bool → Bool
and true: Bool
true
def make_bool_lists: Nat → List (List Bool)
make_bool_lists: Nat: Type
Nat → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| 0: Nat
0 => [[]: List Bool
[]]
| n: Nat
n + 1 => (List.map: {α β : Type} → (α → β) → List α → List β
List.map (fun L: List Bool
L => false: Bool
false::L: List Bool
L) (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n)) ++
(List.map: {α β : Type} → (α → β) → List α → List β
List.map (fun L: List Bool
L => true: Bool
true::L: List Bool
L) (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n))
def override: Interp → var → Bool → Interp
override : Interp: Type
Interp → var: Type
var → Bool: Type
Bool → Interp: Type
Interp
| old_interp: Interp
old_interp, var: _root_.var
var, new_val: Bool
new_val =>
(λ v: _root_.var
v => if (v: _root_.var
v.n: _root_.var → Nat
n == var: _root_.var
var.n: _root_.var → Nat
n)
then new_val: Bool
new_val
else old_interp: Interp
old_interp v: _root_.var
v)
def bool_list_to_interp: List Bool → Interp
bool_list_to_interp : List: Type → Type
List Bool: Type
Bool → Interp: Type
Interp
| l: List Bool
l => bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper l: List Bool
l.length: {α : Type} → List α → Nat
length l: List Bool
l
where bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper : (vars: Nat
vars : Nat: Type
Nat) → (vals: List Bool
vals : List: Type → Type
List Bool: Type
Bool) → Interp: Type
Interp
| _, [] => (λ _: var
_ => false: Bool
false)
| vars: Nat
vars, h: Bool
h::t: List Bool
t =>
let len: Nat
len := (h: Bool
h::t: List Bool
t).length: {α : Type} → List α → Nat
length
override: Interp → var → Bool → Interp
override (bools_to_interp_helper: Nat → List Bool → Interp
bools_to_interp_helper vars: Nat
vars t: List Bool
t) (var.mk: Nat → var
var.mk (vars: Nat
vars - len: Nat
len)) h: Bool
h
def interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool : (num_vars: Nat
num_vars : Nat: Type
Nat) → Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| 0: Nat
0, _ => []: List Bool
[]
| (n': Nat
n' + 1) , i: Interp
i => interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool n': Nat
n' i: Interp
i ++ [(i: Interp
i (var.mk: Nat → var
var.mk n': Nat
n'))]
def interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| vars: Nat
vars, is: List Interp
is => List.map: {α β : Type} → (α → β) → List α → List β
List.map (interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool vars: Nat
vars) is: List Interp
is
def max_variable_index: Expr → Nat
max_variable_index : Expr: Type
Expr → Nat: Type
Nat
| Expr.true_exp: Expr
Expr.true_exp => 0: Nat
0
| Expr.false_exp: Expr
Expr.false_exp => 0: Nat
0
| Expr.var_exp: var → Expr
Expr.var_exp (var.mk: Nat → var
var.mk i: Nat
i) => i: Nat
i
| Expr.un_exp: unary_op → Expr → Expr
Expr.un_exp _ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e
| Expr.bin_exp: binary_op → Expr → Expr → Expr
Expr.bin_exp _ e1: Expr
e1 e2: Expr
e2 => max: {α : Type} → [self : Max α] → α → α → α
max (max_variable_index: Expr → Nat
max_variable_index e1: Expr
e1) (max_variable_index: Expr → Nat
max_variable_index e2: Expr
e2)
def mk_interps_vars: Nat → List Interp
mk_interps_vars : Nat: Type
Nat → List: Type → Type
List Interp: Type
Interp
| n: Nat
n => List.map: {α β : Type} → (α → β) → List α → List β
List.map bool_list_to_interp: List Bool → Interp
bool_list_to_interp (make_bool_lists: Nat → List (List Bool)
make_bool_lists n: Nat
n)
-- main api
def num_vars: Expr → Nat
num_vars : Expr: Type
Expr → Nat: Type
Nat := λ e: Expr
e => max_variable_index: Expr → Nat
max_variable_index e: Expr
e + 1: Nat
1
def mk_interps_expr: Expr → List Interp
mk_interps_expr : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp
| e: Expr
e => mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e)
def truth_table_outputs: Expr → List Bool
truth_table_outputs : Expr: Type
Expr → List: Type → Type
List Bool: Type
Bool
| e: Expr
e => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e (mk_interps_vars: Nat → List Interp
mk_interps_vars (num_vars: Expr → Nat
num_vars e: Expr
e))
where eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps : Expr: Type
Expr → List: Type → Type
List Interp: Type
Interp → List: Type → Type
List Bool: Type
Bool
| _, [] => []: List Bool
[]
| e: Expr
e, h: Interp
h::t: List Interp
t => eval_expr_over_interps: Expr → List Interp → List Bool
eval_expr_over_interps e: Expr
e t: List Interp
t ++ [eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h]
Satisfiability Properties of Expressions
def is_sat: Expr → Bool
is_sat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_or: List Bool → Bool
reduce_or (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_valid: Expr → Bool
is_valid (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := reduce_and: List Bool → Bool
reduce_and (truth_table_outputs: Expr → List Bool
truth_table_outputs e: Expr
e)
def is_unsat: Expr → Bool
is_unsat (e: Expr
e : Expr: Type
Expr) : Bool: Type
Bool := not: Bool → Bool
not (is_sat: Expr → Bool
is_sat e: Expr
e)
Finding Models and Counterexamples
def find_model: Expr → Option Interp
find_model : Expr: Type
Expr → Option: Type → Type
Option Interp: Type
Interp
| e: Expr
e =>
let interps: List Interp
interps := mk_interps_expr: Expr → List Interp
mk_interps_expr e: Expr
e
find_model_helper: List Interp → Expr → Option Interp
find_model_helper interps: List Interp
interps e: Expr
e
where find_model_helper: List Interp → Expr → Option Interp
find_model_helper : List: Type → Type
List Interp: Type
Interp → Expr: Type
Expr → Option: Type → Type
Option Interp: Type
Interp
| [], _ => none: {α : Type} → Option α
none
| h: Interp
h::t: List Interp
t, e: Expr
e => if (eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e h: Interp
h) then some: {α : Type} → α → Option α
some h: Interp
h else find_model_helper: List Interp → Expr → Option Interp
find_model_helper t: List Interp
t e: Expr
e
def find_models: Expr → List Interp
find_models (e: Expr
e : Expr: Type
Expr) :=
List.filter: {α : Type} → (α → Bool) → List α → List α
List.filter -- filter on
(λ i: Interp
i => eval_expr: Expr → Interp → Bool
eval_expr e: Expr
e i: Interp
i) -- i makes e true
(mk_interps_expr: Expr → List Interp
mk_interps_expr e: Expr
e) -- over all interps
def find_models_bool: Expr → List (List Bool)
find_models_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_models: Expr → List Interp
find_models e: Expr
e)
def count_models: Expr → Nat
count_models := List.length: {α : Type} → List α → Nat
List.length ∘ find_models: Expr → List Interp
find_models
def find_counterexamples: Expr → List Interp
find_counterexamples (e: Expr
e : Expr: Type
Expr) := find_models: Expr → List Interp
find_models (¬e: Expr
e)
def find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool : Expr: Type
Expr → List: Type → Type
List (List: Type → Type
List Bool: Type
Bool)
| e: Expr
e => interps_to_list_bool_lists: Nat → List Interp → List (List Bool)
interps_to_list_bool_lists (num_vars: Expr → Nat
num_vars e: Expr
e) (find_counterexamples: Expr → List Interp
find_counterexamples e: Expr
e)
Examples and Practice
defX := {X: Exprvar.mkvar.mk: Nat → var0} def0: NatY := {Y: Exprvar.mkvar.mk: Nat → var1} def1: NatZ := {Z: Exprvar.mkvar.mk: Nat → var2} -- If X being true makes Y true, then does X being false make Y false?2: Nat((X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY))Y: Expr(implies (implies: Bool → Bool → Boolimpliesimplies: Bool → Bool → Boolfalsefalse: Booltrue) (true: Boolimpliesimplies: Bool → Bool → Booltruetrue: Boolfalse)) -- If X implies Y, then does not Y false imply not X?false: Bool((X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Expris_valid ((is_valid: Expr → BoolX ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX))X: Exprfind_counterexamples_bool ((find_counterexamples_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprY ⇒ ¬Y: ExprX)) -- Find all the models of an expression.X: Exprfind_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (¬Y: ExprX ⇒ ¬X: ExprY)) -- Simple model counting.Y: Exprcount_models (count_models: Expr → NatX ∨X: ExprY)Y: Exprcount_models (count_models: Expr → NatX ∧X: ExprY) -- Find all models, return list of functionsY: Exprfind_models (find_models: Expr → List InterpX ∧ ¬X: ExprX) -- expect []X: Expr(find_models (find_models: Expr → List InterpX ∨X: ExprY)).Y: Exprlength -- expect 3length: {α : Type} → List α → Nat(find_models (find_models: Expr → List InterpX ∧X: ExprY)).Y: Exprlength -- expect 1 -- Find all models, returns list of bool listslength: {α : Type} → List α → Natfind_models_bool (find_models_bool: Expr → List (List Bool)X ∧ ¬X: ExprX) -- []X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∨ ¬X: ExprX) -- [[false], [true]X: Exprfind_models_bool (find_models_bool: Expr → List (List Bool)X ∧X: ExprY) -- [[true, true]]Y: Exprfind_models_bool (¬(find_models_bool: Expr → List (List Bool)X ∧X: ExprY) ⇒ ¬Y: ExprX ∨ ¬X: ExprY) -- all four interpsY: Exprfind_models_bool ((find_models_bool: Expr → List (List Bool)X ⇒X: ExprY) ⇒ (Y: ExprY ⇒Y: ExprZ) ⇒ (Z: ExprX ⇒X: ExprZ)) -- all eight interpsZ: Expr
Satisfiability Modulo Theories
So far we have specified software that solves (finds models of) propositions in pure propositional logic. But this logic is esoecially austere. Variables can have only Boolean values, and the operators are all Boolean.
What makes propositional logic much more useful is to allow atomic expressions (variable expressions) to be expanded into expressions in other formal languages. For example, expanding the variables in X and Y in the expression X ∧ Y into arithmetic expressions, we could write the following proposition: X > 0 ∧ Y = 2 * X, with X and Y ranging over the natural numbers.
In this logic, interpretations are extended to associate values of types other than Boolean with variables. Model finding then involves finding values of such variables, e.g., integer-valued variables, that make an expression true. Here a model (solution) would be { X = 1, Y = 2 }.
A Game: You're the Finder. What's the smallest integer value for X and a corresponding integer value for Y that make this proposition true: (X > Y + 2) ∧ (Y ≤ 7)?
-- Your answer here:
The Z3 SMT Solver via a Python API
So let's crank up Z3! Open lecture_15.py. Read and and run it using the run icon at the top of the Python editing panel. You should be able to run it by clicking the Python run icon.
Find Python Z3 documentation here. Then continue to follow instructions for readings and activities given elsewhere to continue with this chapter. Return here when done.
Function Terms as Free Variables
We assume you've now seen how to read, write, and solve propositional logic and arithmetic constraints, and have seen enough examples to know what it feels like to have Z3 find solutions without the need to write problem-specific procedural code.
Encoding Problems in SMT for Automated Model Search
Currently as quoted from Z3 Python Tutorial Python files.