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₀} |
|---|---|
| T | T |
| F | F |
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₀ | F | F |
| i₁ | F | T |
| i₂ | T | F |
| i₃ | T | T |
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₃ | |
|---|---|---|---|
| 0 | F | F | F |
| 1 | F | F | T |
| 2 | F | T | F |
| 3 | F | T | T |
| 4 | T | F | F |
| 5 | T | F | T |
| 6 | T | T | F |
| 7 | T | T | T |
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₃ | |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 2 | 0 | 1 | 0 |
| 3 | 0 | 1 | 1 |
| 4 | 1 | 0 | 0 |
| 5 | 1 | 0 | 1 |
| 6 | 1 | 1 | 0 |
| 7 | 1 | 1 | 1 |
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.
defright_bit (right_bit: Nat → Natn :n: NatNat) :=Nat: Typen%n: Nat2 def2: Natshift_right (shift_right: Nat → Natn :n: NatNat) :=Nat: Typen/n: Nat2 def2: Nat:Nat →Nat: TypeListList: Type → TypeNat |Nat: Type0 => [0: Nat0] |0: Nat1 => [1: Nat1] |1: Natn' + 2 => have : (n': Natshift_right (shift_right: Nat → Natn' +n': Nat2)) < (2: Natn' +n': Nat2) :=2: Natsorrysorry: shift_right (n' + 2) < n' + 2nat_to_bin (nat_to_bin: Nat → List Natshift_right (shift_right: Nat → Natn' +n': Nat2)) ++ [2: Natright_bit (right_bit: Nat → Natn' +n': Nat2)]2: Nat
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.
nat_to_binnat_to_bin: Nat → List Nat0 -- expect [0]0: Natnat_to_binnat_to_bin: Nat → List Nat1 -- expect [1]1: Natnat_to_binnat_to_bin: Nat → List Nat3 -- expect [1,1]3: Natnat_to_binnat_to_bin: Nat → List Nat5 -- expect [1,0,1]5: Natnat_to_binnat_to_bin: Nat → List Nat6 -- expect [1,1,0]6: Nat
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.
defzero_pad :zero_pad: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Typev,v: Natl =>l: List Natzero_pad_recursive (zero_pad_recursive: Nat → List Nat → List Natv - (v: Natl.l: List Natlength))length: {α : Type} → List α → Natl wherel: List Natzero_pad_recursive :zero_pad_recursive: Nat → List Nat → List NatNat →Nat: TypeListList: Type → TypeNat →Nat: TypeListList: Type → TypeNat |Nat: Type0,0: Natl =>l: List Natl |l: List Natv'+1,v': Natl =>l: List Natzero_pad_recursivezero_pad_recursive: Nat → List Nat → List Natv' (v': Nat0::0: Natl)l: List Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat0]0: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1,1: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat0,0: Nat1,1: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat3 [3: Nat1,1: Nat0,0: Nat1]1: Natzero_padzero_pad: Nat → List Nat → List Nat5 [5: Nat1,1: Nat0,0: Nat1]1: Nat
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).
defmk_bit_row : (mk_bit_row: Nat → Nat → List Natrow:row: NatNat) → (Nat: Typecols :cols: NatNat) →Nat: TypeListList: Type → TypeNat |Nat: Typer,r: Natc =>c: Natzero_padzero_pad: Nat → List Nat → List Natc (c: Natnat_to_binnat_to_bin: Nat → List Natr)r: Natmk_bit_rowmk_bit_row: Nat → Nat → List Nat55: Nat6 -- expect [0, 0, 0, 1, 0, 1]6: Nat
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 defbit_to_bool :bit_to_bool: Nat → BoolNat →Nat: TypeBool |Bool: Type0 =>0: Natfalse | _ =>false: Booltruetrue: Boolbit_to_boolbit_to_bool: Nat → Bool0 -- expect false0: Natbit_to_boolbit_to_bool: Nat → Bool1 -- expect true1: Nat
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.
defbit_list_to_bool_list :bit_list_to_bool_list: List Nat → List BoolListList: Type → TypeNat →Nat: TypeListList: Type → TypeBool | [] =>Bool: Type[] |[]: List Boolh::h: Natt => (t: List Natbit_to_boolbit_to_bool: Nat → Boolh) :: (h: Natbit_list_to_bool_listbit_list_to_bool_list: List Nat → List Boolt) -- expect [false, false, false, true, false, true]t: List Natbit_list_to_bool_list [bit_list_to_bool_list: List Nat → List Bool0,0: Nat0,0: Nat0,0: Nat1,1: Nat0,0: Nat1]1: Nat
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
defmk_row_bools : (mk_row_bools: Nat → Nat → List Boolrow :row: NatNat) → (Nat: Typevars :vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: Typer,r: Natv =>v: Natbit_list_to_bool_list (bit_list_to_bool_list: List Nat → List Boolmk_bit_rowmk_bit_row: Nat → Nat → List Natrr: Natv) -- expect [false, false, false, true, false, true]v: Natmk_row_boolsmk_row_bools: Nat → Nat → List Bool55: Nat66: Nat
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.
defoverride :override: Interp → var → Bool → InterpInterp →Interp: Typevar →var: TypeBool →Bool: TypeInterp |Interp: Typeold_interp,old_interp: Interpvar,var: _root_.varnew_val => (λnew_val: Boolv => if (v: _root_.varv.v: _root_.varn ==n: _root_.var → Natvar.var: _root_.varn) -- when applied to var thenn: _root_.var → Natnew_val -- return new value elsenew_val: Boolold_interpold_interp: Interpv) -- else retur old value defv: _root_.varv₀ :=v₀: varvar.mkvar.mk: Nat → var0 def0: Natv₁ :=v₁: varvar.mkvar.mk: Nat → var1 def1: Natv₂ :=v₂: varvar.mkvar.mk: Nat → var2 -- Demonstration def2: Natall_false :all_false: InterpInterp := λInterp: Type_ =>_: varfalsefalse: Boolall_falseall_false: Interpv₀ -- expect falsev₀: varall_falseall_false: Interpv₁ -- expect falsev₁: varall_falseall_false: Interpv₂ -- expect false -- interp for [false, true, false], i.e., [0, 1, 0] defv₂: varinterp2 :=interp2: Interpoverrideoverride: Interp → var → Bool → Interpall_falseall_false: Interpv₁v₁: vartruetrue: Boolinterp2interp2: Interpv₀ -- expect falsev₀: varinterp2interp2: Interpv₁ -- expect truev₁: varinterp2interp2: Interpv₂ -- expect false -- interp for [false, true, true], i.e., [0, 1, 1] defv₂: varinterp3 :=interp3: Interpoverrideoverride: Interp → var → Bool → Interpinterp2interp2: Interpv₂v₂: vartruetrue: Boolinterp3interp3: Interpv₀ -- expect falsev₀: varinterp3interp3: Interpv₁ -- expect truev₁: varinterp3interp3: Interpv₂ -- expect truev₂: var
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.
defbools_to_interp :bools_to_interp: List Bool → InterpListList: Type → TypeBool →Bool: TypeInterp |Interp: Typel =>l: List Boolbools_to_interp_helperbools_to_interp_helper: Nat → List Bool → Interpl.l: List Boollengthlength: {α : Type} → List α → Natl wherel: List Boolbools_to_interp_helper : (bools_to_interp_helper: Nat → List Bool → Interpvars :vars: NatNat) → (Nat: Typevals :vals: List BoolListList: Type → TypeBool) →Bool: TypeInterp | _, [] =>Interp: Typeall_false |all_false: Interpvars,vars: Nath::h: Boolt => lett: List Boollen := (len: Nath::h: Boolt).t: List Boollengthlength: {α : Type} → List α → Natoverride (override: Interp → var → Bool → Interpbools_to_interp_helperbools_to_interp_helper: Nat → List Bool → Interpvarsvars: Natt) (t: List Boolvar.mk (var.mk: Nat → varvars -vars: Natlen))len: Nath -- Demonstration defh: Boolinterp3' :=interp3': Interpbools_to_interp [bools_to_interp: List Bool → Interpfalse,false: Booltrue,true: Booltrue]true: Boolinterp3'interp3': Interpv₀ -- expect falsev₀: varinterp3'interp3': Interpv₁ -- expect truev₁: varinterp3'interp3': Interpv₂ -- expect truev₂: var
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.
defmk_interp_vars_row : (mk_interp_vars_row: Nat → Nat → Interpvars:vars: NatNat) → (Nat: Typerow:row: NatNat) →Nat: TypeInterp |Interp: Typev,v: Natr =>r: Natbools_to_interp (bools_to_interp: List Bool → Interpmk_row_boolsmk_row_bools: Nat → Nat → List Boolrr: Natv) defv: Natinterp3'' :=interp3'': Interpmk_interp_vars_rowmk_interp_vars_row: Nat → Nat → Interp33: Nat3 -- vars=3, row=3 -- Demonstration3: Natinterp3''interp3'': Interpv₀ -- expect falsev₀: varinterp3''interp3'': Interpv₁ -- expect truev₁: varinterp3''interp3'': Interpv₂ -- expect truev₂: var
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! defmax_variable_index :max_variable_index: Expr → NatExpr →Expr: TypeNat |Nat: TypeExpr.var_exp (Expr.var_exp: var → Exprvar.mkvar.mk: Nat → vari) =>i: Nati |i: NatExpr.un_exp _Expr.un_exp: unary_op → Expr → Expre =>e: Exprmax_variable_indexmax_variable_index: Expr → Nate |e: ExprExpr.bin_exp _Expr.bin_exp: binary_op → Expr → Expr → Expre1e1: Expre2 =>e2: Exprmax (max: {α : Type} → [self : Max α] → α → α → αmax_variable_indexmax_variable_index: Expr → Nate1) (e1: Exprmax_variable_indexmax_variable_index: Expr → Nate2)e2: Exprmax_variable_index {max_variable_index: Expr → Natv₀}v₀: varmax_variable_index ({max_variable_index: Expr → Natv₀} ∧ {v₀: varv₂}) -- Given expression, return number of variables it assumes defv₂: varnum_vars :num_vars: Expr → NatExpr →Expr: TypeNat := λNat: Typee =>e: Exprmax_variable_indexmax_variable_index: Expr → Nate +e: Expr1 /- Generate list of 8 interpretations for three variables -/ def1: Natinterps3 :=interps3: List Interpmk_interpsmk_interps: Nat → List Interp33: Natinterps3.interps3: List Interplength -- expect 8length: {α : Type} → List α → Nat
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 caseseval_expr_interps (eval_expr_interps: List Interp → Expr → List Boolmk_interpsmk_interps: Nat → List Interp2) ({2: Natv₀} ∧ {v₀: varv₁}) -- [F,F,F,T]v₁: vareval_expr_interps (eval_expr_interps: List Interp → Expr → List Boolmk_interpsmk_interps: Nat → List Interp2) ({2: Natv₀} ∨ {v₀: varv₁}) -- [F,T,T,T]v₁: var
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.
truth_table_outputs (truth_table_outputs: Expr → List BoolX ∧X: ExprY)Y: Exprtruth_table_outputs (truth_table_outputs: Expr → List BoolX ∨X: ExprZ) -- Write the truth tables on paper then check hereZ: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ∧X: ExprY) ∨ (Y: ExprX ∧X: ExprZ))Z: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ∨X: ExprY) ∧ (Y: ExprX ∨X: ExprZ)) -- Study expression and predict outputs before looking. -- What names would you give to these particular propositions?Z: Exprtruth_table_outputs ((¬(truth_table_outputs: Expr → List BoolX ∧X: ExprY) ⇒ (¬Y: ExprX ∨ ¬X: ExprY)))Y: Exprtruth_table_outputs (((¬truth_table_outputs: Expr → List BoolX ∨ ¬X: ExprY) ⇒ ¬(Y: ExprX ∧X: ExprY)))Y: Exprtruth_table_outputs ((¬(truth_table_outputs: Expr → List BoolX ∨X: ExprY ) ⇒ (¬Y: ExprX ∧ ¬X: ExprY)))Y: Exprtruth_table_outputs (((¬truth_table_outputs: Expr → List BoolX ∧ ¬X: ExprY) ⇒ ¬(Y: ExprX ∨X: ExprY )))Y: Exprtruth_table_outputs ((truth_table_outputs: Expr → List BoolX ⇔X: ExprY))Y: Expr
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 defreduce_or :reduce_or: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typefalse |false: Boolh::h: Boolt =>t: List Booloror: Bool → Bool → Boolh (h: Boolreduce_orreduce_or: List Bool → Boolt) deft: List Boolreduce_and :reduce_and: List Bool → BoolListList: Type → TypeBool →Bool: TypeBool | [] =>Bool: Typetrue |true: Boolh::h: Boolt =>t: List Booloror: Bool → Bool → Boolh (h: Boolreduce_andreduce_and: List Bool → Boolt) deft: List Boolis_sat :is_sat: Expr → BoolExpr →Expr: TypeBool := λBool: Typee :e: ExprExpr =>Expr: Typereduce_or (reduce_or: List Bool → Booltruth_table_outputstruth_table_outputs: Expr → List Boole) defe: Expris_valid :is_valid: Expr → BoolExpr →Expr: TypeBool := λBool: Typee :e: ExprExpr =>Expr: Typereduce_and (reduce_and: List Bool → Booltruth_table_outputstruth_table_outputs: Expr → List Boole) -- A few testse: Expris_valid (is_valid: Expr → BoolX) -- expect falseX: Expris_sat (is_sat: Expr → BoolX) -- exect trueX: Expris_sat (is_sat: Expr → BoolX ∧ ¬X: ExprX) -- expect false #evalX: Expr(X ∧ ¬X) -- expect true(X ∧ ¬X): ?m.180690is_valid (is_valid: Expr → BoolX ∨ ¬X: ExprX) -- expect trueX: Expris_valid ((¬(is_valid: Expr → BoolX ∧X: ExprY) ⇔ (¬Y: ExprX ∨ ¬X: ExprY))) -- expect trueY: Expris_valid (¬(is_valid: Expr → BoolX ∨X: ExprY) ⇒ (¬Y: ExprX ∧ ¬X: ExprY)) -- expect trueY: Expr-- 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 -- Testsget_model_fun (get_model_fun: Expr → SomeModelOrNoneX) -- expect Sum.inl _ (a function)X: Exprget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprX) -- expect Sum.inr Unit.unit -- List of Booleans for first *num_vars* variables under given Interp defX: Exprinterp_to_bools :interp_to_bools: Interp → Nat → List BoolInterp → (Interp: Typenum_vars :num_vars: NatNat) →Nat: TypeListList: Type → TypeBool | _,Bool: Type0 =>0: Nat[] |[]: List Booli, (i: Interpn' + 1) =>n': Natinterp_to_boolsinterp_to_bools: Interp → Nat → List Boolii: Interpn' ++ [(n': Nati (i: Interpvar.mkvar.mk: Nat → varn'))]n': Nat
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.
defsome_model_or_none_to_bools :some_model_or_none_to_bools: SomeModelOrNone → Nat → List BoolSomeModelOrNone → (SomeModelOrNone: Typenum_vars :num_vars: NatNat) →Nat: TypeListList: Type → TypeBool |Bool: TypeSum.inlSum.inl: {α : Type ?u.194322} → {β : Type ?u.194321} → α → α ⊕ βi,i: Interpn =>n: Natinterp_to_boolsinterp_to_bools: Interp → Nat → List Boolii: Interpn |n: NatSum.inr _, _ =>Sum.inr: {α : Type ?u.194352} → {β : Type ?u.194351} → β → α ⊕ β[] -- Test cases[]: List Boolsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprY))Y: Expr22: Natsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (get_model_fun: Expr → SomeModelOrNoneX ∧ ¬X: ExprX))X: Expr22: Natsome_model_or_none_to_bools (some_model_or_none_to_bools: SomeModelOrNone → Nat → List Boolget_model_fun (¬get_model_fun: Expr → SomeModelOrNoneX ∨ ¬X: ExprY))Y: Expr2 -- list of all models, then convert to list of lists of bools?2: Nat