Comments on the Tucker and Noonan book:
Page 72: Footnote 11 suggests giving the various semantic functions distinct names, rather than overloading them all as M. For example, a function for statements could be MStatement. This footnote errs in that the function would still need to take the particular statement as an argument. As such, the type of MStatement would still be Statement×Σ→Σ.
Pages 75-76: The last paragraph on page 75 and first paragraph on page 76 speak of "the meaning of A" and similarly for B, C, and D. However, A and the others are themselves names for the meanings, not names for expressions that have meanings. (Recall that a meaning is what the M function computes. In the case of these expressions, the meanings are integer values.) Similarly, the last paragraph on page 75 refers to "the Binary B," but B is not a Binary, it is an integer that is the meaning of a Binary.
Pages 77-78: The Java code on these pages can be modernized by using generics and by using the Visitor pattern or one of the other solutions we looked at for how to define operations on ASTs.
The entire section is marred by the fact that Tucker and Noonan's "denotational semantics" is in fact essentially the operational semantics (more specifically, the natural semantics) dressed up a little differently. I intend to spend the class period not discussing their version but rather discussing something that is more clearly a denotational semantics, as discussed below.
A denotational semantics, as that term is generally understood, assigns a meaning to each component of an AST using a function from ASTs to meanings. As such, the meaning of a statement might be a partial function from states to states; the meaning of a compound statement is built up from the meanings of the constituent statements. This is subtly different from Tucker and Noonan's version; rather than saying that a statement only has a meaning in a specified state, we have a single, universal meaning for the statement.
The function from ASTs to meanings is total; each statement has a well defined meaning. That meaning is itself a partial function, which we can view as a set of ordered pairs, where (σ0,σ1) is in the set if execution of the statement in state σ0 terminates and yields state σ1. If the statement doesn't terminate when executed in a particular state, σ, that just means that there is no ordered pair in the set that has σ as its first component. The set of pairs constitutes a function because no two pairs have the same first component; this is because we are assuming the programming language is one with deterministic execution. Nondeterministic execution can be modeled by letting the set have more than one pair with the same first component, in which case it is a relation between possible before-and-after pairs, rather than a function from the state before to the state after.
The interesting part of defining meanings is coping with looping statements or recursive procedure definitions. To illustrate the concept, we will look at something just a bit simpler than partial functions from states to states. In particular, we will consider partial functions from integers to integers. These would arise naturally if we were to develop denotational semantics for a subset of Scheme. Consider, for example, the following procedure definition:
(define g (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (else (g (- n 3))))))
The meaning associated with g
is
{(0,0), (1,1), (3,0), (4,1), (6,0), (7,1), ...}. To get rid of the
informal "..." notation, we can express this partial function as
{(3m+i, i) | m∈Z* ∧ i∈{0,1}}. (The set
Z* is the set of all nonnegative integers.)
A
denotational semantics would derive this answer as the limit of an
infinite sequence of approximations:
G0 = {}
G1 = {(0,0), (1,1)} ∪ {(n, r) | (n-3, r) &isin G0} = {(0,0), (1,1)}
G2 = {(0,0), (1,1)} ∪ {(n, r) | (n-3, r) &isin G1} = {(0,0), (1,1), (3,0), (4,1)}
G3 = {(0,0), (1,1)} ∪ {(n, r) | (n-3, r) &isin G2} = {(0,0), (1,1), (3,0), (4,1), (6,0), (7,1)}
.
.
.
Gk = {(0,0), (1,1)} ∪ {(n, r) | (n-3, r) &isin Gk-1}
.
.
.
It should be fairly clear that this infinite sequence of sets keeps
growing, with each set being a proper superset of the previous one;
thus, the limit must be an infinite set.
We say that a set G is an upper bound of the
sequence of sets Gk if it is a superset of each
set in the sequence. If G is not only an upper bound, but also a subset of
all other upper bounds, then we say it is the least upper bound of the
sequence. This least upper bound is what we mean by a limit. In this
particular case, the least upper bound is exactly the partial function
mentioned earlier as a meaning for g
:
{(3m+i, i) |
m∈Z* ∧ i∈{0,1}}.
A second example, more practical than the first one, is the standard procedure for computing factorials:
(define f (lambda (n) (if (= n 0) 1 (* (f (- n 1)) n))))
The meaning of f
can again be found as the limit of an
infinite sequence of partial functions. Not surprisingly, it turns
out to be {(n, n!) | n ∈ Z*}.
In these two examples, we were able to construct explicit descriptions of the limits. Math tells us the limit always exists, but it doesn't necessarily guarantee we will have any way to construct the limit or even prove interesting facts about it. Consider, for example, the following procedure
(define c (lambda (n) (cond ((= n 1) 1) ((even? n) (c (/ n 2))) (else (c (+ (* 3 n) 1))))))
The meaning of c
is some partial function, which is
mathematically well defined as the limit of an infinite sequence of
approximations. We could write out a general formula for the infinite
sequence, as we did earlier for Gk. Some
questions about the limit are easy to answer. For example, it isn't
hard to see that it maps every element of its domain to 1. However, if
you can determine whether the partial function is defined over all the
positive integers, you've just won yourself a prize for solving a very
difficult problem. There may not even be a way to answer that
question; for some similar procedures, the corresponding question is
undecidable, like the halting problem.