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.
*