Propositional Logic: A Satisfiability Solver

You will recall that we say that an expression (formula) in propositional logic is valid if it evaluates to true under all possible interpretations (Boolean values for the variables that appear in it). It is satisfiable if there is at least one intepretation under which it is true. And unsatisfiable if there's not even on interpretation that makes the expression true.

We can understand these concepts clearly in terms of truth tables. As we've seen, each of the input columns represents a variable that can appear in the expression being evaluated. Each row up to but no including the final column represents one of the possible intepretations of the variables in the column (a function mapping those variables to Boolean values). The final column lists the values of the expression for each of the intepretations.

As an example, consider the propositional logic expression, X. Here's the truth table for it. There's one input column, for the one variable. There are two interpretations (rows), that we can write as {X ↦ T} and {X ↦ F}. Finally, the output column gives the truth value of the expression we're evaluating, X, under each interpretation.

v₀{v₀}
TT
FF

From such a truth table it's trivial to determine whether a formula is valid, satisfiable, or unsatisfiable. If all output values are true, the formula is valid. It is also said to be a tautology. If at least one output is true, the formula is satisfiable, and the interpretations that make it true are said to be models of the formula, or solutions. And if all the outputs are false, the expression is unsatisfiable and so has no models/solutions. From the truth table here, we can easily see that X is not valid, but it is satisfiable, with {X ↦ T} as a model.

Exercise: Is the propositional logic expression, X ∧ ¬X valid, satisfiable, or unsatisfiable? What are its models, if any?

v₀{v₀} ∧ ¬{v₀}
T_
F_

Exercise: Answer the same two questions for X ∨ ¬X. To derive your answers, fill in and analyze the truth tables for the two expressions.

v₀{v₀} ∨ ¬{v₀}
T_
F_

Chapter Plan

In the rest of this chapter we'll see how we can perhaps automate the generation of truth tables for any expressions, and thus have what we need to automatically determine its validity, satisfiability, or unsatisfiability, and models if any.

The key element will be a function that, when given the number, v, of variables (columns) in a truth table, computes a list of all 2^v interpretation functions. By evaluating the expression under each interpretation we'll get the list of output values in the truth table. We then just analyze that list to determine whether all, some, or none of the output values is true. The models of the expression, if any, are the interpretations in the rows that have true output values.

The rest of this chapter is in several sections. First we include our specification of the syntax and semantics of propositional logic, without commentary. You can skip over this section unless you need to review the definitions.

Next, we define a function that when given the number of variables, v, in an expression, computes the input side of a truth table as a list of 2^v rows, Each row is a distinct list of v Boolean values, as would appear in a paper-and-pencil truth table, and represents one of the 2^v possible interpretations of the variables that might appear in the expression. The i'th entry in each row is the value assigned by that row/interpretation to the i'th variable (column), with i ranging from 0 to v-1.

Here's an example of the input side of a truth table for an expression with two variables. Be sure you can explain precisely what function, from variables to Boolean values, each row represents.

v₀v₁
i₀FF
i₁FT
i₂TF
i₃TT

We can write the interpretation functions, corresponding to the rows, as follows.

  • i₀ = {v₀ ↦ F, v₁ ↦ F}
  • i₁ = {v₀ ↦ F, v₁ ↦ T}
  • i₂ = {v₀ ↦ T, v₁ ↦ F}
  • i₃ = {v₀ ↦ T, v₁ ↦ T}

Next, the trickiest part, we define a function that takes as its input a row of Boolean values, {b₀, ..., bᵥ₋₁} and that returns the corresponding interpretation function, of type Interp (i.e., var → Bool). We can then use such a function as an argument to our semantic function, eval_expr to compute the truth value of a given expression under that particular interpretation.

Each such interpretation function behaves as follows. For i in the range of 0 to v-1, given the i'th variable, vᵢ (mk_var i) as input, it returns bᵢ, the Boolean value in the i'th position in the row of Boolean values, as its output. Otherwise, for variables with i indices outside the range of interest, it just returns a default value, here false.

Next, we define a function that takes a list of all 2^v rows of a truth table, containing Boolean values, and convert it into a list of interpretation functions, of type Interp.

Finally, from there, we can easily implement the functions that we really want: take any propositional logic expression and tell us if it's valid, satisfiable, or unsatisfiable. We will also want to be able to get a list of models (interpretations) for any given expression.

Propositional Logic Syntax and Semantics

Here we just repeat our specification of the syntax and semantics of propositional logic. By know you are expected to understand the concrete syntax of propositional logic, as defined here, and also the purpose of the eval_expr semantic evaluator, that takes any expression in this syntax along with an interpretation and returns the truth value of the expression under the given interpretation of its atomic propositional variables.

structure 
var: Nat → var
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
|
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
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
def
eval_expr: Expr → Interp → Bool
eval_expr
:
Expr: Type
Expr
Interp: Type
Interp
Bool: Type
Bool
| (
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
)

Generating Truth Table Rows

Our first function will take as its input the number of variables (columns), v, for which the input side of a truth table is to be generated. We will represent this value as list of 2^v rows, each of which is a list list of v Boolean values. Here, for example, is a table that represents the input side of a truth table with three variables.

v₀v₁v₃
0FFF
1FFT
2FTF
3FTT
4TFF
5TFT
6TTF
7TTT

Key Insight

Let's see what pattern emerges if we replace each true and false value in this table with a corresponding binary digit (bit), 0 for false and 1 for true.

v₀v₁v₃
0000
1001
2010
3011
4100
5101
6110
7111

Do you see it? What's the relationship between the row index and the sequence of binary digits in each row? Think about it before reading on. The answer: each row of binary digits represents the corresponding row index in binary notation. In other words, the rows of a truth table really correspond directly to binary representations of corresponding row indices, ranging from 0 to 2^v - 1. The upshot is that we can compute the r'th row of the input side of a truth table by doing nothing more than representing the row index as a binary numeral, padded with zeros on the left so that it's at least v digits wide, and then replacing the binary digits with the corresponding Boolean truth values.

Algorithm Design

These insights unlock an algorithm design strategy. First, define a function that, when given a number of variables, v, and a row index, r, returns the interpretation that associates the variables in the columns to the Boolean represented by the bit values in the binary representation of r.

Once we have this function, it'll be straightforward to take a number, v, of variables, v, and return a list of the 2^v rows for the input side of a truth table.

Row Index to List of Binary Digits

We'll start by defining a function that converts a given natural number into its binary representation as a list of binary digits. We'll just the natural numbers, 0 and 1 to represent binary digits.

The key observations here are (1) the rightmost bit of a binary representation of a number is 0 if the number is even and 1 if it's odd; (2) the rest of the bits are shifted one place to the right, dropping the rightmost bit, by division by 2. So what we do, while bits remain is to repeatedly (1) compute the rightmost bit, (2) shift the rest of the bits right.

To illustrate, consider row number, 5. The binary representation of 5 is 101. From right to left that's 12^0 + 02^1 + 12^2 = 1 + 0 + 4 = 5. The number is odd so the rightmost bit is 1, which is the remainder, 5 % 2 = 1*. Now recurse on 5 shifted right by one bit, i.e., on 5/2 using natural number division. 5/2 is 2, so the nexxt bit is 2 % 2 = 0. Recursing on 2 / 2 = 1, we find the next bit to be 1 % 2 = 1. Finally recurse on 1 / 2 = 0. Zero is one of the two base cases, so we're done. The result is the sequence of bits [1, 0, 1]. Exercise: Do it yourself for 6. What's the second base case?

Here's code. We abstract the computations of the right bit and shift right operations as named functions. The recursion, sadly, is not structural.

def 
right_bit: Nat → Nat
right_bit
(
n: Nat
n
:
Nat: Type
Nat
) :=
n: Nat
n
%
2: Nat
2
def
shift_right: Nat → Nat
shift_right
(
n: Nat
n
:
Nat: Type
Nat
) :=
n: Nat
n
/
2: Nat
2
def
Warning: declaration uses 'sorry'
Warning: declaration uses 'sorry'
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
=> [
0: Nat
0
] |
1: Nat
1
=> [
1: Nat
1
] |
n': Nat
n'
+ 2 => have : (
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) < (
n': Nat
n'
+
2: Nat
2
) :=
sorry: shift_right (n' + 2) < n' + 2
sorry
nat_to_bin: Nat → List Nat
nat_to_bin
(
shift_right: Nat → Nat
shift_right
(
n': Nat
n'
+
2: Nat
2
)) ++ [
right_bit: Nat → Nat
right_bit
(
n': Nat
n'
+
2: Nat
2
)]

Okay, you're wondering, what's that weird expression, have : (shift_right (n' + 2)) < (n' + 2) := sorry? To make a long story short, the recursion here is not structural. (Why?) That means that Lean won't be able to prove to itself that the argument to the recursion is always decreasing, which is needed to prove that the recursion always terminates. To avoid Lean giving an error, we have to give Lean an explicit proof that the argument decreases on each recursive call. The mystery code tells Lean that we have such a proof. The sorry says, but we're not going to give it now; just trust us. That's good enough for Lean not to complain. For now, that's what you need to know.

[0]
nat_to_bin: Nat → List Nat
nat_to_bin
0: Nat
0
-- expect [0]
[1]
nat_to_bin: Nat → List Nat
nat_to_bin
1: Nat
1
-- expect [1]
[1, 1]
nat_to_bin: Nat → List Nat
nat_to_bin
3: Nat
3
-- expect [1,1]
[1, 0, 1]
nat_to_bin: Nat → List Nat
nat_to_bin
5: Nat
5
-- expect [1,0,1]
[1, 1, 0]
nat_to_bin: Nat → List Nat
nat_to_bin
6: Nat
6
-- expect [1,1,0]

Left Pad with Zeros

We represent F (false) values in our truth tables as zeros. One remaining issue is that we have to make each output list of binary digits v digits long, to include F values (zeros) on the left. That is, we need to left-pad our lists with zeros so that each list is at least v digits wide.

To do so, we'll iteratively prepend zeros to a given list a number of times equal to v minus the length of a given list. In Lean v - l is zero in all cases where l ≥ v (these are natural numbers), so there is nothing left to do if the input list is long enough.

It's a pretty easy recursion. We take this opportunity to show how you can do a little top-down programming by writing a top-level program that uses one that is then provided as a sort of private subroutine. Learn how to write code like this.

Finally, we note that it's very common to implement a function as a non-recursive top-level program that in turn calls a recursive subroutine, as illustrated here.

def 
zero_pad: Nat → List Nat → List Nat
zero_pad
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
v: Nat
v
,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
(
v: Nat
v
- (
l: List Nat
l
.
length: {α : Type} → List α → Nat
length
))
l: List Nat
l
where
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
:
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Nat: Type
Nat
|
0: Nat
0
,
l: List Nat
l
=>
l: List Nat
l
|
v': Nat
v'
+1,
l: List Nat
l
=>
zero_pad_recursive: Nat → List Nat → List Nat
zero_pad_recursive
v': Nat
v'
(
0: Nat
0
::
l: List Nat
l
)
[0, 0, 0]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
0: Nat
0
]
[0, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
]
[0, 1, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
,
1: Nat
1
]
[0, 1, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
0: Nat
0
,
1: Nat
1
,
1: Nat
1
]
[1, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
3: Nat
3
[
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]
[0, 0, 1, 0, 1]
zero_pad: Nat → List Nat → List Nat
zero_pad
5: Nat
5
[
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]

We can now write a function that will produce the required list of binary digits for the (input part of the) n'th row of a truth table with v variables (columns).

def 
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
: (
row: Nat
row
:
Nat: Type
Nat
) (
cols: Nat
cols
:
Nat: Type
Nat
)
List: Type → Type
List
Nat: Type
Nat
|
r: Nat
r
,
c: Nat
c
=>
zero_pad: Nat → List Nat → List Nat
zero_pad
c: Nat
c
(
nat_to_bin: Nat → List Nat
nat_to_bin
r: Nat
r
)
[0, 0, 0, 1, 0, 1]
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
5: Nat
5
6: Nat
6
-- expect [0, 0, 0, 1, 0, 1]

List of Bits to List of Bools

Next we need a function to convert a list of bits (Nats) to a list of corresponding Bools. We will convert Nat zero to false, and any other Nat to true.

-- Convert nat to bool where 0 ↦ false, ¬0 ↦ true
def 
bit_to_bool: Nat → Bool
bit_to_bool
:
Nat: Type
Nat
Bool: Type
Bool
|
0: Nat
0
=>
false: Bool
false
| _ =>
true: Bool
true
false
bit_to_bool: Nat → Bool
bit_to_bool
0: Nat
0
-- expect false
true
bit_to_bool: Nat → Bool
bit_to_bool
1: Nat
1
-- expect true

With this element conversion function we can now define our list conversion function. There are two cases. First,' given an empty list of Nat we return an empty list of Bool. Second, we have a bit (Nat) at the head of a non-empty list, in which case we return a list with that Nat converted to a Bool at the head, and the conversion of t, the rest of the list of Nats, into a list of Bools recursively.

def 
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
:
List: Type → Type
List
Nat: Type
Nat
List: Type → Type
List
Bool: Type
Bool
| [] =>
[]: List Bool
[]
|
h: Nat
h
::
t: List Nat
t
=> (
bit_to_bool: Nat → Bool
bit_to_bool
h: Nat
h
) :: (
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
t: List Nat
t
) -- expect [false, false, false, true, false, true]
[false, false, false, true, false, true]
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
[
0: Nat
0
,
0: Nat
0
,
0: Nat
0
,
1: Nat
1
,
0: Nat
0
,
1: Nat
1
]

Make the r'th Row of a Truth Table with v Variables

Now we can easily define a function that when given a truth table row number and the number of variables (columns) Given row and columns return list of Bools

def 
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
: (
row: Nat
row
:
Nat: Type
Nat
) (
vars: Nat
vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
r: Nat
r
,
v: Nat
v
=>
bit_list_to_bool_list: List Nat → List Bool
bit_list_to_bool_list
(
mk_bit_row: Nat → Nat → List Nat
mk_bit_row
r: Nat
r
v: Nat
v
) -- expect [false, false, false, true, false, true]
[false, false, false, true, false, true]
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
5: Nat
5
6: Nat
6

List Bool → Interp

We now devise an algorithm to convert a list of Booleans, [b₀, ..., bᵥ₋₁] into an interpretation. Denote (mk_var i), the i'th variable, as vᵢ. The idea then is to convert the Boolean list into an interpretation function with the following behavior: { v₀ ↦ b₀, ..., vᵥ₋₁, vᵢ → false for i ≥ v}.

So how do we turn a list of values into a function from variables to Boolean values? In short, we start with some interpretation, given a variable and a value for it, we return a new function that's exactly the same as the given one except when the variable argument is the same as the variable for which we want to return a new value. In that case, the new function returns the new value when it is applied to the variable whose value is being overridden. The top-level algorithm will then iteratively override the value of each variable according to the values in a given row of a truth table.

The hardest-to-understand function is override. Given an interpretation, a variable whose value is to be overridden, and a new value for that variable, we return a function that when given any variable does a case analysis: if the variable is other than the one being override, we just use the given interpretation to compute and return a result, otherwise the new function returns the specified new value.

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 def
v₀: var
v₀
:=
var.mk: Nat → var
var.mk
0: Nat
0
def
v₁: var
v₁
:=
var.mk: Nat → var
var.mk
1: Nat
1
def
v₂: var
v₂
:=
var.mk: Nat → var
var.mk
2: Nat
2
-- Demonstration def
all_false: Interp
all_false
:
Interp: Type
Interp
:= λ
_: var
_
=>
false: Bool
false
false
all_false: Interp
all_false
v₀: var
v₀
-- expect false
false
all_false: Interp
all_false
v₁: var
v₁
-- expect false
false
all_false: Interp
all_false
v₂: var
v₂
-- expect false -- interp for [false, true, false], i.e., [0, 1, 0] def
interp2: Interp
interp2
:=
override: Interp → var → Bool → Interp
override
all_false: Interp
all_false
v₁: var
v₁
true: Bool
true
false
interp2: Interp
interp2
v₀: var
v₀
-- expect false
true
interp2: Interp
interp2
v₁: var
v₁
-- expect true
false
interp2: Interp
interp2
v₂: var
v₂
-- expect false -- interp for [false, true, true], i.e., [0, 1, 1] def
interp3: Interp
interp3
:=
override: Interp → var → Bool → Interp
override
interp2: Interp
interp2
v₂: var
v₂
true: Bool
true
false
interp3: Interp
interp3
v₀: var
v₀
-- expect false
true
interp3: Interp
interp3
v₁: var
v₁
-- expect true
true
interp3: Interp
interp3
v₂: var
v₂
-- expect true

To turn a list of Booleans into an interpretation, we thus start with some base interpretation, such as all_false, then iterate through the entries in the list, repeatedly overriding the most recently computed interpretation with the so-called maplet, { vᵢ ↦ bᵢ }. The end result will be the interpretation { v₀ ↦ b₀, ..., vᵥ₋₁ ↦ bᵥ₋₁, ...}, with all variables after vᵥ₋₁ being mapped to the value given by the starting interpretation (in practice, all_false, for us).

Note: We introduce a new Lean programming mechanism: the ability to define a function in terms of a "sub-routine" that is subsequently defined in a where block. It is common to define functions this way when the top-level function is non-recursive and takes or computes some additional data that it then passes on to a recursive function that does most of the work.

def 
bools_to_interp: List Bool → Interp
bools_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
| _, [] =>
all_false: Interp
all_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
-- Demonstration def
interp3': Interp
interp3'
:=
bools_to_interp: List Bool → Interp
bools_to_interp
[
false: Bool
false
,
true: Bool
true
,
true: Bool
true
]
false
interp3': Interp
interp3'
v₀: var
v₀
-- expect false
true
interp3': Interp
interp3'
v₁: var
v₁
-- expect true
true
interp3': Interp
interp3'
v₂: var
v₂
-- expect true

From Number of Variables and Row Index to Interpretation

Building in steps, we next define a function that takes a number of variables and a row index and that returns the corresponding interpretation function. It uses mk_row_bools to create the right row of Boolean values then applies the preceding bools_to_interp function to it to return the corresponding interpretation function.

def 
mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row
: (
vars: Nat
vars
:
Nat: Type
Nat
) (
row: Nat
row
:
Nat: Type
Nat
)
Interp: Type
Interp
|
v: Nat
v
,
r: Nat
r
=>
bools_to_interp: List Bool → Interp
bools_to_interp
(
mk_row_bools: Nat → Nat → List Bool
mk_row_bools
r: Nat
r
v: Nat
v
) def
interp3'': Interp
interp3''
:=
mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row
3: Nat
3
3: Nat
3
-- vars=3, row=3 -- Demonstration
false
interp3'': Interp
interp3''
v₀: var
v₀
-- expect false
true
interp3'': Interp
interp3''
v₁: var
v₁
-- expect true
true
interp3'': Interp
interp3''
v₂: var
v₂
-- expect true

From Number of Variables to List of Interpretations

Finally, now, given nothing but a number of variables, we can iteratively generate a list of all 2^v interpretations. We use the same style of function definition above, where the top-level program computes 2^v from v and then passes 2^v (the number of interpretations/rows to generate, along with v, the number of variables, to a recursive function that does most of the work.

def 
mk_interps: Nat → List Interp
mk_interps
(
vars: Nat
vars
:
Nat: Type
Nat
) :
List: Type → Type
List
Interp: Type
Interp
:=
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper
(
2: Nat
2
^
vars: Nat
vars
)
vars: Nat
vars
where
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper
: (
rows: Nat
rows
:
Nat: Type
Nat
) (
vars: Nat
vars
:
Nat: Type
Nat
)
List: Type → Type
List
Interp: Type
Interp
|
0: Nat
0
, _ =>
[]: List Interp
[]
| (
n': Nat
n'
+ 1),
v: Nat
v
=> (
mk_interp_vars_row: Nat → Nat → Interp
mk_interp_vars_row
v: Nat
v
n': Nat
n'
)::
mk_interps_helper: Nat → Nat → List Interp
mk_interps_helper
n': Nat
n'
v: Nat
v

(e : Expression) → Nat, The Number of Variables In e

-- Analyze and understand how this function works!
def 
max_variable_index: Expr → Nat
max_variable_index
:
Expr: Type
Expr
Nat: Type
Nat
|
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
)
0
max_variable_index: Expr → Nat
max_variable_index
{
v₀: var
v₀
}
2
max_variable_index: Expr → Nat
max_variable_index
({
v₀: var
v₀
} {
v₂: var
v₂
}) -- Given expression, return number of variables it assumes 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
/- Generate list of 8 interpretations for three variables -/ def
interps3: List Interp
interps3
:=
mk_interps: Nat → List Interp
mk_interps
3: Nat
3
8
interps3: List Interp
interps3
.
length: {α : Type} → List α → Nat
length
-- expect 8

From List Interp and Expr to List of Bool Outputs

Now how about a function that takes a list of interpretations and an expresssion and that produces a list of output values?

def 
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
:
List: Type → Type
List
Interp: Type
Interp
Expr: Type
Expr
List: Type → Type
List
Bool: Type
Bool
| [], _ =>
[]: List Bool
[]
--| h::t, e => (eval_expr e h)::eval_expr_interps t e |
h: Interp
h
::
t: List Interp
t
,
e: Expr
e
=>
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
t: List Interp
t
e: Expr
e
++ [
eval_expr: Expr → Interp → Bool
eval_expr
e: Expr
e
h: Interp
h
]

The change in the preceding algorithm made after class puts the list of output values in order with respect to our enumeration of interpretations.

-- Test/Demonstration cases
[false, false, false, true]
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
(
mk_interps: Nat → List Interp
mk_interps
2: Nat
2
) ({
v₀: var
v₀
} {
v₁: var
v₁
}) -- [F,F,F,T]
[false, true, true, true]
eval_expr_interps: List Interp → Expr → List Bool
eval_expr_interps
(
mk_interps: Nat → List Interp
mk_interps
2: Nat
2
) ({
v₀: var
v₀
} {
v₁: var
v₁
}) -- [F,T,T,T]

From Expr to max Variable Index

But our interface isn't yet ideal. We're providing an expression as an argument, and from it we should be able to figure out how many variables are involved. In other words, we shouldn't have to provide a list of interpretations as a separate (and here the first) argument. The observation that leads to a solution is that we can analyze any expression to determine the max index of any variable appearing in it. If we add 1 to that index, we'll have the number of variables in the expression and thus the number of columns in the truth table. We can then use mk_interps with that number as an argument to create the list of interpretations, corresponding to truth table rows, that ne need to pass to eval_expr_interps to get the list of outputs values.

Expr → List Bool: One Value For Each Interpretation

Here's a really important function. Given an expression in propositional logic (using our syntax) it returns the list of outputs values under each of the possible interpretations of the variables (thus atomic expressions) in the given expression.

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_interps: List Interp → Expr → List Bool
eval_expr_interps
(
mk_interps: Nat → List Interp
mk_interps
(
num_vars: Expr → Nat
num_vars
e: Expr
e
))
e: Expr
e

Demonstration/Tests: Confirm that actual results are as expected by writing out the truth tables on paper. Note that in the second case, with the max variable index being 2 (Z is var.mk 2), we have 3 variables/columns, thus 8 rows, and thus a list of 8 output values.

Let's give nicer names to three atomic propositions (i.e., variable expressions).

def 
X: Expr
X
:= {
v₀: var
v₀
} def
Y: Expr
Y
:= {
v₁: var
v₁
} def
Z: Expr
Z
:= {
v₂: var
v₂
}

Now we can produce lists of outputs under all interpretations of variables from index 0 to the max index of any variable appearing in the given expression. Confirm that the results are expected by writing out the truth tables on paper, computing the expected outputs, and checking them against what we compute here.

[false, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Y: Expr
Y
)
[false, true, false, true, true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(
X: Expr
X
Z: Expr
Z
) -- Write the truth tables on paper then check here
[false, false, false, false, false, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
) (
X: Expr
X
Z: Expr
Z
))
[false, false, false, true, true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
) (
X: Expr
X
Z: Expr
Z
)) -- Study expression and predict outputs before looking. -- What names would you give to these particular propositions?
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(((¬
X: Expr
X
¬
Y: Expr
Y
) ¬(
X: Expr
X
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)))
[true, true, true, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
(((¬
X: Expr
X
¬
Y: Expr
Y
) ¬(
X: Expr
X
Y: Expr
Y
)))
[true, false, false, true]
truth_table_outputs: Expr → List Bool
truth_table_outputs
((
X: Expr
X
Y: Expr
Y
))

HOMEWORK PART 1:

Write three functions

  • sat : Expr → Bool
  • unsat: Expr → Bool
  • valid: Expr → Bool

Given any expression, e, in propositional logic, the first returns true if e is sastisfiable, otherwise false. The second returns true if e is unsatisfiable, otherwise false. The third returns true if e is valid, and otherwise returns false. You can write helper functions if/as needed. Write short comments to explain what each of your functions does. Write a few test cases to demonstrate your results.

-- Here

def 
reduce_or: List Bool → Bool
reduce_or
:
List: Type → Type
List
Bool: Type
Bool
Bool: Type
Bool
| [] =>
false: Bool
false
|
h: Bool
h
::
t: List Bool
t
=>
or: Bool → Bool → Bool
or
h: Bool
h
(
reduce_or: List Bool → Bool
reduce_or
t: List Bool
t
) def
reduce_and: List Bool → Bool
reduce_and
:
List: Type → Type
List
Bool: Type
Bool
Bool: Type
Bool
| [] =>
true: Bool
true
|
h: Bool
h
::
t: List Bool
t
=>
or: Bool → Bool → Bool
or
h: Bool
h
(
reduce_and: List Bool → Bool
reduce_and
t: List Bool
t
) def
is_sat: Expr → Bool
is_sat
:
Expr: Type
Expr
Bool: Type
Bool
:= λ
e: Expr
e
:
Expr: Type
Expr
=>
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
:
Expr: Type
Expr
Bool: Type
Bool
:= λ
e: Expr
e
:
Expr: Type
Expr
=>
reduce_and: List Bool → Bool
reduce_and
(
truth_table_outputs: Expr → List Bool
truth_table_outputs
e: Expr
e
) -- A few tests
true
is_valid: Expr → Bool
is_valid
(
X: Expr
X
) -- expect false
true
is_sat: Expr → Bool
is_sat
(
X: Expr
X
) -- exect true
false
is_sat: Expr → Bool
is_sat
(
X: Expr
X
¬
X: Expr
X
) -- expect false #eval
Error: unknown identifier 'is_unsat'
(X ∧ ¬X): ?m.180690
(X ¬X)
-- expect true
true
is_valid: Expr → Bool
is_valid
(
X: Expr
X
¬
X: Expr
X
) -- expect true
true
is_valid: Expr → Bool
is_valid
((¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
))) -- expect true
true
is_valid: Expr → Bool
is_valid
(¬(
X: Expr
X
Y: Expr
Y
) (¬
X: Expr
X
¬
Y: Expr
Y
)) -- expect true
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
Error: type expected, got (X : Expr)
Error: cannot evaluate code because '_eval._lambda_1' uses 'sorry' and/or contains errors
-- expect false -- Test cases

A SAT Solver

A SAT solver takes an expression, e, and returns a value of the sum type, SomeOrNone, an instance of which which holds either some model, if there is at least one, or nothing. We use a sum type, Interp ⊕ Unit: (Sum.inl m) returns the model, m, while Sum.inr Unit.unit signals that there is no model to return.

def SomeModelOrNone := Interp  Unit   -- This is a *type*


/-
Here's the function. Note thus use of several "let bindings"
in this code. They bind names, as shorthands, to given terms, 
so a final return value can be expressed more succinctly and 
clearly. This is a common style of coding in most functional
programming languages. Here we bind names to two terms, then
the expression, *find_model interps e*, defines the return
value. 
-/
def get_model_fun : Expr  SomeModelOrNone
| e =>
  let num_vars := num_vars e
  let interps := (mk_interps num_vars)
  find_model interps e
where find_model : List Interp  Expr  SomeModelOrNone
| [], _ => Sum.inr Unit.unit
| h::t, e => if (eval_expr e h) then Sum.inl h else find_model t e

-- Tests
Sum.inl fun v => Decidable.rec (fun h => false) (fun h => true) (Bool.rec (isFalse (_ : false = true Bool.noConfusionType False false true)) (isTrue (_ : true = true)) ((Nat.rec { fst := fun x => Nat.rec (fun x => true) (fun n n_ih x => false) x PUnit.unit, snd := PUnit.unit } (fun n n_ih => { fst := fun x => Nat.rec (fun x => false) (fun n_1 n_ih x => x.1.1 n_1) x { fst := n_ih, snd := PUnit.unit }, snd := { fst := n_ih, snd := PUnit.unit } }) v.1).1 0))
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
) -- expect Sum.inl _ (a function)
Sum.inr PUnit.unit
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
X: Expr
X
) -- expect Sum.inr Unit.unit -- List of Booleans for first *num_vars* variables under given Interp def
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
:
Interp: Type
Interp
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
| _,
0: Nat
0
=>
[]: List Bool
[]
|
i: Interp
i
, (
n': Nat
n'
+ 1) =>
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
i: Interp
i
n': Nat
n'
++ [(
i: Interp
i
(
var.mk: Nat → var
var.mk
n': Nat
n'
))]

Given some model, return list of Boolean values of first num_vars variables, or in the case of no model, just return an empty list.

def 
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
:
SomeModelOrNone: Type
SomeModelOrNone
(
num_vars: Nat
num_vars
:
Nat: Type
Nat
)
List: Type → Type
List
Bool: Type
Bool
|
Sum.inl: {α : Type ?u.194322} → {β : Type ?u.194321} → α → α ⊕ β
Sum.inl
i: Interp
i
,
n: Nat
n
=>
interp_to_bools: Interp → Nat → List Bool
interp_to_bools
i: Interp
i
n: Nat
n
|
Sum.inr: {α : Type ?u.194352} → {β : Type ?u.194351} → β → α ⊕ β
Sum.inr
_, _ =>
[]: List Bool
[]
-- Test cases
[true, false]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
Y: Expr
Y
))
2: Nat
2
[]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(
X: Expr
X
¬
X: Expr
X
))
2: Nat
2
[true, false]
some_model_or_none_to_bools: SomeModelOrNone → Nat → List Bool
some_model_or_none_to_bools
(
get_model_fun: Expr → SomeModelOrNone
get_model_fun
(¬
X: Expr
X
¬
Y: Expr
Y
))
2: Nat
2
-- list of all models, then convert to list of lists of bools?