A Theory of Termination via Indirection Robert Dockins and Aquinas Hobor (Princeton University) (NUS) Goal • Use step-indexed models to prove program termination in the presence of certain (approximated) recursive domain equations • Testbed: soundness of a logic of total correctness for a language with 1. Function pointers 2. Semantic assertions (assert truth of a (classical) logical formula at the current program point) 2 Goal • Use step-indexed models to prove program termination in the presence of certain (approximated) recursive domain equations • Testbed: soundness of a logic of total correctness for a language with 1. Function pointers 2. Semantic assertions (assert truth of a (classical) logical formula at the current program point) 3 A very simple language 4 Simple semantic definitions 5 Simple semantic definitions Uh oh… 6 We detail our predicates/assertions informally and refer to the paper for formal construction 7 We detail our predicates/assertions informally and refer to the paper for formal construction 8 We detail our predicates/assertions informally and refer to the paper for formal construction 9 We detail our predicates/assertions informally and refer to the paper for formal construction 10 What does funptr l t [P] [Q] mean? 1. The program has some code c at address l . (This is why we want predicates to be able to judge programs.) 2. When c is called with some initial store ½, if t(½) is defined then c makes at most t(½) function calls before returning to its caller. 3. P and Q are actually functions from some function-specific type A to predicates. If t(½) is defined then for all a, if P(a) holds before the call then Q(a) will hold afterwards. 11 Why it was (considered) hard • Certain people who will remain anonymous (some in this room, some elsewhere, e.g., Princeton, NJ) thought step-indexed models were inapplicable to liveness problems • We have discussed the difficulty of statements like, “method X cannot be used to prove Y” 12 Why it was (considered) hard • Certain people who will remain anonymous (some in this room, some elsewhere, e.g., Princeton, NJ) thought step-indexed models were inapplicable to liveness problems • We have discussed the difficulty of statements like, “method X cannot be used to prove Y” 13 Why it was (considered) hard • But if you asked, such people said: 1. Step-indexed models have an outer “forall n” that quantifies (universally) over the number of steps; this works well for safety properties but very poorly for liveness properties 2. While “safe” is hereditary/monotonic/closed under approximation, “halts” is not (actually, this is related to point 1, too…) 3. It’s never been done and termination is hard 14 Why it was (considered) hard • But if you asked, such people said: 1. Step-indexed models have an outer “forall n” that quantifies (universally) over the number of steps; this works well for safety properties but very poorly for liveness properties 2. While “safe” is hereditary/monotonic/closed under approximation, “halts” is not (actually, this is related to point 1, too…) 3. It’s never been done and termination is hard 15 Why it was (considered) hard • But if you asked, such people said: 1. Step-indexed models have an outer “forall n” that quantifies (universally) over the number of steps; this works well for safety properties but very poorly for liveness properties 2. While “safe” is hereditary/monotonic/closed under approximation, “halts” is not (actually, this is related to point 1, too…) 3. It’s never been done and termination is hard 16 Why it’s actually not that hard 1. It turns out that the outside quantification can just as easily be existential if we can only find the right value to instantiate it… – This is exactly what a termination proof gives us! – “P terminates” a.k.a. “exists n. P reaches halt in n steps” 17 Why it’s actually not that hard 1. It turns out that the outside quantification can just as easily be existential if we can only find the right value to instantiate it… – This is exactly what a termination proof gives us! – “exists n. prog reaches halt in n steps” 18 Hoare Judgment • Our Hoare Judgment looks a bit complex: ¡, R `n { P } c { Q } • ¡ contains function pointer assertions and R is the postcondition of the current function • n is an upper bound on the number of function calls c makes before it terminates 19 Hoare Rules, 1 20 Hoare Rules, 1 21 Hoare Rules, 2 22 Hoare Rules, 2 23 Hoare Rules, 3 24 Hoare Rules, 3 25 The Call Rule The interesting rule is for call… and it’s not too bad: 26 The Call Rule The interesting rule is for call… and it’s not too bad: 1. x must evaluate to a label l 27 The Call Rule The interesting rule is for call… and it’s not too bad: 2. l must be a pointer to a function with termination measure t, precondition Pl and postcondition Ql 28 The Call Rule The interesting rule is for call… and it’s not too bad: 3. The termination measure t must evaluate to some n on the current store 29 The Call Rule The interesting rule is for call… and it’s not too bad: 3. The termination measure t must evaluate to some n on the current store… and so this call will take no more than n+1 calls. 30 The Call Rule The interesting rule is for call… and it’s not too bad: 4. We require the (parameterized) precondition to be true at call, and guarantee the (parameterized) postcondition will be true on return. 31 So, not too bad… • Most of these rules are really nothing to get excited about… even the call rule is pretty simple when you understand the parts… • (This is a virtue, of course…) • But we’re not done. We’ve only shown the rule for using the funptr, not the rules for verifying that a function actually terminates 32 Verifying function bodies • Verifying function bodies, while very important, is not part of the main line of this talk – why proving termination with a stepindexed model isn’t so bad. • We can verify functions in many interesting cases – recursive, mutually recursive, higherorder (we can handle map of map), etc. • If you want, ask me about this at the end. (I have 35 slides as backup!) 33 Why it’s actually not that hard 2. The second reason people thought that termination was really hard with stepindexed models was that “halting” is not hereditary/monotonic/downward closed. A. What is hereditary? B. Why is halting not hereditary? 34 Why it’s actually not that hard 2. P hereditary ´ (w ² P Æ wÃw’) ! w’ ² P Here à is the “age” or “approximate” operation on step-indexed worlds. We can only approximate a finite number of times before we “hit bottom”. The step relation approximates w during function call. 35 Why it’s actually not that hard 2. The standard definition: w ² halts(¾) ´ 9 wh. (w,¾) * (wh, []) Let w à w’. The problem is that the relation approximates the world (at function call), so w’ might not have enough “time” left to actually reach the halting state []. 36 Why it’s actually not that hard 2. Our definition: w ² haltsn(¾) ´ |w| > n ! (9 wh. (|w|-|wh| · n) Æ (w,¾) * (wh, []) ) This is actually very similar to the basic definition. 37 Why it’s actually not that hard 2. Our definition: w ² haltsn(¾) ´ |w| > n ! (9 wh. (|w|-|wh| · n) Æ (w,¾) * (wh, []) ) Here is the exists and requirement that we step to the halt state. 38 Why it’s actually not that hard 2. Our definition: w ² haltsn(¾) ´ |w| > n ! (9 wh. (|w|-|wh| · n) Æ (w,¾) * (wh, []) ) This is the key “trick” – if the amount of time left in w is not enough, then we become true (and thus hereditary) trivially. 39 Why it’s actually not that hard 2. Our definition: w ² haltsn(¾) ´ |w| > n ! (9 wh. (|w|-|wh| · n) Æ (w,¾) * (wh, []) ) We must be sure that n is really a bound on the number of steps required to halt. (Alternative: see paper’s Coq development.) 40 Why it’s actually not that hard 2. Our definition: w ² haltsn(¾) ´ |w| > n ! (9 wh. (|w|-|wh| · n) Æ (w,¾) * (wh, []) ) So, really not that bad. This is the “fundamental” trick that makes everything else possible. 41 Why it’s actually not that hard 2. See the paper (and/or Coq development) for details on how to use this definition to build up the definition for the Hoare tuple, etc. Actually I have presented a slightly modified version (we continue to tinker), but feel free to ask us for the latest version. 42 Why it’s actually not that hard 3. The third objection is that termination is tricky and have never been done with stepindexing before. We observe the following: • Our top-level “erased” theorems are stated in absolutely standard form: if a program is verified in our logic, then it terminates. • No indirection theory in statement of (erased) top-level theorem. 43 Why it’s actually not that hard 3. The third objection is that termination is tricky and have never been done with stepindexing before. We observe the following: • All our proofs are machine checked in Coq. • Our core proofs are quite compact. 44 Compact Proofs • The main file is 826 lines long, and contains: A. Semantic model of the terminating function pointer, Hoare judgment, whole-program verification judgment B. 10+ Hoare rules and soundness proofs C. 3 whole-program rules and soundness proofs D. Top-level theorems (if P is verified, then it halts) • Really quite short… 45 46 V-rules • We need another judgment, written ª : ¡, that says that program ª contains functions verified to “funptr” specifications in ¡. • We will verify functions one at a time (or in the case of mutually-recursive groups, as a group). • The “base case” is an easy rule: 47 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 48 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 1. We have verified part of ª to specification ¡ 49 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 2. We want to add this specification for l 50 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 3. We must have verified the code for l 51 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 4. When the precondition P(a) holds 52 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 4. When the precondition P(a) holds and the termination measure is equal to some n. 53 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 5. This n is also an upper bound on the number of calls this function makes. 54 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 6. The postcondition is just ?: you can’t fall out the bottom 55 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 6. The postcondition is just ?: you can’t fall out the bottom, but the return condition is Q(a). 56 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 7. We can assume every funptr we have previously verified, and… 57 The “Vsimple” rule So this looks pretty bad, but we can take it apart: 8. We can call ourselves using a modified function specification precondition: t has decreased. 58 The “Vsimple” rule Here are the critical uses for t. If we assume the termination measure is equal to n at the function start, then we can make no more than n calls and can recurse only when t < n. 59 Using Vsimple • We will verify the unary addition function • Our language, while Turing-complete, is extremely painful to program in; this is not fundamental but it does mean that our examples are somewhat “lame”. • Expanding the result to a more programmable language would not be very difficult now. 60 Using Vsimple: naturals as lists • listnat(0,0) • listnat(n,v) \Rightarrow listnat(n+1, (0,v)) • • • • • 0 1 2 3 … 0 (0, 0) (0, (0, 0)) (0, (0, (0, 0))) … 61 Precondition, postcondition, termination measure addP(n,m) ´ 9 v1, v2. (r1 v1) Æ (r2 v2) Æ listnat(n,v1) Æ listnat(m,v2) addQ(n,m) ´ 9 v2. (r2 v2) Æ listnat(n+m,v2) addt(½) ´ the unique n s.t. 9 v1. (½(r1) = v1) and listnat(n,v1) 62 Code (stored at label 1) 63 The verification • Actually, using the Hoare rules is very easy. 64 The verification • Actually, using the Hoare rules is very easy. • But very tedious. We won’t bore you. • The more interesting question is, what does the verification obligation generated from Vsimple look like? 65 Verification obligation from Vsimple n ¡n, xxx 66 Vsimple, in a nutshell • The Vsimple rule is fine for most functions, but it can only verify, well, simple recursive functions (as well as calls to both simple and complex functions previously verified). • If we want “the goodies” (mutual recursion, polymorphic termination arguments, etc.) then we need to call in Vsimple’s big brother… 67 The “Vfull” rule This is moderately horrible. We have (at least): 68 The “Vfull” rule This is moderately horrible. We have (at least): 1. A set © of funptr specifications in the goal as well as the assumptions 69 The “Vfull” rule This is moderately horrible. We have (at least): 2. The same basic termination measure trick 70 The “Vfull” rule This is moderately horrible. We have (at least): 3. A parameter b used for higher-order functions 71 The “Vfull” rule This is moderately horrible. We have (at least): 3. There can be a strong dependency between b and the other factors (e.g., the type of a) 72 The “Vfull” rule The Vsimple rule is just a special case of Vfull. 73 Using Vfull • We are going to verify the simplest nontrivial higher order function we can, “apply”. • It takes as an argument a pair of a function f and an argument v and just applies f to v. • The “interesting” part is how the polymorphism is verified as opposed to the function behavior. 74 Defining a calling convention • To specify the “apply” function, we must define a calling convention for the sub-function 75 Defining a calling convention • The registers r1 – r4 are callee-saves; registers from r5 ! 1 registers are caller-saves 76 Defining a calling convention • A stdfun’s precondition, postcondition, and termination measure only depend on r0. 77 Apply’s precondition, postcondition, and termination measure 78 Apply’s precondition, postcondition, and termination measure • These look “obvious” until you realize that P, Q, and t seem free in the definition. We will see how the Vfull rule “pipes” these in. 79 Apply’s precondition, postcondition, and termination measure • Notice that we can use the called function’s termination measure to build our own 80 The code is simple, although the verification is a bit cluttered… 81 If you’ve got it, flaunt it: we toss in a higherorder assert for fun… 82 Notice how we track the termination measure 83 The verification obligation from Vfull This is somewhat large (thank goodness for machine-checking!). There are a few points of particular interest. 84 The verification obligation from Vfull 1. We can check this function first, — i.e., ¡ = > That is, we verify this function before we verify the functions we will pass to it. 85 The verification obligation from Vfull 2. The Vfull rule “pipes in” t, P, and Q – in fact, the triple (t,P,Q) is the “b” from Vfull. 86 The verification obligation from Vfull 3. As before, we thread the termination argument into the verification required. 87 “Stacking” apply • As it happens, the apply function, itself, is a standard function (r1 – r4 are preserved, argument passed in r0, return in r0). • In fact, we can pass “apply” to itself without difficulty. The rules will prevent us from unbounded recursion. We can “apply (apply (apply … (apply (f))))” but the function f “at the bottom” must terminate “on its own”. 88 More examples… • We could go on to map, map of map, etc., but maybe it would be better here to just refer to the paper and mechanized development. • Are there questions? 89