Loop Invariants
cs.miami.edu
cs.miami.edu
if ( a[smallestSoFar] > a[nextToCheck] ) {
This code makes me twitch. Finding a smaller value by checking to see if the old value is larger than the new value... Instead of checking to see if the new value is smaller than the old value.Additionally, calling the value you are currently checking 'nextToCheck' is also confusing. It's not the next one, it's the current one.
if ( a[smallestSoFar] > a[nextToCheck] ) {
smallestSoFar = nextToCheck ;
}
When a > b then a = bThe order of the variables doesn't change.
The value is called nextToCheck because the article is specifically talking about loop invariants. The value of nextToCheck only makes sense when loop invariant is established prior to the loop and after work has been done in the loop to reestablish the loop invariant.
In both of those cases the value in nextToCheck is clearly meant to denote the half open interval [0, nextToCheck) which contains the smallestSoFar.
currentComparisonIndex is not the correct name for the loop variable because that name is only accurate after checking the termination condition and prior to incrementing. This is not what the loop invariant states the purpose of nextToCheck is. Therefore you would not have established a proper loop invariant prior to entering your loop. nextToCheck might more fully be called 'nextToCheckAfterTerminationCheck'
Of course this is all meta above the level of the language is self. You could call the variable 'foo' and of course the code would still function.
How would this look if you were writing code that dealt with something like a linked list where you have 'cur' and 'next' pointers? 'cur' would be 'next' and 'next would be 'nextNext' ?
When I am looking at code that is trying to find a smaller number I expect to see a < operator.
Maybe back in the day when people were using basic and assembly language it was easy to write some gawdawful mess but since structured programming I think it is easier to do the right thing rather than the wrong thing. Partially because we have good constructs to work with, secondly they are good constructs to reason with -- for instance, you might have the idea of a loop invariant in your head when you write the loop, even if you don't call it that.
The people behind the Microsoft Static Driver Verifier say that they are unable to decide if a program is safe to be a kernel driver about 4% of the time. This is grounds for rejecting the driver. If your kernel driver is anywhere near the limits of formal decidability, it shouldn't be in the kernel.
Typically, you fix undecidability by adding loop counters or checking. If you have some complex algorithm where termination is hard to prove, adding a loop counter and an iteration limit will guarantee termination. (This comes up as a practical matter in floating point work. There are algorithms which are guaranteed to terminate on real numbers, but not on finite-precision floating point numbers. One example I've run into is GJK, used in collision detection.)
Undecidability is just not a problem in real program verification.
It reminds me of the hand-wavy inductive proofs I run through my head when I write recursive functions. Usually it's not even about eliminating bugs, it's just the most natural way to understand why the code works. It seems less natural for imperative for/while loops but still a good tool to keep in mind.
Here's how we did it in the Pascal-F verifier [1] around 1980. This is the simplest example - proving that all entries in an array are zero after the array is initialized.
type tabix = 1..100;
type tab = array [tabix] of integer;
rule function allzero(a: tab; i,j: tabix): boolean; begin end;
var table1: tab;
i,j: tabix;
...
for i := 1 to 100 do begin
table1[i] := 0;
assert(allzero(table1,1,i-1));
state(allzero(table1,1,i));
end;
assert(allzero(a,1,100));
...
assert(table1[j] = 0); { table1[j] must be 0 }
The loop invariant is state(allzero(table1,1,i));
which says that the predicate "allzero" is true for table1 from 1 to i."allzero" is defined in the Boyer-Moore theorem prover as a recursive function.
(defn allzero (a i j)
(if (lessp j i)
t
(and (allzero a (add1 i) j) (equal (selecta! a i) 0))))
That's a recursive function. The prover is able to prove that it terminates.
"(selecta! a i)" is LISP for "a[i]" here.We need three theorems about "allzero". First, that it's vacuously true for j < i:
(implies (and (arrayp! a)
(numberp i)
(numberp j)
(lessp j i))
(allzero a i j)))
Notation: "arrayp!" is true if the item is an array. "numberp" is true if it's an integer. "lessp"
is "<".Then that it's true for a single element if that element is zero:
(implies (and (arrayp! a)
(numberp i)
(numberp j)
(equal i j)
(equal (selecta! a i) 0))
(allzero a i j))))
And finally, the hard one, that if it's true up to N, and a[N+1] is zero, it's
true up to N+1. (implies (and (arrayp! a)
(numberp i)
(numberp j)
(allzero a i j)
(equal (selecta! a (add1 j)) 0))
(allzero a i (add1 j))))
These are all proved, once, with the Boyer-Moore theorem prover. Once proven, they can be
used as rules by the much simpler and faster Oppen-Nelson prover built into the Pascal-F verifier. Over time, you build up a library of such rules, which are applied automatically
by the fast prover.This is 1980 technology; there's been a lot of progress in the last 35 years. This ran on a 1 MIPS VAX 11/780 and later on Sun II workstations. Back then, it was very slow to do this. That's a solved problem. It's still rather painful. But it works.
[1] http://www.animats.com/papers/verifier/verifiermanual.pdf
So to find a minimum in a list A[1],A[2],...,A[n] you can something like:
i := 1;
// Invariant: A[min] is the smallest in {A[1], ..., A[i]}
while not i = n do begin
if A[i+1] < A[min] then min:= i+1;
i:= i+1;
end
The negation of the condition: 'i = n', combined with the invariant: 'A[min] is the minimum of {A[1], ..., A[i]}', gives you the result you wanted. func Search(n int, f func(int) bool) int {
// Define f(-1) == false and f(n) == true.
// Invariant: f(i-1) == false, f(j) == true.
i, j := 0, n
for i < j {
h := i + (j-i)/2 // avoid overflow when computing h
// i ≤ h < j
if !f(h) {
i = h + 1 // preserves f(i-1) == false
} else {
j = h // preserves f(j) == true
}
}
// i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
return i
}
There's lots more examples of you search for "invariant" thru the std library source code.A few, to give a taste:
ast/print.go printer.Write
// invariant: data[0:n] has been written
printer/printer.go trimmer.Write: // invariants:
// p.state == inSpace:
// p.space is unwritten
// p.state == inEscape, inText:
// data[m:n] is unwritten
path/filepath/path.go: path.Clean // Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// dotdot is index in buf where .. must stop, either because
// it is the leading slash or it is a leading ../../.. prefix.
Invariants really are a really useful way to think about code. /*@ loop invariant i;
loop invariant j >= 0;
loop assigns j, eol;
*/
for (j = 0; j < (size_t) i; j++) {
if (read_buffer[j] == '\n') {
eol = 1;
j++;
break;
}
}
Loop invariants are part of the ACSL specification language, and they can be verified automatically with Frama-C. http://frama-c.com/acsl.htmlLoop invariants get important in the section on Bentley-McIlroy three section partioning.
for (let i = 0; i < someVar; i++) {
invariant: i < 10000;
console.log(i);
}
along with other kinds of contract, like preconditions and postconditions - https://github.com/codemix/babel-plugin-contracts bigFloat delta = 0.5
bigFloat total = 0.0
while total < 1.0:
total+=delta
delta*=0.5
Where does this fit in the model of using a loop invariant to show the loop terminates?
The loop is always approaching the terminal case (and eventually the machine runs out of memory)- correction: define a loop invariant. Assuming the loop terminates, it will hold when the program exits the loop.
- termination: define a loop variant, something that changes at every iteration. If you can find a variant that is a strictly decreasing sequence of natural numbers (or an increasing, bounded sequence), you've established that the loop terminates.
Once you've done these two steps you know that the program terminates and the invariant holds at the end. (in your case, you won't be able to find a good variant - depending on the floating point semantics you use of course).
The amount of memory required to store the delta variable should be at least log_2(n) bits, where n is the iteration number. This has to be true, regardless of the implementation details of bigFloat because delta has to take on n distinct values. Eventually it exhausts the memory of the machine.
It seems pretty straightforward, unless your compiler is smart enough to see what's going on and optimize the body of the loop away, in which case it wouldn't terminate.
As you said, the machine eventually runs out of memory. But "loop invariants" don't usually include the machine running out of memory as the termination condition, because that's more a crash than a loop termination. (That is, whatever part of the program you wanted to execute after the zeno loop won't execute, because the program terminated, not just the loop.) So when we talk about loop termination, program-crashing events are usually out of scope. (That's something we also need to prove - no undesirable side effects - but that's actually a separate issue.)
when this is false, the loop should terminate
even BigFloats have limited precision
I have just also realised that delta would be 0 if < epsilon. So the invariant delta > 0, break when false. (I think, the OP is my introduction to invariant loops).
Scheme, for instance, has a 32 bit mantissa for BigFloat by default [1]
Haskell's mantissa precision is determined by the type of the mantissa when created [2]
For GNU MPFR "the precision in bits can be set exactly to any valid value" [3] which is what is used in Lisp, Python, Perl, Racket, Java, ADA, Ruby & Eiffel.
[1] https://github.com/55portal55/bigfloat/blob/master/bigfloat....
[2] https://hackage.haskell.org/package/numbers-3000.2.0.1/docs/...
This is not the commonly understood nomenclature, notwithstanding what some languages call a BigFloat. See: https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic
In any case, it's clearly not what Rhapso intended, either, because otherwise his code snippet wouldn't exhaust memory.
BigRational deltaIn our Pascal-F system, we had a MEASURE statement for that. You didn't need it for FOR loops, though.