Last time, we discussed the big-step operational semantics for IMP as well as the denotational semantics. To recap: Big-step: --------- (X1) => i => i1 => i2 i1 b i2 = i (X2) ------------------------------------------- => i (X3) => s(x) (B1) => s => i (B2) --------------------- => s[x|->i] => s1 => s' (B3) --------------------------- => s' => i (i != 0) => s' (B4) -------------------------------------- => s' => 0 => s' (B5) -------------------------------------- => s' => s' (B6) ----------------------------------------------- => s' Denotational: ------------- E[i] = {(s,i)} E[x] = {(s,s(x))} E[e1 b e2] = {(s,i) | (s,i1) in E[e1] && (s,i2) in E[e2] && i = i1 b i2.} C[skip] = {(s,s)} C[x:=e] = {(s,s[x|->i]) | (s,i) in E[e]} C[c1;c2] = { (s,s') | exists s1.(s,s1) in C[c1] and (s1,s') in C[c2] } C[if e then c1 else c2] = { (s,s') | (s,i) in E[e] && i != 0 && (s,s') in C[c1] } U { (s,s') | (s,0) in E[e] && (s,s') in C[c2] } C[while e do c] = U(F^i({})) where F(S) = S U {(s,s') | (s,0) in E[e] && s' = s} U {(s,s') | (s,i) in E[e] && i != 0 && exists s1. (s,s1) in C[c] && (s1,s') in S} (Note, technically F is parameterized by E[e] and C[c] so we should write: C[while e do c] = U((F E[e] C[c])^i({})) where F(E,C)(S) = S U {(s,s') | (s,0) in E && s' = s} U {(s,s') | (s,i) in E && i != 0 && exists s1. (s,s1) in C && (s1,s') in S} but I don't want to confuse things below.) The meaning of a while loop is the union of a sequence of sets S0, S1, S2, S3, ... where Si = F^i({}). Let's pretend that we have a command "diverge". Then intuitively, S0 = C[diverge] = {} S1 = S0 U C[if e then {c; diverge} else skip] S2 = S1 U C[if e then {c; if e then {c;diverge} else skip} else skip] S3 = S2 U C[if e then {c; if e then {c;if e then {c;diverge} else skip} else skip} else skip] ... That is, the function F(S) behaves sort of like: C[if e then {c; S} else skip] = S U { (s,s') | (s,i) in E[e] && i != 0 && (s,s') in C[c;S] } U { (s,s') | (s,0) in E[e] && (s,s') in C[skip] } = S U { (s,s') | (s,0) in E[e] && (s,s') && s = s' } U { (s,s') | (s,i) in E[e] && i != 0 && exists s1.(s,s1) in C[c] && (s1,s') in S } Of course, what I wrote above doesn't make sense because I'm mixing syntax and semantics but it helps to see where the definition of F comes from. Now we've already established that => s' iff ->* (i.e., that the big-step and small-step semantics coincide.) What we'd like to do now is prove that the denotational and small-step semantics coincide. It's easiest to do this by going through the big- step semantics. So the theorem we would like is: Theorem: => s' iff (s,s') in C[c]. We'll obviously break this into a number of steps: Lemma 1: => i iff (s,i) in E[e]. The forward direction is a straightforward induction over the derivation that gives us => i, and the backwards direction is a straightforward induction over e, appealing to the definition of E[-]. Now we can prove the forward direction of our main theorem by induction on the derivation D that gives us => s'. Most of these cases are trivial. For instance, if the derivation ends with: D1 D2 ------------ ------------ => s1 => s' ----------------------------- => s' Then by our induction hypothesis applied to D1 and D2, we know that (s,s1) in C[c1] and (s1,s') in C[c2]. Thus by the definition of C[c1;c2], we have (s,s') in C[c1;c2]. The only interesting case is for the while loop. Here, D is a derivation that looks like this: D1 ------------------------------------------------ => s' ------------------------------------------------ => s' We remark at this point that doing induction on c (instead of D) would get us into trouble since the size of the command gets bigger, not smaller. Anyway, using our induction hypothesis on D1, we know that (s,s') in C[if e then {c;while e do c} else skip], but we need to show (s,s') in C[while e do c]. I claim that in fact: C[while e do c] = C[if e then {c;while e do c} else skip] (Note that we really want to establish that the first set is equal to the second set if we want to prove the iff.) That is, I claim that: U(F^i({})) = C[if e then {c;while e do c} else skip] where F(S) is defined as above. We remark that: C[if e then {c;while e do c} else skip] = F(C[while e do c]) = F(U(F^i({}))) So what we're trying to prove is that: C[while e do c] = F(C[while e do c]), or U(F^i({})) = F(U(F^i({}))) That is, the meaning of a while-loop is a *fixed-point* of the set-theoretic function F that we used to define the while-loop (and is in fact the least fixed point of F.) Or put another way, the limit of F doesn't gain us any more pairs of stores than are captured by the finite unwindings of while d do c. So how do we show that C[while e do c] is a fixed-point of F? Lemma 2: C[while e do c] <= F(C[while e do c]): This follows immediately from the definition of F since we always add in the previous set: F(S) = S U ... Lemma 3: F(C[while e do c]) <= C[while e do c]. Suppose (s,s') in F(C[while e do c]). Then by the definition of F, either: (a) (s,s') in C[while e do c] (b) (s,0) in E[e] and s' = s or (c) (s,i) in E[e] and i != 0 and exists s1 s.t. (s,s1) in C[c] and (s1,s') in C[while e do c]. If (a) is true, then we are done. If (b) is true, then we claim that (s,s') in F^1({}), again appealing to the definition of F. So suppose (c) is true. Since (s1,s') in C[while e do c], there exists some i such that (s1,s') in F^i({}). It then follows that (s,s') in F^i+1({}) and thus (s,s') in C[while e do c]. -------------------------------------------------------------------- An aside on dataflow analysis: Of course, determining whether an IMP program will terminate is generally undecidable. However, we could formulate a conservative analysis to try to warn programmers when their programs might diverge. In particular, we can pick up easy things like: while i do c where i is a non-zero integer. More generally, we might do a dataflow analysis to determine when we have: while e do c and e will always evaluate to a non-zero integer. The way we're going to do this is to formulate a non-standard denotational semantics, C'. C' will look a *lot* like C, except that it will be computable. The trick is to recognize that the only thing that isn't really computable is the "limit" that we take for while-loops. Somehow, we need to jump to a limit in a finite amount of time. To do this, we're going to abstract quite a bit. First, let us start by formulating *abstract* stores. An abstract store S maps an identifier to a *set* of integers: S : Id -> 2^Z We're going to represent concrete stores by abstract stores with the property that S represents s when s(x) in S(x). Now to keep things finite, we're not going to consider all possible subsets of Z, but rather, only a small collection. In particular, we're going to choose the following subsets of Z: X0. Empty = {} X1. Zero = { 0 } X2. Neg = { i : i < 0 } X3. Pos = { i : i > 0 } X4. NonNeg = { i : i >= 0 } X5. NonPos = { i : i <= 0 } X6. NonZero = { i : i != 0 } X7. Z Note that these subsets form a nice lattice with Empty being at the bottom, and Z being at the top. In particular, we have the property that for any of our two abstract sets X1 and X2, their union can be represented as an abstract set. Let me define the following operation on abstract stores: S1 + S2 = S s.t. S(x) = S1(x) U S2(x) Now I will define a non-standard semantics for IMP. The big difference is that our non-standard semantics will define *total* functions operating on abstract stores: E' : Exp -> AbsStore -> AbsSet C' : Com -> AbsStore -> AbsStore So E' will map an expression and abstract store to one of our Xi's above, and C' will map a command and abstract store to a new abstract store. E'[i]S = Zero if i = 0 Pos if i > 0 Neg if i < 0 E'[x]S = S(x) E'[e1 b e2]S = (E'[e1]S) B'[b] (E'[e2]S) where B'[b] lifts operations to sets, making sure their results are one of our 7 subsets of Z. In particular, we want the smallest subset Xi such that for all j1 in E'[e1]S and j2 in E'[e2]S, we have (j1 b j2) in Xi. For instance, then Pos B'[+] Pos = Pos, Pos B'[+] Zero = Pos, Neg B'[*] Neg = Pos, ... The basic theorem we want out of this definition is that if S represents s, then E[e]s in E'[e]S. This ensures the analysis is conservative. C'[skip]S = S C'[x:=e]S = S[x|->E'[e]S] C'[c1;c2]S = C'[c2](C'[c1]) Note that the definition for sequencing makes sense because C' is a total function. For conditionals, we can sometimes specialize based on E'[e]S: C'[if e then c1 else c2]S = C'[c2]S when E'[e]S = Zero C'[if e then c1 else c2]S = C'[c1]S when E'[e]S = Xi and 0 not in Xi but in the worst case, we have to consider both branches: C'[if e then c1 else c2]S = C'[c1]S + C'[c2]S (0 and i!=0 in E'[e]S) Recall that in the real denotational semantics, we took the union of the sets of pairs of stores. Here, we took the join of the abstract stores. So we know that if in one branch, x < 0 and in the other, x > 0, then in the join, we'll have x != 0 which captues both sets of stores. However, note that we also lose any information *across* variables. For instance, if we know x > y, then we have no way of expressing this in the abstract store when y maps to something like Z or { i | i != 0 }. What about while-loops? Again, we can specialize sometimes: C'[while e do c]S = S when E'[e]S = Zero but in the general case, we need to "run" the loop. In ML we might write something like this: (1) C'[while e do c]S = C'[if e then {c; while e do c} else skip]S = if E'[e]S = Zero then S else if E'[e]S = X and 0 not in X then C'[while e do c](C'[c]S) else S + C'[while e do c](C'[c]S) but alas, this won't terminate for most loops. Here's another attempt: (2) C'[while e do c]S = if E'[e]S = Zero then S else let S1 = C'[c]S in if (S1 = S) then S else if E'[e]S = X and 0 not in X then C'[while e do c]S1 else S + C'[while e do c]S1 end The check to see if S1 = S allows the loop to terminate early -- we don't need to go around the loop again since nothing will change. That is, we've found a fixed-point of the (abstract) function we're using to define C' of the while-loop. The big question is *will* the loop terminate? It's not clear that it will, but there's one way that we can force it to terminate: Simply join the input store S with the S1: (3) C'[while e do c]S = if E'[e]S = Zero then S else let S1 = (C'[c]S) + S in if (S1 = S) then S else if E'[e]S = X and 0 not in X then C'[while e do c]S1 else S + C'[while e do c]S1 end This is conservative since for all Xi and Xj, Xi <= Xi U Xj. Now it's also clear that each time we go around the loop we must be changing something in the store S1 from Xi to Xj where Xi <= Xj. But we can only change a given variable's set a finite number of times since our lattice has finite height. Furthermore, we can only change a finite number of variables in the abstract store S since the command c can only modify a finite set of variables.