CS61b: Lecture 38 Monday, November 27 ANNOUNCEMENTS: Verification ------------ Testing -- showing by running some examples that a program works Verification -- proving that a program works correctly for all inputs by examing the code. Testing ------- Modular testing: testing each function and each class separately. ## First test the normal input, then test the boundary cases ## boundaries in the interface ## branches in the code Two kinds of test code for modular testing: drivers: code written for testing that calls the code being tested stubs: bits of code that are called by the code being tested Integration testing: testing a set of functions/classes together Verification ------------ What does is mean for a program to be correct? Need a specification (precise, although not necessarily deterministic), written as pre/post conditions. Recall that the precondition and postcondition clauses in a specification define what the procedure will do. ## * Precondition: a predicate (boolean valued function) that must ## be true when the procedure is called. ## * Postcondition: a predicate that must be true when the procedure returns ## Two standard notions of correctness * Partial correctness: if the Precondition is true when the procedure is invoked, then if the procedure terminates, the Postcondition must be true when the procedure returns. * Total correctness: if the Precondition is true when the procedure is invoked, then the procedure must terminate, and the Postcondition must be true when the procedure returns. ## Total correctness = partial correctness + termination Why do we separate these two? Because they require different proof techniques. The standard notation in this business is: { P } program text { Q } If the program text were a procedure body, then P and Q would be the precondition and postcondition, respectively. This syntactic convention came about before C++ did, and since our program text contains curly braces, it will be too confusing. Instead we will write: /* assert (P); */ program text /* assert (Q); */ Neither partial nor total correctness say anything about what happens if the precondition is false when the procedure is called. If P is false before executing the program text, then the correctness condition is, "If false before then Q after," which is vacuously true regardless of what the program does. In some cases we could remove the comments around P and Q, and test that are assertions are true (in some example input) by running the program. This is a good idea if P and Q are valid C++ expressions and if they are relatively inexpensive, but sometimes we will not be writing C++ code for P and Q. (An example we've seen so far is predicates about size of an array.) STRAIGHT LINE CODE We'll start with a trivial example. int cube(int x) { /* Postcondition: returns x^3 */ int res; res = x*x; res = res*x; return res; } The first and last lines are a written to make the job of reasoning about the middle of the program simpler. The two statements in the middle are of interest, and the post condition should be clear: res = x^3. What is the precondition? Since there is none on the procedure, the precondition is the predicate "TRUE." We can therefor write the "proof obligation" as: Original annoted program: Missing proof step: /* assert(TRUE) */ res = x*x; /* res == x^2 */ res = res*x; /* assert(res == x^3) */ Without belaboring this too much, we could fill in the missing proof step by saying that res == x^2 in the middle. The general rule for assignment statements of the form: v = expr is: /* assert(P'), where P' is P with expr for v */ v = expr; /* assert(P), where P contains v */ This assumes expr doesn't have any side effects. In the above /* assert((x*x)*x == x^3 */ res = x*x; /* assert(res*x == x^3 */ res = res*x; /* assert(res == x^3) */ PROVING PROPERTIES OF LOOPS We could be even more formal about the above proof, which would be necessary if we wanted to prove arbitrary programs correct. However, it would involved a lot of formalism and uninteresting details. Instead, let's consider a program construct that is often tricky to get right--the loop--and see how to prove things about loops. This should also help you write programs like binary search, quicksort, and similar things more quickly (with less debugging time). There are two techniques for proving things about loops, one for partial correctness and one for termination. loop invariant: a predicate that is true on each iteration of the loop body. variant function: an integer valued function that gets closer to zero on every iteration of the loop, and once the function hits zero, the loop will terminate An Example: Search // Precondition: n > 0 and the size of a == n. // Postcondition: Returns res. If key is in a, a[res] = key, // otherwise (if key is not in a), res = -1. int search (int a[], int n, int key) { int res = -1; int i = 0; while (i < n) { if (a[i] == key) { res = i; } i++; } return res; } Loop invariant: (res = -1) => (forall j, 0 <= j < i, a[j] != key) and (res != -1) => a[res] = key. Variant function: n-i Let's look at the loop invariant in more detail. What do we want it to do? It is used for a while loop of the form: /* assert(P) */ while (B) { S; } /* assert(Q) */ where S may be a sequence of statements. Given an invariant, Inv: 1) Inv must be true before we execute the while loop. I.e., P => Inv. 2) If Inv is true before evaluating B, it is true after evaluating B. (This is easy to gaurantee is B doesn't contain any assignment statements.) 3) If Inv and B are true at the beginning of the loop body, then Inv must be true at the end of the loop 4) Inv plus the negation of the condition (i.e., Inv & not(B)) must imply Q. { res = -1; i = 0; /* assert (n>0 & size of a == n) */ while (i < n) { /* assert((i < n) & (res = -1) => (forall j, 0 <= j < i, a[j] != key)); & (res != -1) => a[res] = key */ if (a[i] == key) res = i; /* assert((i < n) & (res = -1) => (forall j, 0 <= j < i+1, a[j] != key)); & (res != -1) => a[res] = key */ else i = i+1; /* assert((i < n) & (res = -1) => (forall j, 0 <= j < i, a[j] != key)); & (res != -1) => a[res] = key */ } /* assert((i >= n) & (res = -1) => (forall j, 0 <= j < n, a[j] != key)); & (res != -1) => a[res] = key */ /* implies */ /* assert If key is in a, then a[res] == key, otherwise, res = -1. */ return res; } This proves the loop invariant, and that the postcondition holds if the loop completes. However, we also have to check each of the returns, to see that the postcondition. In this case the "return i;" in the middle of the loop. The loop invariant, plus the boolean condition (in the if) are enough to prove the postcondition there.