Model Finders and Counterexample Generators

The main topic of this chapter is model and counterexample generation: given a proposition in propositional logic, find models if there are any, and similarly find counterexamples if there are any.

We'll begin by generalizing some patterns we've been seeing in functions that handle lists. The first section introduces and illustrates the use of List map, foldr, and filter functions.

Second, we'll see that with these functions in hand and a better understanding of recursion, we can improve our propositional logic satisfiability checking functions.

Finally, we will introduce the concept of a model finder for expressions in propositional logic, also known as a SAT solver, and see how that idea can also provide a way to generate counterexamples to propositions that are not always true.

Higher-Order Functions On Lists

List.map

The List.map function, converts a list of α terms, into a list of corresponding β values by applying a given function, f : α → β, to each α in turn. E.g., map (λ (s : String) => s.length) ["Hello", "Lean"] returns [5, 4].

Here's the type of List.map in the Lean libraries.

@List.map : {α : Type u_1} {β : Type u_2} β) List α List β
@
List.map: {α : Type u_1} → {β : Type u_2} → (α → β) → List α → List β
List.map
[1, 2, 3, 4, 5]
List.map: {α β : Type} → (α → β) → List α → List β
List.map
(λ
n: Nat
n
=>
n: Nat
n
+
1: Nat
1
) [
0: Nat
0
,
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
]
[1, 4, 6]
List.map: {α β : Type} → (α → β) → List α → List β
List.map
String.length: String → Nat
String.length
[
"I": String
"I"
,
"Love": String
"Love"
,
"Logic!": String
"Logic!"
]

List.foldr

The foldr function converts a binary operation along with its identity element into a generalized n-ary operation that takes any number of arguments, in a list. As an example, our reduce_or function, taking a list of Bools and reducing it to just one, indicating whether the list has at least one true value, is simply an n-ary extension of or. Applying such an n-ary operation on no arguments (an empty list) simply returns the identity element (base case value).

@List.foldr : {α : Type u_1} {β : Type u_2} β β) β List α β
@
List.foldr: {α : Type u_1} → {β : Type u_2} → (α → β → β) → β → List α → β
List.foldr
15
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.add: Nat → Nat → Nat
Nat.add
0: Nat
0
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 15
0
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.mul: Nat → Nat → Nat
Nat.mul
0: Nat
0
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 120, oops!
120
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
Nat.mul: Nat → Nat → Nat
Nat.mul
1: Nat
1
[
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
] -- expect 120, ah!

List.filter

The List.filter function takes a list, l of α values, and an α → Bool predicate function that indicates whether a given α value has a particular property, and returns the sublist of α values in l that have property, p.

@List.filter : {α : Type u_1} Bool) List α List α
@
List.filter: {α : Type u_1} → (α → Bool) → List α → List α
List.filter
[0, 2, 4, 6]
List.filter: {α : Type} → (α → Bool) → List α → List α
List.filter
(λ (
n: Nat
n
:
Nat: Type
Nat
) =>
n: Nat
n
%
2: Nat
2
==
0: Nat
0
) [
0: Nat
0
,
1: Nat
1
,
2: Nat
2
,
3: Nat
3
,
4: Nat
4
,
5: Nat
5
,
6: Nat
6
,
7: Nat
7
]

Propositional Logic: The Next Generation

Here again is our definition of the syntax and semantics of propositional logic, now supporting all the connectives, including ⇔. There's little additional information here to review, so you may skim this section quickly.

Syntax

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

Semantics

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
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_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
-- main semantic evaluation function 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
)

Satisfiability Properties

Next we present an improved version of or code for checking of expressions for validity, satisfiability, and unsatisfiability.

One significant enhancement, suggested by Mikhail, is replacement of our rather ponderous approach to generating the input sides of truth tables with a single recursive function. We also use our new map, filter, and reduce functions to replace numerous specialized instances.

Truth Table Input Rows

We had previousl developed an explanatory but ponderous approach to generating a list of of all lists of boolean input rows. The idea was to treat the each (input) row as a binary expansion of the row index (a lit of bit), convert bits to bools, and add padding on the left. Mikhail noticed that we could replace it all with a single recursive function.

Exercise: Study this function definition until you understand fully how it works. Along the way, use it to generate a few outputs then inspect them to be sure you know what the function does. Figure out the recursion works to the point you're confident you could write the code yourself. To test yourself, erase the implementation then write it again.

-- Mikhail
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'
)) -- REVIEW
[[]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
0: Nat
0
[[false], [true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
1: Nat
1
[[false, false], [false, true], [true, false], [true, true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
2: Nat
2
[[false, false, false], [false, false, true], [false, true, false], [false, true, true], [true, false, false], [true, false, true], [true, true, false], [true, true, true]]
make_bool_lists: Nat → List (List Bool)
make_bool_lists
3: Nat
3

Bool List to/from Interpretation Function

Given a list of n Boolean values, [b₀, ..., bₙ₋₁], we have to be able to turn it into an interpretation function, so that we can evaluate expressions with that interpretation using eval_expr. The resulting function will be { v₀ ↦ b₀, ..., vₙ₋₁ ↦ bₙ₋₁}, where each vᵢ means (var.mk i).

Our approach will be to start with a given interpretation (such as the all false interpretation) and then for each bᵢ in the list of Booleans, we will iteratively override the function so that when it's used to evaluate the value of vᵢ it will return bᵢ.

-- Function override
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
) -- when applied to var then
new_val: Bool
new_val
-- return new value else
old_interp: Interp
old_interp
v: _root_.var
v
) -- else retur old value -- Bool list to interpretation function -- Uses list length as number of variables to associate with bools 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 recursively computed interp mapping variable to head bool
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

To think about: smells like some kind of fold. Iteratively combine bool at head of list with given interpretation by overriding at with the h ead value for the which? variable

In addition to converting Boolean lists to interpretations it will also be useful to turn interpretations back into Boolean lists, where the length of each list is typically fixed at a specified number of variables (all variables beyond a certain point being irrelevant to a given expression).

-- From number of variables, interpretation, to list of Bools
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'
))] -- From number of variables, list of interpretations, to list of Bool lists 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

Maximum Variable Index in Expression

We will consider the number of variables to include in a truth table for a given expression to be the one plus the zero-based index of the highest-indexed variable in any given expression. For example, if an expression uses only v₉ explicitly we will consider it to use all ten variables, v₀ to v₉ inclusive.

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
)

Number of Variables in Expression

We take the number of variables in an expression to be the index of the highest-indexed variable in the expression, plus one to account for the usual zero-based indexing.

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

From Expression to List of Interpretations

Given an expression, we compute the number, n, of variables it uses then we generate a list of all 2^n interpretation functions for it. Note that we just eliminate a whole raft of ponderous code with a single clever recursive function, thanks to Mikhail.

-- Number of variables to interpretations list using Mikhail's code
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
) -- From expression to a list of interpretations for it 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
)

Truth Table Outputs

Exercise: Replace the following definition of truth_table_outputs with a single line of code using List.map. The resulting list of Boolean values should reflect the values of the given expression under each interpretation in the list of interpretations. You will use map to convert a list of interpretations (for e) into a list of Boolean values.

-- The column of truth table outputs for 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
] -- REVIEW def
truth_table_outputs: Expr → List Bool
truth_table_outputs
:
Expr: Type
Expr
List: Type → Type
List
Bool: Type
Bool
|
e: Expr
e
=>
List.map: {α β : Type} → (α → β) → List α → List β
List.map
(
eval_expr: Expr → Interp → Bool
eval_expr
e: Expr
e
) (
mk_interps_vars: Nat → List Interp
mk_interps_vars
(
num_vars: Expr → Nat
num_vars
e: Expr
e
)) -- | e => eval_expr_over_interps e (mk_interps_vars (num_vars e)) -- where eval_expr_over_interps : Expr → List Interp → List Bool -- | _, [] => [] -- | e, h::t => eval_expr_over_interps e t ++ [eval_expr e h]

n-ary And and Or functions

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

Finally we can define the API we want to provide for checking arbitrary propositional logic expressions for their satisfiability properties: for being satisfiable, valid, or unsatisfiable.

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
)

Models and Counterexamples

We now turn to the third and last major topic in this chapter. Given a propositional logic expression, e, a model finder finds a model of e if there is one. It returns either a model of e if there is one or a signal that there isn't one.

To return either a model if there is one or a signal that there isn't one, we could use a sum type: either a model on the left or Unit.unit on the right to signal that there is no model.

def 
SomeInterpOrNone: Type
SomeInterpOrNone
:=
Interp: Type
Interp
Unit: Type
Unit
-- NB: this is a *type*

A better solution is to use the standard polymorphic Option type. Its two constructors are some α and none. The first is used to construct an option carrying a value, (a : α). The second is used (in lieu of Sum.inr Unit.unit) to indicate that there's no value to provide.

def 
o1: Option Bool
o1
:=
Option.some: {α : Type} → α → Option α
Option.some
true: Bool
true
def
o2: Option Bool
o2
:= @
Option.none: {α : Type} → Option α
Option.none
Bool: Type
Bool
-- need to make type argument explicit

Model Finder

Here's the main API for our model finder. Given an expression, e, return some m, m a model of e if there is one, or none if not.

Option : Type u_1 Type u_1
@
Option: Type u_1 → Type u_1
Option
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
-- REVIEW -- Utility: convert a "Option model" into a list of Bools, empty for none def
some_model_or_none_to_bools: SomeInterpOrNone → Nat → List Bool
some_model_or_none_to_bools
:
SomeInterpOrNone: Type
SomeInterpOrNone
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
Sum.inl: {α : Type ?u.57773} → {β : Type ?u.57772} → α → α ⊕ β
Sum.inl
i: Interp
i
,
n: Nat
n
=>
interp_to_list_bool: Nat → Interp → List Bool
interp_to_list_bool
n: Nat
n
i: Interp
i
|
Sum.inr: {α : Type ?u.57803} → {β : Type ?u.57802} → β → α ⊕ β
Sum.inr
_, _ =>
[]: List Bool
[]

Model Enumerator

The main API of our model enumeration section is the function, find_models, that takes an expression, e,, and returns a list of all models of e. It does so by generating an exhaustive list of all interpretations then filtering them to save those that make e true.

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 -- Render models of e : Expr as List of Bool Lists (num_vars e long) 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
)

Model Counter

A model counter takes an expression and tells you how many models it has. From a list of all models, it's obviously easy to derive the number of models: it's just the length of the list. Note that we use function composition to define our model counting function.

def 
count_models: Expr → Nat
count_models
:=
List.length: {α : Type} → List α → Nat
List.length
find_models: Expr → List Interp
find_models

Counter-Example Generator

More interesting, and oft used, is counter-example finding. When we say we want to disprove a proposition, mean is that we want to show that it's not valid: that there's at least one interpretation that makes the proposition is false. If that is so, then it makes the negation of the proposition true. Counterexamples, if there are any, are models of the negation of a propositio; and we now know how to find such models using our model finder. Defining a counter-example finder is thus trivial.

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
)

Tests and Demonstrations

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
}
[false, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
)
true
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
or: Bool → Bool → Bool
or
false: Bool
false
(
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
))
false
List.foldr: {α β : Type} → (α → β → β) → β → List α → β
List.foldr
and: Bool → Bool → Bool
and
true: Bool
true
(
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
))

Is it true that 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
))

Is it true that if X being true means that Y must be true, then does Y being false imply X is 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 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
))

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

Search for models (returns 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

Search for models (returns list of list of bools)

[]
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

Homework

Forthcoming:

  • Expand make_bool_lists applied to values 0-3.
  • Validate a list of standard inference rules.
  • Find the Fallacies, Explain Counterexamples.
  • Replace ponderous function definition using map.