Logika: Programming Logics
6. Loop, Invariants, Induction

6. Loops and Loop Invariants

At the end of the last chapter we discussed how a conditional branching creates two “paths” for the logic to flow thru and they are combined in a disjunction after the condition. A loop is a command that restarts itself over and over while the loop condition remains true.

In order to analyze loop behavior we will need to know: what is true before the loop executes, what is done inside the loop and what true after the loop, regardless of how many times the loop executes. At the end of the loop, the conjunction of the negation of the loop-condition and property called the loop-invariant are true.

The loop’s invariant is a statement about what is true before you begin the body of the loop and after iteration of the loop-body. Put another way, the loop-invariant must be true each time you go to check the loop condition; it is true the first time you check to see if you go into the loop, and still still true the last time you check and “skip” the loop.

../../_images/06-image-10.png

6.1. While Loops

Consider the factorial n!. We can understand factorial as a repeated product: for integer n > 0:

n! == 1 * 2 * 3 * ...up to... * n

It is easy to write a loop program that computes the repeated product:

 1import org.sireum.logika._
 2val n: Z = readInt("Type a nonnegative int: ")
 3assume( n >= 0)
 4var i : Z = 0
 5var fac: Z  = 1
 6
 7while (i < n) {
 8l"""{
 9    modifies i, fac
10    }"""
11    i = i + 1
12    fac = fac * i
13    }
14

The loop adjoins the multiplications, *1, *2, *3, etc., to the running total, fac, until the loop reaches i == n. Consider some execution cases:

n == 0: the loop repeats 0 times: it computes  fac == 1  ==  0!
n == 1: the loop repeats 1 time:  it computes  fac == 1 * 1  ==  1!
n == 2: the loop repeats 2 times: it computes  fac == (1 * 1) * 2  ==  2!
n == 3: the loop repeats 3 times: it computes  fac == (1 * 1 * 2) * 3  ==  3!
n == 4: the loop repeats 4 times: it computes  fac == (1 * 1 * 2 * 3) * 4  ==  4!
. . .
the loop repeats k+1 times: it computes  fac == (k!) * (k+1)  ==  (k+1)!

For input, n == 4, the loop computes correctly 4! in 4 iterations because the loop computes correctly 3! in 3 iterations – the fourth loop iteration builds upon the work of the previous three. This is the standard use of a loop: each loop iteration builds on the previous iterations to move one step closer to the final goal.

We should always ask this crucial question about every loop we write:

Say that the loop has been running for some iterations; what has it accomplished so far?

For the factorial example, the response is: “after i iterations, variable fac has the value, i!, that is, fac == i!.”

This answer is important, because it reveals the strategy the loop uses to reach its goal in stages: as i counts upwards towards n, fac == i! repeatedly holds true. Because the loop stops when i == n, this means the loop will achieve its goal: fac == n!.

The answer to the “crucial question” listed above is the loop’s invariant property. Now, why is it an invariant?

This is because the output from the loop’s prior iterations flows into the loop’s next iteration. So, whatever is accomplished looks the same, iteration after iteration after iteration. This is a logical property that can be proved.

Note

Logika Programming fact

A fact statement can be used to introduce global (named) axioms and (pure) proof functions (whose behaviors are specified via axioms) to help with specifying contracts. It is important to note that, as the name implies, Logika treats these axioms as facts–if you make an error in the definition, Logika will dutifully enforce that error everywhere in the program.

1import org.sireum.logika._
2l"""{
3  fact // axioms
4    def factorial(n: Z): Z
5    fZero. factorial(0) == 1
6    fPos.  ∀x: Z  x > 0 → factorial(x) == factorial(x - 1) * x
7}"""

Lets go through this quickly. In this class, facts will be given to you, you are only expected to know how to use them.

On line 4 we see def factorial(n:Z): Z. This tells you that the fact is named factorial, it has one named argument n, which is an integer, and that factorial itself evaluates to an integer (think of this as a return type).

Line 5 is the first rule, fZero. factorial(0) == 1. This rule, used as a justification, allows you to state factorial(0) == 1 as a claim. Using this new claim, you can prove additional things through substitution. Be sure you understand the fPos rule. This is a syntactical form and semantical meaning you will see often in programming logic.

We deduce the invariant property on the factorial-loop body like this:

 1import org.sireum.logika._
 2
 3l"""{
 4  fact // axioms
 5    def factorial(n: Z): Z
 6    fZero. factorial(0) = 1
 7    fPos.  ∀x: Z  x > 0 → factorial(x) = factorial(x - 1) * x
 8}"""
 9
10
11val n: Z = 3
12var i: Z = 0
13var fac: Z = 1
14l"""{
15      1. i == 0              premise
16      2. i >= 0              algebra 1
17      3. fac == 1            premise
18      4. factorial(0) == 1   fact fZero
19      5. factorial(i) == 1   subst2 1 4
20      6. fac = factorial(i)  algebra 3 5
21      }"""
22// begin -- fac == factorial(i)
23
24    //first "update"
25    i = i + 1
26      l"""{
27            1. i_old == 0              premise
28            2. i = i_old + 1           premise
29            3. i_old >= 0              premise
30            4. i > 0                   algebra 1 2 3
31            5. fac = factorial(i_old)  premise
32            6. fac = factorial(i-1)    algebra 5 2
33      }"""
34
35    fac = fac * i
36     l"""{
37          1. fac_old = factorial(i-1)     premise
38          2. fac = fac_old * i            premise
39          3. i > 0                        premise
40          4. ∀x: Z  x > 0 → factorial(x) =
41                 factorial(x - 1) * x     fact fPos
42          5. i > 0 → factorial(i) =
43                 factorial(i - 1) * i     ∀e 4 i
44          6. factorial(i) =
45               factorial(i - 1) * i       →e 5 3
46          7. factorial(i) =
47               fac_old * i                algebra 6 1
48          8. fac = factorial(i)           algebra 7 2
49     }"""
50    // End first update -- fac == factorial(i)
51
52         //second "update"
53         i = i + 1
54           l"""{
55                 1. i_old > 0              premise
56                 2. i = i_old + 1           premise
57                 3. i_old >= 0              premise
58                 4. i > 0                   algebra 1 2 3
59                 5. fac = factorial(i_old)  premise
60                 6. fac = factorial(i-1)    algebra 5 2
61           }"""
62
63         fac = fac * i
64          l"""{
65               1. fac_old = factorial(i-1)     premise
66               2. fac = fac_old * i            premise
67               3. i > 0                        premise
68               4. ∀x: Z  x > 0 → factorial(x) =
69                      factorial(x - 1) * x     fact fPos
70                5. i > 0 → factorial(i) =
71                       factorial(i - 1) * i     ∀e 4 i
72                6. factorial(i) =
73                     factorial(i - 1) * i       →e 5 3
74                7. factorial(i) =
75                     fac_old * i                algebra 6 1
76                8. fac = factorial(i)           algebra 7 2
77          }"""
78         // End second update -- fac == factorial(i)
79
80            // Third update is exact copy of second

This little example is just the loop repeated twice. What we see is both at the begining and after each update, fac == factorial(i). Notice how the knowledge generated by completing one iteration “feeds into” the next iteration. And, after each iteration, that knowledge is that fac == factorial(i).

A working Logika program would look something like

 1import org.sireum.logika._
 2l"""{
 3  fact // axioms
 4    def factorial(n: Z): Z
 5    fZero. factorial(0) == 1
 6    fPos.  ∀x: Z  x > 0 → factorial(x) == factorial(x - 1) * x
 7}"""
 8
 9val n: Z = readInt("Type a nonnegative int: ")
10assume( n >= 0)
11var i : Z = 0
12var fac: Z  = 1
13l"""{
14     1. i == 0             premise
15     2. fac == 1           premise
16     3. factorial(0) == 1   fact fZero
17     4. factorial(i) == fac algebra 1 2 3
18     5. i >= 0              algebra 1
19    }"""
20
21while (i <= n) {
22l"""{
23     invariant factorial(i) = fac
24               i >= 0
25     modifies i, fac
26}"""
27l"""{
28      1. factorial(i) == fac premise
29}"""
30    i = i + 1
31    l"""{
32        0. i_old >= 0                       premise
33        1. factorial(i_old) == fac          premise
34        2. i = i_old + 1                    premise
35        3. factorial(i - 1) == fac          algebra 1 2
36        4. i > 0                            algebra 0 2
37    }"""
38    fac = fac * i
39    l"""{
40          1. factorial(i - 1) == fac_old    premise
41          2. fac == fac_old * i             premise
42          3. fac == factorial(i - 1) * i    algebra 1 2
43          4. ∀x: Z  x > 0 → factorial(x) =
44                    factorial(x - 1) * x    fact fPos
45          5. i>0 → factorial(i) =
46                 factorial(i - 1) * i       ∀e 4 i
47          6. i > 0                          premise
48          7. factorial(i) =
49                  factorial(i - 1) * i      →e 5 6
50          8. fac = factorial(i)             algebra 3 7
51        }"""
52}

Pay particular attention to lines 17 and 18, where we prove the invariant is true before entering the loop the first time. Similarly, lines 47 and 50 re-establish the invariant before we try to loop again. Thus, we prove the loop invariant holds each time we check the loop-condition and possibly proceed through the loop-body.

Of course, on each loop the loop counter, i, gets larger by one, meaning that we are closer to achieving the final goal, but the changing values of fac and i ensure that fac == factorial(i) remains true: “after i iterations, fac has value factorial(i).”

The invariant property documents the loop’s structure – how the loop achieves its goal.

As exciting as that discussion was, the loop-invariant is a means to an end, not the end itself. What is it we are trying to achieve? In this example, we would like to be able to prove that fac == n-factorial. As facts cannot be used in assertions, what we will really assert is i == n, and then we know from our invariant that fac == n-factorial. Consider:

 1import org.sireum.logika._
 2l"""{
 3  fact // axioms
 4    def factorial(n: Z): Z
 5    fZero. factorial(0) == 1
 6    fPos.  ∀x: Z  x > 0 → factorial(x) == factorial(x - 1) * x
 7}"""
 8// omitting  manual proofs through end of loop
 9val n: Z = readInt("Type a nonnegative int: ")
10assume( n >= 0)
11var i : Z = 0
12var fac: Z  = 1
13
14while (i < n) {
15l"""{
16     invariant factorial(i) = fac
17               i >= 0
18     modifies i, fac
19}"""
20
21    i = i + 1
22    fac = fac * i
23}
24l"""{
25     1. ¬ (i < n)            premise  //negation of loop condition
26     2. i >= 0               premise  // invariant
27     3. factorial(i) = fac   premise  // invariant
28
29     //. i = n               some justification but probably algebra or subst
30}"""
31assert( i == n)

For certain, when the loop quits after i iterations, we know that the invariant holds. But we also know that the loop’s test has gone false, that is, ~(i < n), that is, i >=n. However these facts are not enough to prove i == n. In this case we say our invariant is too weak, it theoretically admits too many possibilities.

The solution is to strengthen the invariant, make is so it allows fewer possibilities after the loop. In particular, we will want to strengthen the invariant so the only possible value of i is n. We can do this by adding a condition to the invariant such that values of i > n are not allowed.

 1import org.sireum.logika._
 2l"""{
 3  fact // axioms
 4    def factorial(n: Z): Z
 5    fZero. factorial(0) == 1
 6    fPos.  ∀x: Z  x > 0 → factorial(x) == factorial(x - 1) * x
 7}"""
 8// omitting  manual proofs through end of loop
 9val n: Z = readInt("Type a nonnegative int: ")
10assume( n >= 0)
11var i : Z = 0
12var fac: Z  = 1
13
14while (i < n) {
15l"""{
16     invariant factorial(i) = fac
17               i >= 0
18               i <= n
19     modifies i, fac
20}"""
21
22    i = i + 1
23    fac = fac * i
24}
25l"""{
26     1. ¬ (i < n)            premise  //negation of loop condition
27     2. i >= 0               premise  // invariant
28     3. factorial(i) = fac   premise  // invariant
29     4. i <= n               premise  // invariant
30     5. i >= n               algebra 1
31     6. i = n                algebra 4 5
32}"""
33assert( i == n)

The added condition to the invariant at line 18 does the trick. Updating the proof sections so it will verify is left for you as an optional exercise.

Here is a summary of the main points just made:

  • A loop is a function that repeatedly calls itself. (It is a tail-recursive function.)

  • The loop’s invariant states a strategy for accomplishing a goal:

    the loop has been running for awhile; what has it accomplished so far?

  • The loop’s invariant is exactly the precondition for executing the loop’s body, and it is exactly the postcondition of what is generated by executing the loop’s body.

  • When the loop terminates, the falsity of the termination test coupled with the invariant should imply that the loop has achieved its goal.

Even if you forget all about algebra and proofs, whenever you write a loop, document the loop with its invariant stated in words. If you are unable to state in words the invariant, then you don’t understand yourself what your loop is doing.

6.1.1. Law for While-Loops (Claim Transformation)

Here is the law we use for deducing the properties of a while-loop. It uses an invariant assertion, I:

"""{
   ...
   I
}"""  // we must prove this True before the loop is entered
      // because it must be true before checking B
while (B) {
  l"""{
       invariant  I
       modifies  VARLIST   // the variables updated in  C
  }"""
  l"""{
        1. B   premise     // inside loop, so B must be true
        2. I   premise     // the premises for the loop's body
       ...
  }"""
  C
  l"""{
       ...
       I      // we must prove  I  at the end of the body
              // because it must be true before re-checking B
  }"""
}  // end loop

l"""{
      1. ~B    premise
      2. I     premise
      ...
}"""

That is, to deduce the knowledge produced by a while-loop (when we do not know in advance how many times the loop will iterate), we must deduce an invariant I that:

  1. is proved true before the first time we check the loop condition

  2. is proved true at the end of the loop’s body

Then, no matter how many times (0,1,2,…) the loop repeats, we know that I must hold true when the loop stops. We also know ~B holds when the loop stops.

../../_images/06-image-20.png

Because the loop will update some variables in its body, we must know these variables’ names, so that any premises other than the loop invariant that enter the loop body that mention these variables are cancelled.

A challenge lies in formulating the appropriate invariant that states what the loop is accomplishing while it repeats. Invariant discovery is an art form; there cannot exist a mechanical algorithm to do this. (This is a key result of computability theory, the study of what problems are mechanically solvable.) So, we now study how to discover loop invariants.

6.2. Examples of Invariant Discovery

A key intellectual task in programming is stating a loop’s invariant. The invariant tells us “what the loop is doing” as it iterates – progresses in small steps – towards its goal.

IMPORTANT: Saying what “the loop is doing” is different from saying what the loop “will do” before it starts or what the loop “has done” after it has finished. We must ask the crucial question:

Say that the loop has been running for a while – what has it accomplished so far?

The answer to this question, whether stated in English or algebra, is the invariant, the loop’s true meaning.

Following are some examples of invariant discovery.

6.2.1. Multiplication as Repeated Addition

Most people forget that the Greeks and Arabs intended multiplication to be just repeated addition. So, what does this program calculate for z when it finishes? What is the loop’s invariant? (How does the loop reach its goal?)

 1import org.sireum.logika._
 2
 3val x: Z = readInt("Type an int: ")
 4val y: Z = readInt("Type another: ")
 5var z: Z = 0
 6var count: Z = 0
 7
 8while (count != x) {
 9    l"""{
10         modifies z, count
11    }"""
12    println ("(a) x =", x, " y =", y, " count =", count, " z =", z)
13    z = z + y
14    count = count + 1
15}
16println ("(b) x =", x, " y =", y, " count =", count, " z =", z)
17assert (x * y == z)

Apparently, this program computes x * y and saves it in z. To understand, we execute a test case and watch what is printed:

Type an int: 3
Type another: 4
(a) x = 3  y = 4  count = 0  z = 0
(a) x = 3  y = 4  count = 1  z = 4
(a) x = 3  y = 4  count = 2  z = 8
(b) x = 3  y = 4  count = 3  z = 12

The trace information in each row shows this pattern between the values of the variables:

count * y  == z

This is what the loop is doing — what it means — z holds the value of count * y. Because the loop stops when count == x, we conclude that z == x * y.

We can apply logic laws to prove that count * y == z count <= x is invariant for the loop’s body:

 1import org.sireum.logika._
 2
 3val x: Z = readInt("Type a positive int: ")
 4assume(x >=0)
 5val y: Z = readInt("Type any int : ")
 6var z: Z = 0
 7var count: Z = 0
 8l"""{
 9    1. z == 0         premise
10    2. count = 0      premise
11    3. x >= 0         premise
12    4. z = count * y  algebra 1 2
13    5. count <= x     algebra 2 3
14}"""
15
16while (count != x) {
17    l"""{
18         invariant z = count * y
19                   count <= x
20         modifies z, count
21    }"""
22    println ("(a) x =", x, " y =", y, " count =", count, " z =", z)
23    z = z + y
24    l"""{
25        1. z = z_old + y        premise
26        2. z_old = count * y    premise
27        3. z = y * (count +1)   algebra 1 2
28    }"""
29    count = count + 1
30    l"""{
31        1. z = y * ( count_old +1)  premise
32        2. count = count_old + 1    premise
33        3. count_old <= x           premise
34        4. count_old != x           premise
35        5. count <= x               algebra 2 3 4
36        6. z = count * y            algebra  1 2
37    }"""
38}
39l"""{
40     1. count <= x     premise
41     2. ¬(count != x)  premise
42     3. count = x      algebra 1 2
43     4. z = count * y  premise
44     5. x * y = z      algebra 3 4
45}"""
46println ("(b) x =", x, " y =", y, " count =", count, " z =", z)
47
48assert( x * y == z)

6.2.2. Division as Repeated Subtraction

Maybe you remember that division was invented to represent repeated subtraction, e.g., “how many times can you subtract 4 from 20? (5) — 4 goes into 20 five times — 20 divided by 4 is 5 (with remainder 0)”.

Here is the program that does repeated subtraction like division is meant to do:

 1import org.sireum.logika._
 2
 3var n: Z = readInt("Type an nonegative int: ")
 4var d: Z = readInt("Type a positive int: ")
 5assume (n >= 0  &  d > 0)
 6var q: Z = 0
 7var r: Z = n
 8
 9while (r >= d) {
10    l"""{
11         //invariant  ???
12         modifies q, r
13    }"""
14    println("(a) n =", n, " d =", d, " q =", q, " r =", r)
15    q = q + 1
16    r = r - d
17}
18assert ( )
19print ("(b) n =", n, " d =", d, " q =", q, " r =", r)
20print (n, " divided by ", d, " is ", q, " with remainder ", r)

Here is a sample execution with trace information printed:

Type an nonegative int: 14
Type a positive int: 3
(a) n = 14  d = 3  q = 0  r = 14
(a) n = 14  d = 3  q = 1  r = 11
(a) n = 14  d = 3  q = 2  r = 8
(a) n = 14  d = 3  q = 3  r = 5
(b) n = 14  d = 3  q = 4  r = 2
14 divided by 3 is 4 with remainder 2

This is a “numbers game”, where we are allowed to use only + and - on the numbers. The underlying strategy (invariant) at point (a) is:

l"""{
     invariant  (d * q) + r == n
     modifies q, r
}"""

When the loop quits, that is, when there is no longer enough value in r to allow yet one more subtraction of q, then the result is exactly the quotient-remainder that results from dividing n by d. The proof is left as an exercise.

6.2.3. Averaging Test Scores

Even a task like summing scores and computing their average depends on a loop invariant. We will approach this using accumulators for scores and the number of scores.

 1import org.sireum.logika._
 2var acc : Z = 0  // accumulator
 3var cnt: Z = 0   // number of entered scores
 4var scr: Z = 0   // value of current entered score
 5var average: Z = 0
 6
 7while (s >=0) {
 8    l"""{
 9        invariant  ???
10        modifies acc, cnt, scr
11        }"""
12    scr = readInt("Enter score , or -1 to quit")
13    if (scr >=0 )  {
14        acc = acc + scr
15        cnt = cnt + 1
16    }
17}
18if (cnt > 0) {
19            average = acc / cnt
20            }
21print (cnt, " scores were entered with an average of ", average)

If we examine the loop at an arbitrary iteration, we see that acc holds the sum of the value of each previously entered valid score, and cnt contains the number of valid scores entered.

When a valid score is entered, the “new” value of the accumulator should equal the old value plus the score. At first blush this seems straight forward

acc = acc + scr
l"""{
     1. acc = acc_old + scr    premise
}"""

But recall that acc_old only has a scope of the proof block following the assignment. Thus invariant acc = acc_old + scr will not work. We will need extra variables to explicitly keep track of the previous loop-iteration’s values. Next we note that “if a valid score is entered ” means we have an implication, so acc = prevA + scr must be conditioned on s >=0.

The above arguments apply to the count variable as well. This leads to

 1import org.sireum.logika._
 2var acc : Z = 0    // accumulator
 3var pAcc: Z = acc  // the accumulator value of previous loop-iteration
 4var cnt: Z = 0     // number of entered scores
 5var pCnt: Z = -1   // number of previous entered scores
 6var scr: Z = 0     // value of current entered score
 7var average: Z = 0
 8
 9while (scr >=0) {
10    l"""{
11        invariant   acc = pAcc + scr ∨ scr < 0
12                    cnt = pCnt + 1 ∨ scr < 0
13                    pCnt > -1 -> cnt >=0
14        modifies acc, pAcc, cnt, scr, pCnt
15        }"""
16    scr = readInt("Enter score , or -1 to quit")
17    if (scr >=0 )  {
18        pAcc = acc
19        acc = acc + scr
20        pCnt = cnt
21        cnt = cnt + 1
22    }
23}
24if (pCnt > -1) {
25            cnt = cnt + 1
26            average = acc / cnt
27            }
28print (cnt, " scores were entered with an average of ", average)

The proof is left as an exercise.

6.2.4. Squaring an Array’s Numbers

Say that a is a sequence of ints, and say we write a loop that squares each of a’s elements and stores the information in sequence b:

 1import org.sireum.logika._
 2var a : ZS = ZS(5, 10, 15, 20)
 3var b: ZS = a.clone  // creates in b as a copy of a
 4var i : Z = 0
 5
 6while (i != a.size) {
 7    l"""{
 8        invariant  ???
 9        modifies i, b
10    }"""
11    b(i) = a(i)*a(i)
12    i = i + 1
13}
14println (a)
15println(b)

In words, the loop’s invariant is that

while the loop is running, b’s elements, from 0 up to i, are the square of a’s, and the rest are unchanged.

We express this using the logical operator forall ().

First we will want to capture the idea that everything before i in sequence b has been changed by the loop. This is expressed by line 1 below.

1∀j:Z ( 0<= j ∧ j< i) → (b(j) == (a(j) * a(j)))
2∀j:Z ( i<= j ∧ j< b.size) → (b(j) == a(j) )
3b.size = a.size

Lines 2 and 3 express what has not been changed by the loop. All indices i and above still equal a, and the sizes of the 2 sequences are the same.

For all j in the range from 0 to i-1, b(j)== a(j) * a(j); for all j in the range, i to b.size - 1, b(j) == a (j); and we ensure the size of the arrays match. This last statement is key, and we will see why in a moment.

This indicates clearly that sequence b is split into toe segments: one whose elements are the square of a and one whose elements are not yet altered.

When the loop quits, it is because i == b.size. In this situation, the range from i to b.size-1 is empty – all the array’s elements are squared.

Please note, a working invariant in Logika will require more than this key idea. Here is a program that with an set of claims for the invariant which produces the desired behavior. Below we’ll discuss why the “extra” claims are necessary.

 1import org.sireum.logika._
 2var a : ZS = ZS(5, 10, 15, 20)
 3var b: ZS = a.clone
 4var i : Z = 0
 5
 6while (i != b.size) {
 7    l"""{
 8        invariant  i >= 0
 9                   i <= b.size
10                   a.size = b.size
11                   //∀j:Z ( 0<= j ∧ j< i) → (b(j) == (a(j) * a(j)))  equal to line 12
12                   ∀x: (0 ..< i) (b(x) == a(x)* a(x))
13                   ∀x: (i ..< b.size) (b(x) == a(x))
14        modifies i, b
15    }"""
16    b(i) = a(i)*a(i)
17    i = i + 1
18}
19println (a)

Logika will throw an error it we do not prove that all sequence-indexing operations will be inside the sequent itseld. Lets cover the potential of index-out-of-bounds errors in b first. Lines 8 and 9 ensure that at the start of every loop-iteration, the variable i lies inside the indices of b. Specifically, line 16 will not throw an index-out-of-bounds error for sequence b.

However, Logika knows the loop modifies b; so the invariant must account for the possibility of adding or deleting elements in b, i.e changing its size. The invariant a.size = b.size on line 10 enforces a rule that b is always the size of a at the start of every iteration, so line 16 will never get an index-out-of-bounds error from a.

Line 10 also helps us prove our goal. Because the loop does not modify a, and b is always the same size as a, ∀j:Z ( 0<= j j< b.size) (b(j) == (a(j) * a(j))) is true at the end of the loop. If b were allowed to be smaller or bigger than a, indices in on sequence, but not the other, would either throw an error or be ignored. That is not the behavior we want. The proof is left as an exercise.

6.3. Logika Solution Modes

Up to now, we have been running Logika in “manual mode”

../../_images/06-image-30.png

We are now reaching the point where additional practice in manual mode may no longer be a learning activity, and where the proof-blocks after claim transformations can become dozens of lines long. Logika offers two advanced modes which we periodically use to reduce the number and length of required proof-blocks

6.3.1. Auto-mode

../../_images/06-image-30.png

This mode enables you to use auto in lieu of normal justification statements. Consider the final proof for Multiplication as Repeated addition. In manual mode it required roughly 50 lines to complete. In Auto mode

 1import org.sireum.logika._
 2
 3val x: Z = readInt("Type a positive int: ")
 4assume(x >=0)
 5val y: Z = readInt("Type any int : ")
 6var z: Z = 0
 7var count: Z = 0
 8
 9while (count != x) {
10    l"""{
11         invariant z = count * y
12                   count <= x
13         modifies z, count
14    }"""
15    println ("(a) x =", x, " y =", y, " count =", count, " z =", z)
16    z = z + y
17    l"""{
18        3. z = y * (count +1)   auto
19    }"""
20    count = count + 1
21    l"""{
22        5. count <= x               auto
23        6. z = count * y            auto
24    }"""
25}
26
27println ("(b) x =", x, " y =", y, " count =", count, " z =", z)
28
29assert( x * y == z)

it requires fewer than 30 lines. This brevity is achieved with some loss of human readability. Why some manual proof lines can be omitted or why some auto proof-blocks and lines are required may not be intuitively obvious.

6.3.2. Symexe-mode

Even more powerful is Symexe mode. This mode allows you to reason about your program based on invariants and method pre-, post- and frame conditions.

../../_images/06-image-40.png

It reduces the multiplication problem to roughly 20 lines

 1import org.sireum.logika._
 2
 3val x: Z = readInt("Type a positive int: ")
 4assume(x >=0)
 5val y: Z = readInt("Type any int : ")
 6var z: Z = 0
 7var count: Z = 0
 8
 9while (count != x) {
10    l"""{
11         invariant z = count * y
12                   count <= x
13         modifies z, count
14    }"""
15    println ("(a) x =", x, " y =", y, " count =", count, " z =", z)
16    z = z + y
17    count = count + 1
18}
19
20println ("(b) x =", x, " y =", y, " count =", count, " z =", z)
21
22assert( x * y == z)

6.3.3. Pitfalls of Auto and Symexe

When using these more advanced modes, it is not always obvious why Logika will not verify. Sometimes semantic errors in the program keep it from verifying; i.e. Logika has found a corner or edge case for which the program does not account. Other times the invariants and conditions do not actually prove the goal. Inevitably, sometimes it will be both.

In either case an option is to turn off “Auto” and begin typing each proof-block as if in manual mode (this can be done with Symexe enabled) until you find the logical or programming error.

6.4. Loop Termination and Total Correctness

THIS AREA BEING REFURBISHED FOR YOUR FUTURE ENJOYMENT

../../_images/proofs.png

This image is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. https://xkcd.com/1724/

6.5. Sequences and the use of Universal and Existential Assertions

Computer scientists and other programmers often need to express some truths (propositions) about all elements in a collection or sequence of objects.

6.5.1. Sequences

In this course, we use only on type of sequence, an ordered list. The general rules for applying logic to collections can be abstracted from these examples and applied to different types of collections as needed.

As the name implies, there are no “NULL” or gaps in a sequence. However it is possible to make an empty sequence (var b : ZS = ZS()).

Types and Constructors

In class we deal with two types of lists, lists of integers and lists of booleans.

type

Description

constructor

ZS

List of integers

var <var_name>: ZS = ZS (<int, int, …>)

BS

List of booleans

var <var_name>: BS = BS (<bool, bool, ..>)

The version of Logika we use does not support sequences of sequences.

Operations and Properties

Given var a: ZS = ZS(1,2,3). a = [1,2,3]

Indexing a(Z)

Sequences are index from 0. Logika will show an error if you have not proven that a variable used for indexing does not lie within the sequence’s range.

a(0)= 1

a.size

Sequences have a property providing their number of elements.

a.size == 3

Append :+

Sequences can have a value appended to their end.

a = a:+(4) // a == [1, 2, 3, 4]

Prepend +:

Sequences can have a value prepended to their front.

a= (0)+:a // a = [ 0, 1, 2, 3]

Reassignment

Sequences instantiated as var can be reassigned. This appears to be the only way to “shrink” a sequence.

a = ZS(5,6) // a = [ 5, 6]

var b: ZS =

a.clone

Creates an exact copy of a for b. Dirrect assignment b = a, is not permitted

Law for Sequence Element Assignment (Claim Transformation)

To assign a value to an element i of a sequence a is very similar to the assignment of non-sequence element.

l"""{
     prove :
     0 <= i      // must be stated with 0 on LHS
     i < a.size
     P           // other claims involving a(i)
 }"""

 a(e1) = e2     // expr is type of sequence
 l"""{
     1. [a_old/a] P                    premise
     2. a.size = a_old.size            premise
     3. a([a_old/a]e1) = [a_old/a]e2   premise
         //  what changed in the sequence
     4. ∀i: (0 .. < a.size)
          (i != [a_old/a]e1) →
               a(i)==a_old(i)          premise
         // what did not change in the sequence
  }"""

in the Logika-proof block immediately following a sequence element assignment, Logika creates a local “sequence_old” for reference. A concrete Logika example (solved in auto mode) of this law is below:

 1import org.sireum.logika._
 2var s: ZS = ZS (0,1,2,3,4,5,6,7,8,9)
 3val j: Z = s(2)                                   // e1 = s(2)
 4l"""{
 5     1. j == s(2)                  premise
 6     2. s(2) == 2                  auto
 7     3. s.size = 10                premise
 8     20. 0 <= j                    algebra 1 2    // 0 <= e1
 9     21. j < s.size                algebra 1 3 2  // e1 < a.size
10     22. s(j) < 5                  algebra 1 2    // P
11}"""
12
13s(j) = s(j) + 1                                   // e2 = s(j) + 1
14l"""{
15     1. s_old(j) < 5               premise        // P
16     2. s_old(j) = 2               premise        // P
17     3. s.size = s_old.size        premise        // a.size = a_old.size
18     4. s(s_old(2)) = s_old(j) +1  premise        // a([a_old/a]e1)= [a_old/a]e2
19     5. ∀i: (0 .. < s.size)                       // ∀i: (0 .. < a.size)
20            (i != s_old(2)) →                     //    (i != [a_old/a]e1) →
21              s(i)==s_old(i)       premise        //      a(i)==a_old(i)
22}"""

Note the use of auto on line 2. This eliminates roughly a dozen lines of claim manipulations. Proving an element in a sequence, created by ZS(1,2,...) is equal to a value is a laborious process in manual mode.

6.5.2. Use of Quantifiers

The basic form of specifying some claim P(a(x)) holds for every member of the sequence a are

∀<var-name>: <var-type> ( lower-bound ∧ upper-bound)  → P(a(x))

∀<var-name>( lower-bound .. upper-bound)  P(a(x))   // shorthand version

Where the lower- and upper-bounds are inclusive statements about where to start and end looking through the sequence. For example ∀x: Z  ( x>=0 x < a.size)  P(a(x)) would check every member of a to see that P(a(x)) holds where as ∀x: Z  ( 0 .. < (a.size/ 2))  P(a(x)) would check the first half. Logika will verify , if and only if P(a(x)) is proven true for every a(x) in the range.

Similarly, the forms for there exist are

∃<var-name>: <var-type> ( lower-bound ∧ upper-bound)  → P(a(x))

∃<var-name>( lower-bound .. upper-bound)  P(a(x))

Of course in this case at least 1 member of a(x) in the checked range must satisfy P(a(x)) for Logika to verify.

Note

Best Practice for Sequences

It is best, when modifying a sequence in a loop (or function), to always specify what changes and what remains the same, including the size of the sequence. Failure to do so may lead to sound, but incomplete proofs. If you have no errors, Logika will verify sound but incompletely specified proofs.

6.6. Summary

The law for while-loops is

"""{ ... I }"""
while B :
  """
  { invariant  I
    modifies   VARLIST   (those variables updated in  C) }
  """
  """
  { 1. B   premise
    2. I   premise
    ... }
  """
  C
  """{ ... I }"""

"""
{ 1. ~B    premise
  2. I     premise
  ... }
"""

This note was adapted from David Schmidt's CIS 301, 2008, Chapter 4 course note.

It was updated in 2018 by Dr John Hatcliff and George Lavezzi
to conform with Logika syntax and more closely match
KSU's CIS 301 course as taught in Spring 2018.