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.map : {α : Type u_1} {β : Type u_2} β) List α List β
@
List.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β
List.map
@List.foldr : {α : Type u_1} {β : Type u_2} β β) β List α β
@
List.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β
List.foldr
@List.filter : {α : Type u_1} Bool) List α List α
@
List.filter: {α : Type u_1} → (α → Bool) → List α → List α
List.filter

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

def 
X: Expr
X
:= {
var.mk: Nat → var
var.mk
0: Nat
0
} def
Y: Expr
Y
:= {
var.mk: Nat → var
var.mk
1: Nat
1
} def
Z: Expr
Z
:= {
var.mk: Nat → var
var.mk
2: Nat
2
} -- If X being true makes Y true, then does X being false make Y false?
Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not X) (Expr.un_exp unary_op.not Y)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
[[false, true]]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))
false
(
implies: Bool → Bool → Bool
implies
(
implies: Bool → Bool → Bool
implies
false: Bool
false
true: Bool
true
) (
implies: Bool → Bool → Bool
implies
true: Bool
true
false: Bool
false
)) -- If X implies Y, then does not Y false imply not X?
Expr.bin_exp binary_op.imp (Expr.bin_exp binary_op.imp X Y) (Expr.bin_exp binary_op.imp (Expr.un_exp unary_op.not Y) (Expr.un_exp unary_op.not X)) : Expr
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
true
is_valid: Expr → Bool
is_valid
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
))
[]
find_counterexamples_bool: Expr → List (List Bool)
find_counterexamples_bool
((
X: Expr
X
Y: Expr
Y
) (¬
Y: Expr
Y
¬
X: Expr
X
)) -- Find all the models of an expression.
[[false, false], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)) -- Simple model counting.
3
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
)
1
count_models: Expr → Nat
count_models
(
X: Expr
X
Y: Expr
Y
) -- Find all models, return list of functions
[]
find_models: Expr → List Interp
find_models
(
X: Expr
X
¬
X: Expr
X
) -- expect []
3
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 3
1
(
find_models: Expr → List Interp
find_models
(
X: Expr
X
Y: Expr
Y
)).
length: {α : Type} → List α → Nat
length
-- expect 1 -- Find all models, returns list of bool lists
[]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- []
[[false], [true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
¬
X: Expr
X
) -- [[false], [true]
[[true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(
X: Expr
X
Y: Expr
Y
) -- [[true, true]]
[[false, false], [false, true], [true, false], [true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
(¬(
X: Expr
X
Y: Expr
Y
) ¬
X: Expr
X
¬
Y: Expr
Y
) -- all four interps
[[false, false, false], [false, false, true], [false, true, false], [false, true, true], [true, false, false], [true, false, true], [true, true, false], [true, true, true]]
find_models_bool: Expr → List (List Bool)
find_models_bool
((
X: Expr
X
Y: Expr
Y
) (
Y: Expr
Y
Z: Expr
Z
) (
X: Expr
X
Z: Expr
Z
)) -- all eight interps

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.

Currently as quoted from Z3 Python Tutorial Python files.

Cats Mice Dogs

Sudoku

Eight Queens