Subsections of Intro to Programming Logic
Programming Logic Goal
In the next three chapters, we will learn how to reason about different kinds of program structures – assignments, conditional statements, loops, function calls, recursion, lists of elements, and global variables. By the end of chapter 10, we will be able to prove the correctness of simple programs using a toy language that is a subset of Scala. While our toy language is not used in practice, the ideas that we will see to prove program correctness – preconditions, postconditions, loop invariants, and global invariants – can be used to specify functions in ANY language.
We will see that the process for formal specifications and proofs of correctness is rather tedious, even for relatively simple programs. And in practice, proving correctness of computer programs is rarely done. So why bother studying it?
Safety critical code
One case where reasoning about correctness is certainly relevant is the arena of safety critical code – where lives depend on a program working correctly. Certain medical devices, such as pacemakers and continuous glucose monitors, have a software component. If that software fails, then a person could die. We can’t test the correctness of medical devices by installing them in a human body and trying them out – instead, we need to be absolutely sure they work correctly before they are used.
Similarly, there is software in things like shuttle launches. While that might not cost lives, it’s also a process that can’t be fully tested beforehand. After all, no one is going to spend over a billion dollar on a “practice” launch. Instead, we need a way to more formally demonstrate that the software will work correctly.
Specifications
In chapter 9, we will learn to write function specifications. These specifications list any requirements the function has in order to work correctly (preconditions) and descibe the impact of calling the function (postconditions) - most notably, what the function returns in terms of its inputs. Even in cases where we do not formally prove correctness of a program, it is very useful to write a specification for all functions. This can make it clear to any calling code what kinds of parameters should be passed, as well as what to expect about the returned value. By providing this structure, there will be fewer surprises and unintended errors.
Logika Programs
As we study program logic, we will use a toy language within Logika. These Logika programs use a subset of the Scala language, and include the following features:
- Variables (booleans, ints, and sequences [which are like arrays/lists])
- Printing and user input
- Math operations
- Conditional operations
- If and if/else statements
- While loops
- Functions
Running Logika programs
Logika programs should be saved with a .logika extension. To run a Logika program, right-click in the text area that contains the code and select “Run Logika Program”.
Verifying Logika programs
There are two modes in Logika – manual and SymExe. In chapters 8 and 9, we will use manual mode. In chapter 10, we will use SymExe mode. You can change the mode in Logika by going to File->Settings->Tools->Sireum->Logika.
For manual mode, uncheck “Auto mode” and mark the Checker kind as “Forward.”
For SymExe mode, check “Auto mode” and mark the Checker kind as “Summarizing SymExe”.
Logika verification should run automatically as you edit Logika programs and their proofs. If a program is verified, you will see a purple checkmark in the lower right corner just as you did in propositional and predicate logic proofs. If there are syntax or logic errors, you will see them highlighted in red.
Sometimes, the Logika verification needs to be run manually. If you don’t see either red errors or a purple checkmark, right-click in the text area that contains the code and select “Logika Check”.
Example programs
This section contains four sample Logika programs that highlight the different language features.
This first example gets a number as input from the user, adds one to it, and prints the result:
import org.sireum.logika._
var x: Z = readInt("Enter a number: ")
x = x + 1
println("One more is ", x)
A few things to note:
- Each Logika program begins with an import statement:
import org.sireum.logika._
- The
var
keyword stands for variable, which is something that can be changed. Logika also has a val
keyword, which creates a constant value that cannot be changed.
- Lines do not end in semi-colons
- The parameter to the
readInt(...)
function call is a prompt telling the user what to type. This parameter is optional.
- The code
var x: Z
creates a variable called x
of type Z
– the Z
means integer
Example 2: If/else statements
Here is a Logika program that gets a number from the user, and uses an if/else statement to print whether the number is positive or negative/zero:
import org.sireum.logika._
val num: Z = readInt("Enter a number: ")
if (num > 0) {
println(num, " is positive")
} else {
println(num, " is negative (or zero)")
}
A couple of things to note here:
- The
else
statement MUST appear on the same line as the closing }
of the previous if-statement
- As mentioned above, the
val
keyword in this program means that x
cannot be changed after being initialized
Example 3: While loops
Our next Logika program uses a while loop to print the numbers from 10 down to 1. This program can’t be run the way it appears below, as Logika wants you to do some verification work first. However, this program demonstrates what a while loop will look like:
import org.sireum.logika._
var cur: Z = 10
while (cur >= 1) {
println("Next number: ", cur)
cur = cur - 1
}
Example 4: Sequences and functions
Our final sample Logika program demonstrates sequences (Logika’s version of an array or list) and functions. It contains a function, sumSequence
, which takes a sequence of integers as a parameter and returns the sum of the numbers in the sequence. At the bottom, we can see our test code that creates a sample sequence and tries calling sumSequence
. As in our third example, this code cannot be run without some verification work:
import org.sireum.logika._
def sumSequence(seq: ZS) : Z = {
var sum: Z = 0
var i: Z = 0
while (i < seq.size) {
sum = sum + seq(i)
i = i + 1
}
return sum
}
////// Calling code ////////////
val list: ZS = ZS(1,2,3,4)
val total: Z = sumSequence(list)
println("Sum of elements:", total)
A few things of note here:
-
The definition def sumSequence(seq: ZS) : Z
means that a function named sumSequence
takes a parameter of type ZS
(sequence of integers, Z
= int and S
= sequence) and returns something of type Z
(int)
-
There is an =
after the function header but before the opening {
of the function
-
Functions in Logika are not part of a class - they are more similar to the structure in Python. We can include as many functions as we want in a file. At the bottom of the file (marked below the optional ////// Calling code ////////////
) is the calling code. When a Logika program runs, those calling code lines (which may call different functions) are executed. When the calling code lines are done, the program is done.
Logika program proof syntax
In order to prove correctness of Logika programs, we will add Logika proof blocks to process what we have learned at different points in the program. In general, every time there is an assignment statement, there will be a following Logika proof block updating all relevant facts.
Proof block
Here is the syntax for a proof block:
l"""{
lineNumber. claim justification
...
}"""
Just as with our propositional and predicate logic proofs, we will number the lines in our proof blocks. Each line will contain a claim and a corresponding justification for that claim. We will still be able to use all of our propositional and predicate logic deduction rules, but we will learn new justifications for processing program statements. Each program may have multiple of these proof blocks to process each assignment statement.
Premise justification
Our first justification in programming logic is premise. In a Logika proof block, we use premise as a justification in the following cases:
In both cases, the claim must capture the current value of the involved variables.
For example, consider the following program:
import org.sireum.logika._
val x: Z = 6
x = x + 1
We could insert a proof block between the two lines to express that x
currently has the value 6:
import org.sireum.logika._
val x: Z = 6
l"""{
1. x == 6 premise
}"""
x = x + 1
But we could NOT have the same proof block after incrementing x
, since x
’s value has changed since that claim was established:
import org.sireum.logika._
val x: Z = 6
//this proof block is correct -- it captures the current value of x
l"""{
1. x == 6 premise
}"""
x = x + 1
//NO! This statement no longer captures the current value of x
l"""{
1. x == 6 premise
}"""
Using previous deduction rules
We can use any of our previous deduction rules in a Logika proof block. For example:
import org.sireum.logika._
val x: Z = 6
val y: Z = 7
l"""{
1. x == 6 premise
2. y == 7 premise
3. (x == 6) ∧ (y == 7) ∧i 1 2
}"""
Assert and Assume
Assert statements
An assert statement in Logika uses the syntax assert(expression)
, where expression
is of type bool
. The assert passes if the expression is true, and throws an error if the expression is false.
Logika assert statements are different than assert statements in languages like Java and C# – in those languages, the code in the assert statement (which often includes test method calls) is actually run, and the assert statement checks whether those methods are returning the expected values. In Logika, assert statements only pass if we have previously PROVED what we are claiming – the code is never executed.
Example
Consider the following program:
import org.sireum.logika._
val x: Z = 6
val y: Z = 6
val z: Z = 4
assert (x == y & y > z)
While we can tell that x
equals y
and that y
is greater than z
, this code would fail in Logika (at least in its manual mode, which is what we are using now). In order for an assert to pass, we must have already proved EXACTLY the statement in the assert.
Why do WE know that x
equals y
and that y
is greater than z
? From looking at the code! We can tell that x
and y
are given the same value (6), and that y
’s value of 6 is bigger than z
’s value of 4.
So far, we know we can pull in the values of each variable as premises, like this:
import org.sireum.logika._
val x: Z = 6
val y: Z = 6
val z: Z = 4
l"""{
1. x == 6 premise
2. y == 6 premise
3. z == 4 premise
//how to say that x and y are equal, and that y is bigger than z?
}"""
assert (x == y & y > z)
But we don’t yet know how to justify claims that describe how variables compare to one another. We will learn how to do that with the algebra
and subst
rules in section 8.4.
Using conditional operators
Notice that the program above used an &
symbol for an AND operator in an assert statement. Because asserts are part of the program and not part of a proof block, they will use the same conditional operators as in Logika programs. Here is a summary:
Meaning |
Operator in proofs |
Operator in Logika programs/assumes/asserts |
p AND q |
p ∧ q |
p & q |
p OR q |
p ∨ q |
p | q |
NOT p |
¬p |
!p |
p IMPLIES q |
p → q |
not available |
If we wanted to write an assert statement that would be true when some variable p
was even and/or was a positive two-digit number, we could say:
assert(p % 2 == 0 | (p > 9 & p < 100))
The implies operator is not available in assert statements. However, we can make use of one of the equivalences we learned about in section 3.4: that p → q
is equivalent to ¬ p ∨ q
. So if we wanted to assert that if p
was positive, then q
was equal to p
, then we could write:
//expressing the implicaton ((p > 0) → (q == p))
assert(!(p > 0) | (q == p))
Or, equivalently, we could write:
assert((p <= 0) | (q == p))
Assume statement
An assume statement in Logika uses the syntax assume(expression)
. If the expression is satisfiable, then we can use expression
as a premise in the following Logika proof block.
Assume example
For example:
var a: Z = readInt()
assume (a > 0)
l"""{
1. a > 0 premise
}"""
Assume statements are almost always used to make assumptions about user input. Perhaps our program only works correctly for certain values of input. If we can assume that the user really did enter acceptable values, then we can use that information (by pulling it in as a premise in the next logic block) to prove the correctness of the program based on that assumption.
Assumes vs. wrapping if-statements
Toy programs often use assume in lieu of wrapping code in an if statement. The following two examples are equivalent:
import org.sireum.logika._
var a : Z = readInt()
assume (a != 0)
l"""{
1. a != 0 premise
}"""
var b: Z = 20 / a
//...is equivalent to:
import org.sireum.logika._
var a : Z = readInt()
var b : Z = 0
if (a != 0) {
b = 20 / a
}
These examples also demonstrate a requirement when we use the division operator in Logika programs, we must first demonstrate that we are not dividing by zero.
Unsatisfiable assume
You will see an error in Logika if your assume statement is not satisfiable. For example:
import org.sireum.logika._
var a: Z = readInt()
assume(a > 0)
assume (a == 0)
If you try verifying this program, you will get a Logika error on the second assume
statement. This is because we already assumed that a
was greater than 0, and it is not possible for a
to also be equal to 0.
Algebra and subst Rules
In this section, we will learn our first two proof rules for programming logic – algebra
and subst
.
Verifying simple programs
Before we delve into our new proof rules, let’s look at the process for verifying simple Logika programs (ones that include user input, variable initialization, and assignment statements using different operations). Here, the // -->
lines are pieces of the verification process that you must add (or consider) in order to prove correctness of your program, and the other lines are the code of the program.
get user input / set initial variable values
// --> add assume statements to specify what must be true about the input
program statement
// --> add logic block to evaluate what has happened in the program
program statement
// --> add logic block to evaluate what has happened in the program
program statement
// --> add logic block to evaluate what has happened in the program
(...more program statements)
// --> add assert statements to express what our program did
We see that if our program involves user input, then we must consider whether our program will only work correctly for certain input values. In that situation, we express our assumptions using assume
statements.
After each program statement, we must add a logic block to evaluate what changed on that line of code. We will see more details on these logic blocks throughout the rest of this chapter. Recall that the syntax for those logic blocks looks like:
l"""{
lineNumber. claim justification
// ... (more claims/justifications)
}"""
Finally, we add one or more assert
statements to express what our program did. These are usually placed at the end of a program, but sometimes we have assert statements throughout the program to describe the progress up to that point.
Algebra justification
The algebra
justification can be used for ANY algebraic manipulation on previous claims. When using this justification, include all relevant proof line numbers in whatever order (you might use as few as zero line numbers or as many as 3+ line numbers).
Example
Consider this example:
import org.sireum.logika._
var x: Z = 6
var y: Z = x
//this assert will not hold yet
assert (y == 6)
Following our process from above, we add logic blocks after each program statement. In these logic blocks, we start by listing the previous program statement as a premise:
import org.sireum.logika._
var x: Z = 6
l"""{
1. x == 6 premise
}"""
var y: Z = x
l"""{
1. y == x premise
//need claim "y == 6" for our assert to hold
}"""
//this assert will not hold yet
assert (y == 6)
For our assert to hold, we must have EXACTLY that claim in a previous logic block – so we know we want our second logic block to include the claim y == 6
.
Here is the program with the second logic block completed – the assert statement will now hold.
import org.sireum.logika._
var x: Z = 6
l"""{
1. x == 6 premise
}"""
var y: Z = x
l"""{
1. y == x premise
2. x == 6 premise //established in a previous logic block, and x is unchanged since then
3. y == 6 algebra 1 2 //we know y is 6 using the claims from lines 1 and 2
}"""
//this assert will hold
assert (y == 6)
We could have also deleted the first logic block in this example. We would still be able to claim x == 6
as a premise in the last logic block, as x
had not changed since being given that value.
subst
We have two deduction rules that involve substitution – subst1
and subst2
. Both of these rules are similar to the find/replace feature in text editors. They preserve truth by replacing one proposition with an equivalent one.
The algebra
justification will work for most mathematical manipulation. However, it will not work for any claim involving ∧
, ∨
, →
, ⊥
, ∀
, ∃
– in those cases, we will be required to use substitution instead.
subst1 justification
Here is the syntax for the subst1
rule. In the example below, line m
must be an equivalence (something equals something else). Line n
can be anything.
l"""{
...
m. LHS_M == RHS_M (some justification)
...
n. LINE_N (some justification)
...
p. (claim) subst1 m n
}"""
(claim1)
rewrites LINE_N
by substituting all ocurrences of LHS_M
(the FIRST side of line m
) with RHS_M
. Here is an example:
l"""{
1. x + 1 == y - 4 (some justification)
2. x*(x + 1) == (x + 1) + y (some justification)
3. x*(y - 4) == (y - 4) + y subst1 1 2
}"""
We wrote line 2 by replacing each occurence of x + 1
with y - 4
.
subst2 justification
Here is the syntax for the subst2
rule. Just as with subst1
, line m
must be an equivalence (something equals something else). Line n
can be anything.
l"""{
...
m. LHS_M == RHS_M (some justification)
...
n. LINE_N (some justification)
...
p. (claim) subst2 m n
}"""
(claim1)
rewrites LINE_N
by substituting all ocurrences of RHS_M
(the SECOND side of line m
) with LHS_M
. Here is an example:
l"""{
1. x + 1 == y (some justification)
2. y*(x + 1) == (x + 1) + y (some justification)
3. (x + 1)*(x + 1) == (x + 1) + (x + 1) subst2 1 2
}"""
We wrote line 2 by replacing each occurence of y
with x + 1
. Note that we put parentheses around our first replacement to ensure a product equivalent to the original statement.
Assignment Statements
Assignment statements in a program come in two forms – with and without mutations. Assignments without mutation are those that give a value to a variable without using the old value of that variable. Assignments with mutation are variable assignments that use the old value of a variable to calculate a value for the variable.
For example, an increment statement like x = x + 1
MUTATES the value of x
by updating its value to be one bigger than it was before. In order to make sense of such a statement, we need to know the previous value of x
.
In contrast, a statement like y = x + 1
assigns to y
one more than the value in x
. We do not need to know the previous value of y
, as we are not using it in the assignment statement. (We do need to know the value of x
).
Assignments without mutation
We have already seen the steps necessary to process assignment statements that do not involve variable mutation. Recall that we can declare as a premise
any assignment statement or claim from a previous logic block involving variables that have not since changed.
For example, suppose we want to verify the following program so the assert statement at the end will hold:
import org.sireum.logika._
val x: Z = 4
val y: Z = x + 2
val z: Z = 10 - x
//the assert will not hold yet
assert(y == z & y == 6)
Since none of the statements involve variable mutation, we can do the verification in a single logic block:
import org.sireum.logika._
val x: Z = 4
val y: Z = x + 2
val z: Z = 10 - x
l"""{
1. x == 4 premise //assignment of unchanged variable
2. y == x + 2 premise //assignment of unchanged variable
3. z == 10 - x premise //assignment of unchanged variable
4. y == 4 + 2 subst1 1 2
5. z == 10 - 4 subst1 1 3
6. y == 6 algebra 4
7. z == 6 algebra 5
8. y == z subst2 7 6
9. y == z ∧ y == 6 ∧i 8 6
}"""
//now the assert will hold
assert(y == z & y == 6)
Note that we did need to do ∧i
so that the last claim was y == z ∧ y == 6
, even though we had previously established the claims y == z
and y == 6
. In order for an assert to hold (at least until we switch Logika modes in chapter 10), we need to have established EXACTLY the claim in the assert in a previous logic block.
Assignments with mutation
Assignments with mutation are trickier – we need to know the old value of a variable in order to reason about its new value. For example, if we have the following program:
import org.sireum.logika._
var x: Z = 4
x = x + 1
//this assert will not hold yet
assert(x == 5)
Then we might try to add the following logic blocks:
import org.sireum.logika._
var x: Z = 4
l"""{
1. x == 4 premise //from previous variable assignment
}"""
x = x + 1
l"""{
1. x == x + 1 premise //NO! Need to distinguish between old x (right side) and new x (left side)
2. x == 4 premise //NO! x has changed since this claim
}"""
//this assert will not hold yet
assert(x == 5)
…but then we get stuck in the second logic block. There, x
is supposed to refer to the CURRENT value of x
(after being incremented), but both our attempted claims are untrue. The current value of x
is not one more than itself (this makes no sense!), and we can tell from reading the code that x
is now 5, not 4.
To help reason about changing variables, Logika has a special name_old
value that refers to the OLD value of a variable called name
, just before the latest update. In the example above, we can use x_old
in the second logic block to refer to x
’s value just before it was incremented. We can now change our premises and finish the verification as follows:
import org.sireum.logika._
var x: Z = 4
l"""{
1. x == 4 premise //from previous variable assignment
}"""
x = x + 1
l"""{
1. x == x_old + 1 premise //Yes! x equals its old value plus 1
2. x_old == 4 premise //Yes! The old value of x was 4
3. x == 4 + 1 subst1 2 1
4. x == 5 algebra 3 //Could have skipped line 3 and used "algebra 1 2" instead
}"""
//now the assert will hold
assert(x == 5)
By the end of the logic block following a variable mutation, we need to express everything we know about the variable’s current value WITHOUT using the _old
terminology, as its scope will end when the logic block ends. Moreover, we only ever have one _old
value available in a logic block – the variable that was most recently changed. This means we will need logic blocks after each variable mutation to process the changes to any related facts.
Variable swap example
Suppose we have the following Logika program:
import org.sireum.logika._
var x: Z = readInt()
var y: Z = readInt()
val temp: Z = x
x = y
y = temp
//what do we want to assert we did?
We can see that this program gets two user input values, x
and y
, and then swaps their values. So if x
was originally 4 and y
was originally 6, then at the end of the program x
would be 6 and y
would be 4.
We would like to be able to assert what we did – that x
now has the original value from y
, and that y
now has the original value from x
. To do this, we might invent dummy constants called xOrig
and yOrig
that represent the original values of those variables. Then we can add our assert:
import org.sireum.logika._
var x: Z = readInt()
var y: Z = readInt()
//the original values of both inputs
val xOrig: Z = x
val yOrig: Z = y
val temp: Z = x
x = y
y = temp
//x and y have swapped
//x has y's original value, and y has x's original value
assert(x == yOrig & y == xOrig) //this assert will not yet hold
We can complete the verification by adding logic blocks after assignment statements, being careful to update all we know (without using the _old
value) by the end of each block:
import org.sireum.logika._
var x: Z = readInt()
var y: Z = readInt()
//the original values of both inputs
val xOrig: Z = x
val yOrig: Z = y
l"""{
1. xOrig == x premise
2. yOrig == y premise
}"""
//swap x and y
val temp: Z = x
x = y
l"""{
1. x == y premise //from the assignment statement
2. temp == x_old premise //temp equaled the OLD value of x
3. xOrig == x_old premise //xOrig equaled the OLD value of x
4. yOrig == y premise //yOrig still equals y
5. temp == xOrig algebra 2 3
6. x == yOrig algebra 1 4
}"""
y = temp
l"""{
1. y == temp premise //from the assignment statement
2. temp == xOrig premise //from the previous logic block (temp and xOrig are unchanged since then)
3. yOrig == y_old premise //yOrig equaled the OLD value of y
4. y == xOrig algebra 1 2
5. x == yOrig premise //from the previous logic block (x and yOrig are unchanged since then)
6. x == yOrig ^ y == xOrig ^i 5 4
}"""
//x and y have swapped
//x has y's original value, and y has x's original value
assert(x == yOrig & y == xOrig) //this assert will hold now
Notice that in each logic block, we express as much as we can about all variables/values in the program. In the first logic block, even though xOrig
and yOrig
were not used in the previous assignment statement, we still expressed how the current values our other variables compared to xOrig
and yOrig
. It helps to think about what you are trying to claim in the final assert – since our assert involved xOrig
and yOrig
, we needed to relate the current values of our variables to those values as we progressed through the program.
Integer Division and Modulo
Working with division and modulo requires extra care, as Logika is finicky about both.
Division
Recall that Z
(int) is the only numeric type in Logika, so any division is integer division. This means something like 9/2
evaluates to 4, just as it would in Java or C#.
Check for division by zero
Before doing division of the form numerator/denominator
, either in a line of code or in a logic block, you must have a line in a previous logic block that states: denominator != 0
. Other forms, such as denominator > 0
or 0 != denominator
, will not work. You are even required to do this when dividing by a constant value that is obviously not zero.
For example, if we do:
import org.sireum.logika._
var x: Z = 10 / 2
Then Logika will give us an error complaining we have not proved that the denominator is not zero. We must add the claim 2 != 0
as follows:
import org.sireum.logika._
l"""{
1. 2 != 0 algebra
}"""
var x: Z = 10 / 2
Note that our justification is just, algebra
, as we don’t need any extra information to claim that 2 is not equal to 0.
Pitfalls
Be careful when making claims that involve division. For example, the following claim will not validate in Logika:
l"""{
1. x == (x/3)*3 algebra //NO!
}"""
While the claim x == (x/3)*3
is certainly true in math, it is not true with integer division. For example, if x
is 7, then (x/3)*3
is 6 – so the two sides are not equal. In general, I recommend avoiding claims involving division if you can at all help it. Instead, try to find a way to express the same idea in a different way using multiplication.
Modulo
Modulo (%) works the same way in Logika as it does in other programming languages. For example, 20 % 6
evaluates to 2.
Modulo checks on numerator and denominator
Before using the modulo operator in the form numerator % denominator
, either in a line of code or as a claim in a logic block, you must have EXACTLY these claims in the previous logic block:
numerator >= 0
denominator > 0
For example:
...
l"""{
1. 2 > 0 algebra
2. a >= 0 (some justification)
}"""
x = a % 2
...
Example
Consider the following Logika program:
import org.sireum.logika._
var num: Z = readInt("Enter positive number: ")
assume(num > 0)
val orig: Z = num
num = num * 2
assert(num % 2 == 0)
num = num + 2
assert(num % 2 == 0)
num = num/2 - 1
assert(num == orig)
It can often be handy to walk through a program with sample numbers before trying to prove its correctness:
- Suppose our input value,
num
, is 11
orig
is initialized to be 11 also
num
is multiplied by 2, and is 22
- It makes sense that
num
would be even, since any number times two is always even (and indeed, 22 is even)
- We add 2 to
num
, so it is now 24
- It makes sense that
num
would still be even, as it was even before and we added 2 (another even number) to it. Indeed, 24 is still even.
- We update
num
by dividing it by 2 and subtracting 1, so num
is now back to its original value of 11 (the same as orig
). This step “undoes” the changes we have made to num
– looking at the code, we can see that the final value of num
is orig*2 + 2
, so if we do (orig*2 + 2) / 2 - 1
, we are left with orig
.
Here is the completed verification, with comments at each step:
import org.sireum.logika._
var num: Z = readInt("Enter positive number: ")
assume(num > 0)
val orig: Z = num
num = num * 2
l"""{
1. num == num_old * 2 premise //we updated num to be its old value times 2
2. orig == num_old premise //orig equaled the old value of num (before our change)
3. num == orig * 2 algebra 1 2 //express the new value of num without referring to "_old"
4. 2 > 0 algebra //needed to use modulo in step 7
5. num_old > 0 premise //we assumed the old value of num (before its change) was > 0
6. num >= 0 algebra 1 5 //needed to use modulo in step 7
7. num % 2 == 0 algebra 1 //we have showed num is now even (needed for next assert)
}"""
assert(num % 2 == 0)
num = num + 2
l"""{
1. num == num_old + 2 premise //we updated num to be its old value plus 2
2. num_old >= 0 premise //from line 6 in previous block, but num has since changed
3. num_old % 2 == 0 premise //from line 7 in previous block, but num has since changed
//we know 2 > 0 from previous block - don't need to restate here
4. num >= 0 algebra 1 2 //needed to use modulo in step 5 (need to redo since num has changed)
5. num % 2 == 0 algebra 1 3 //we have showed num is still even (needed for next assert)
6. num_old == orig * 2 premise //from line 3 in block above, but num has since changed
7. num - 2 == orig * 2 algebra 1 6 //express new value of num without using "_old"
}"""
assert(num % 2 == 0)
l"""{
1. 2 != 0 algebra //needed for dividing by 2
}"""
num = num/2 - 1
l"""{
1. num == num_old/2 - 1 premise //we updated num to be its old value divided by 2 minus 1
2. num_old - 2 == orig * 2 premise //from line 7 in previous block, but num has since changed
3. num_old == orig * 2 + 2 algebra 2
4. num == (orig * 2 + 2)/2 - 1 algebra 1 3 //express new value of num without using "_old"
5. num == orig + 1 - 1 algebra 4
6. num == orig algebra 5 //we have showed num is back to being orig (needed for last assert)
//could have skipped straight here with "algebra 1 2"
assert(num == orig)
Sometimes it can be tricky to figure out what to do at each step in order to get assert statements to pass. If you are unsure what to do, I recommend:
-
Walk through the program several times with sample numbers, keeping track of changes to variables. Why do the assert statements make sense to you? Convince yourself that they are valid claims before you worry about the formal verification.
-
Add a logic block after each variable mutation. Work from the top down:
- Write a premise for every variable assignment and assume statement since the previous logic block.
- Find all claims in the logic block just before you that do not use an “_old” reference – pull each claim into your current block, using an “_old” reference as needed for the most recently changed variable.
- Manipulate your claims that use an “_old” reference until you have statements that capture the current value of the recently changed variable that do not reference “_old”
- If your next statement is an assert, manipulate your claims until you have exactly the claim in the assert.
- If any claims
-
Add a logic block before each use of division (numerator / denominator
) and modulus (numerator % denominator
). Pull in claims from previous blocks as described above to help you show what is needed about numerator
and denominator
:
- For division, build to the claim
denominator != 0
- For modulo, build to the claims
numerator >= 0
and denominator > 0
- If you can, avoid using division in logic block claims
Conditional Statements
To deduce the knowledge generated by a conditional (if/else) statement, we must analyze both branches. This is because some executions will follow the if-branch and some will follow the else-branch. Recall that Logika does not have an if/else if statement. However, you can simulate more than two branches by nesting another if/else inside the outer else.
Motivation
Before we formalize the details of verifying a program with a conditional statement, let’s motivate the topic with an example.
Max program
Suppose we have a Logika program that finds finds the maximum (max
) between two user input numbers (x
and y
):
import org.sireum.logika._
val x: Z = readInt()
val y: Z = readInt()
var max: Z = 0 //give max a dummy starting value
if (x > y) {
max = x
} else {
max = y
}
Max assert
Before worrying about how to do the verification, let’s consider what we should assert at the end in order to be sure that max
really does hold the biggest of the two inputs. Clearly, max
should be greater than or equal to both inputs. So should our assert be:
//Not quite all we want to say
assert(max >= x & max >= y)
Suppose x
was 10, y
was 15…and that max
was 20. (Clearly this isn’t what our code would do, but you can imagine writing something else for the max code that came up with such a calculation). In this case, max
is indeed greater than or equal to both inputs…but it is just as clearly not the max. We know see that we also need to claim that max
equals one of the two inputs. This makes our assert:
//Now we are sure we are describing the max between x and y
assert(max >= x & max >= y & (max == x | max == y))
Analyzing max
Now, we need to prove that our assert holds no matter which branch we follow in the conditional statement. First, when we analyze the code in the if-branch, we have:
max = x
l"""{
1. max == x premise
}"""
and when we analyze the code in the else-branch, we have:
max = y
l"""{
1. max == y premise
}"""
These two deductions imply that, when the if/else statements finishes, one or the other property holds true:
if (x > y) {
max = x
l"""{
1. max == x premise
}"""
} else {
max = y
l"""{
1. max == y premise
}"""
}
l"""{
//max == x in the IF, max == y in the ELSE
1 . max == x v max == y premise
}"""
This illustrates the first principle of conditional commands: the knowledge produced by the command is the disjunction (or) of the knowledge produced by each branch. In the section on propositional logic, we covered how to apply cases analyses on disjunctive assertions to extract useful knowledge.
Recall that the intent of the if/else statement was to set max
so that it holds the larger of x
and y
, so that our assert would hold:
assert(max >= x & max >= y & (max == x | max == y))
The claim we proved so far satisifies the second part of our assert statement, but not the first part. This is because we ignored a critical feature of an if/else statement: By asking a question — the condition — the if/else statement generates new knowledge.
For the if-branch, we have the new knowledge that x > y
; for the else-branch, we have that ¬(x > y)
, that is, y >= x
. We can embed these assertions into the analysis of the conditional command, like this, and conclude that, in both cases, max
is greater than or equal to both inputs:
import org.sireum.logika._
var x: Z = readInt()
var y: Z = readInt()
var max: Z = 0
if (x > y) {
l"""{
2. x > y premise //the condition is true
}"""
max = x
l"""{
1. max == x premise //from the "max = x" assignment
2. max >= x algebra 1
3. x > y premise //condition is still true (x and y are unchanged)
4. max >= y algebra 1 3
}"""
} else {
l"""{
2. ¬(x > y) premise //the condition is NOT true
3. x <= y algebra 2
}"""
max = y
l"""{
1. max == y premise //from the "max = y" assignment
2. x <= y premise //pulled down from previous logic block (x and y are unchanged)
3. max >= x algebra 1 2
4. max >= y algebra 1
}"""
}
//summary of what just happened
l"""{
//max == x in the IF, max == y in the ELSE
1. max == x ∨ max == y premise
2. max >= x premise //true in BOTH branches
3. max >= y premise //true in BOTH branches
4. max >= x ∧ max >= y ^i 2 3
5. (max >= x ∧ max >= y) ∧ (max == x ∨ max == y) ^i 4 1
}"""
assert((max >= x & max >= y) & (max == x | max == y))
Rules for analyzing programs with conditionals
In this section, we will summarize how to analyze programs with conditional statements.
Declaring condition and ¬(condition) as premises
If we have a program such as this with an if/else statement:
Then we can claim C
as a premise immediately inside the if-branch and ¬(C)
as a premise immediately inside the else branch:
if (C) {
l"""{
1. C premise
}"""
} else {
l"""{
1. ¬(C) premise
}"""
}
Be careful with the else
case – you must claim exactly ¬(C)
, and not some claim you know to be equivalent. In our max example, C
was x < y
, and we needed to claim exactly ¬(x < y)
in the else – NOT x >= y
. After you have pulled in the initial claim using the form ¬(C)
, you can use algebra
to manipulate it into a different form.
Each branch reaches a different conclusion
If the if-branch reaches conclusion Q1
and the else branch reaches conclusion Q2
, then afterwards we can list as a premise that one of those conclusions is true (since we know that one of the branches in an if/else will ALWAYS execute):
if (C) {
...
l"""{
...
1. Q1 (some justification) //conclusion in if-branch
}"""
} else {
l"""{
...
1. Q2 (some justification) //conclusion in else-branch
}"""
}
l"""{
1. Q1 ∨ Q2 premise //Q1 from if, Q2 from else
}"""
Note that order matters, and that we must claim (if conclusion) ∨ (else conclusion)
– in the example above, we could not claim Q2 ∨ Q1
afterwards.
Each branch reaches the same conclusion
If the if-branch and the else-branch both reach the SAME conclusion Q
, then afterwards we can list Q
as a premise. Here, we know that one of the branches in an if/else will ALWAYS execute – so if we get to the same conclusion in both cases, then we must always reach that conclusion:
if (C) {
...
l"""{
...
1. Q (some justification) //common conclusion reached in IF
}"""
} else {
l"""{
...
1. Q (some justification) //common conclusion reached in ELSE
}"""
}
l"""{
1. Q premise //Q was true in both the IF and the ELSE
}"""
Example: programs with no “else”
Suppose we have an if-statement with no “else”. Afterwards, we know that either the if-branch was executed (which happens when the condition is true), or that the condition was false. If we reach some conclusion Q
in the if-branch, then afterwards we know that either Q
is true or that the if-statemention condition is not true. Similarly, if some claim old
was true before an if-statement without an else, then afterwards we know that we either reached Q
in the if-branch or that our old
statement is still true.
We can pull in these claims as premises after an if-statement as follows:
l"""{
1. old (some justification)
}"""
if (C) {
...
l"""{
...
1. Q (some justification) //conclusion in if-branch
}"""
}
l"""{
1. Q ∨ ¬(C) premise //either we reached Q in the if, or the condition was false
2. Q ∨ old premise //either we reached Q in the if, or "old" is still true
}"""
For example, suppose we have a program that finds the absolute value of an input number:
import org.sireum.logika._
var num: Z = readInt()
var orig: Z = num
if (num < 0) {
num = num * -1
}
//num is now the absolute value of the original input
Afterwards, we want to assert that num
is the absolute value of the original input – that num
is nonnegative and either equals orig
(the original input) or -1*orig
:
assert(num >= 0 & (num == -1*orig | num == orig))
We start our verification as follows:
import org.sireum.logika._
var num: Z = readInt()
val orig: Z = num
l"""{
1. orig == num premise //from "orig = num" assignment
2. num == orig algebra 1 //switch order to match assert
}"""
if (num < 0) {
num = num * -1
l"""{
1. num_old < 0 premise //if condition (num just changed)
2. num == num_old * -1 premise //from "num = num * -1" assignment
3. orig == num_old premise //orig did equal num (num just changed)
4. num >= 0 algebra 1 2 //a negative number times -1 is nonnegative
5. num == -1*orig algebra 2 3 //needed for last part of assert
}"""
}
l"""{
1. num >= 0 ∨ ¬(num < 0) premise //conclusion from if OR !(condition)
2. num == -1*orig ∨ num == orig premise //conclusion from if OR num still equals orig
//incomplete
}"""
//num is now nonnegative
assert(num >= 0 & (num == orig | num == -1*orig))
In our final logic block, we need to reach the claim num >= 0
so we can combine it with num == -1*orig ∨ num == orig
to match our assert. We can do this with OR elimination on num >= 0 ∨ ¬(num < 0)
:
import org.sireum.logika._
var num: Z = readInt()
val orig: Z = num
l"""{
1. orig == num premise //from "orig = num" assignment
2. num == orig algebra 1 //switch order to match assert
}"""
if (num < 0) {
num = num * -1
l"""{
1. num_old < 0 premise //if condition (num just changed)
2. num == num_old * -1 premise //from "num = num * -1" assignment
3. orig == num_old premise //orig did equal num (num just changed)
4. num >= 0 algebra 1 2 //a negative number times -1 is nonnegative
5. num == -1*orig algebra 2 3 //needed for last part of assert
}"""
}
l"""{
1. num >= 0 ∨ ¬(num < 0) premise //conclusion from if OR !(condition)
2. num == -1*orig ∨ num == orig premise //conclusion from if OR num still equals orig
3. {
4. num >= 0 assume
}
5. {
6. ¬(num < 0) assume
7. num >= 0 algebra 6
}
8. num >= 0 ∨e 1 3 5
9. num >= 0 ∧ (num == -1*orig ∨ num == orig) ∧i 8 2 //match assert
}"""
//num is now nonnegative
assert(num >= 0 & (num == -1*orig | num == orig))
Nested conditionals
We employ the same rules when analyzing programs with nested conditional statements. If we reach a common conclusion in both the if and else branches of an inner if/else statement, for example, then we can claim the common conclusion as a premise after that inner if/else statement (but still inside the outer if/else). The outline below summarizes what we can claim at various places in nested if/else statement:
if (C1) {
if (C2) {
l"""{
1. C1 premise //outer if condition is true
2. C2 premise //inner if condition is true
}"""
...
l"""{
1. common (some justification)
2. Q1 (some justification)
}"""
}
else {
l"""{
1. C1 premise //outer if condition is true
2. ¬(C2) premise //inner if condition is false
}"""
...
l"""{
1. common (some justification)
2. Q2 (some justification)
}"""
}
l"""{
1. common premise //common conclusion in inner if/else
2. Q1 ∨ Q2 premise //Q1 from inner if, Q2 from inner else
}"""
} else {
if (C3) {
l"""{
1. ¬(C1) premise //outer if condition is false
2. C3 premise //inner if condition is true
}"""
...
l"""{
1. common (some justification)
2. Q3 (some justification)
}"""
}
else {
l"""{
1. ¬(C1) premise //outer if condition is false
2. ¬(C3) premise //inner if condition is false
}"""
...
l"""{
1. common (some justification)
2. Q4 (some justification)
}"""
}
l"""{
1. common premise //common conclusion in inner if/else
2. Q3 ∨ Q4 premise //Q1 from inner if, Q2 from inner else
}"""
}
l"""{
1. common premise //"common" was true in both the outer IF and the outer ELSE
2. (Q1 ∨ Q2) ∨ (Q3 ∨ Q4) premise //(Q1 ∨ Q2) from outer if, (Q3 ∨ Q4) from else
}"""