MCS-287 Notes for 2006-03-21

Comments on the Tucker and Noonan book:

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 (σ01) 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.


Course web site: http://www.gustavus.edu/+max/courses/S2006/MCS-287/
Instructor: Max Hailperin <max@gustavus.edu>