Programming Languages (CSC-302 99S)

Notes on Assignment 3: Denotational Semantics

1. The Semantics of SIMPLE

In defining the semantics of SIMPLE, we decided on types for a number of the semantic functions and also began defining many of those functions. In this problem, you will consider how to develop semantic functions for some of the remainder of the language.

Here are the types of the semantic functions.

meaningProg : Prog -> Input -> Output
meaningSL : SL -> Cont -> Env -> Input -> Output
meaningStat : Stat -> Cont -> Env -> Input -> Output
meaningExp : Exp -> Env -> N
meaningNum : Num -> N

(note that meaningNum is built-in).

Here are the corresponding semantic domains.

Input = N* (lists of natural numbers)
Output = N* (lists of natural numbers)
Env = Ide -> N (functions from symbols to natural numbers)
Cont = Env -> Input -> Output 

You may also wish to refer to the class outline that gives an overview of the SIMPLE semantics.

1.a. The Meanings of Conditionals

SIMPLE has two kinds of conditionals.

Stat => if E then L1 else L2 fi
Stat => if E then L fi

Write equations that define the semantics of these two statements.

One question that came up had to do with how to deal with Boolean values, when all we have are numbers. The answer is to use the standard C technique of having 0 represent false and every other number represent true. Some of you used positive for true, and 0 or negative for false. That's also okay.

In case you hadn't realized it, the fi represents ``end of if''.

For the if-then-else, we evaluate the expression and compare it to 0. If it's zero, we do the false part (using meaningSL). Otherwise, we do the true part (again using meaningSL).

meaningStat [[if E then L1 else L2 fi]] =
  \cont . \env . \inp .
  (meaningExp E env) = 0 -> meaningSL L2 cont env inp 
                          , meaningSL L1 cont env inp

For the if-then-no-else, we evaluate the expression and compare it to 0. If it's zero, we go on (using the continuation). Otherwise, we do the true part (using meaningSL). To continue (in the false case), we just apply the continuation to the current environment and input.

meaningStat [[if E then L fi]] =
  \cont . \env . \inp .
  (meaningExp E env) = 0 -> cont env inp
                          ; meaningSL L cont env inp

Note that in the if-then-no-else version, you still need to do something in the case that the expression evaluates to ``false''. Some of you neglected to do this.

1.b. The Syntax of Case Statements

Suppose we decide to extend SIMPLE with a simple case statement of the following form:

case (E) of
  Num1: S1 ;
  Num2: S2 ;
  Numn: Sn

Show how we'd extend the abstract syntax to incorporate this new kind of statement.

Note that I've added esac to clarify the end of the case statement (mostly for uniformity).

As you may have noted, we have a list of things, so we'll need an additional syntactic domain for ``list of number/statement pairs''. I've decided that that list should be nonempty, as did most of you.

C in CaseList
Stat => case (E) of C esac
CaseList => V:S
CaseList => V:S ; C

A number of you failed to support case statements written in the way given in the problem. Instead, you seemed to discuss them in terms of their implementation as a sequence of conditionals. The goal was really to support case statements using the structure given above. If you did this wrong, I took off on the syntax part, but was more forgiving for semantics.

1.c. The Semantics of Case Statements

Write equations that define the semantics of the new case statement.

Note that I purposefully left some of the definition of case unspecified in the assignment. In particular, it was your responsibility as semantics designer to decide what would happen if the expression did not match any of the cases (or multiple versions of the cases).

Between the original semantics and this assignment, I moved from using V in Val to N in Num for numbers. The latter leads to some confusion with the N for natural numbers, but I'm sure you can figure it all out.

A few of you neglected to evaluate the syntactic N (using meaningNum). You need to do such evaluation before comparing to the evaluated expression.

The strategy here is to evaluate the expression and then pass it on to a ``meaning of case-lists'' function. That second function compares the value of the expression to each case value in turn. If we run out of case values, we simply continue. If there is more than one label that matches, we use the first matching label.

meaningStat [[case (E) of C esac]] =
  \cont . \env . \inp .
  meaningCases C (meaningExp E env) cont env inp
meaningCases :: CaseList -> N -> Cont -> -> Env -> Input-> Output
meaningCases [[N:S]] =
  \n . \cont . \env . \inp .
  n = (meaningExp N env) -> meaningStat S cont env inp
                          , cont env inp
meaningCases [[N:S;C]] =
  \n . \cont . \env . \inp .
  n = (meaningExp N env) -> meaningStat S cont env inp
                          , meaningCases C n cont env inpt

1.d. The Meanings of Expressions

Define meaningExp, the semantic function for SIMPLE expressions. Recall that the abstract syntax for expressions is as follows.

Exp => E1 + E2
    |  E1 - E2
    |  E1 * E2
    |  E1 / E2
    |  I
    |  N
    |  (E)
  E is an element of Exp
  I is an element of Ide
  N is an element of Num

You should have been able to find this (or something close to this) in Louden. Basically, we want to do appropriate recursive calls.

We'll start with the binary expressions. In each case, we simply need to evaluate the two subexpressions in the same environment and then apply the appropriate operation.

meaningExp [[E1 + E2]] =
  \env .
  (meaningExp E1 env) +
  (meaningExp E2 env)
meaningExp [[E1 - E2]] =
  \env .
  (meaningExp E1 env) -
  (meaningExp E2 env)
meaningExp [[E1 * E2]] =
  \env .
  (meaningExp E1 env) *
  (meaningExp E2 env)
meaningExp [[E1 / E2]] =
  \env .
  (meaningExp E1 env) /
  (meaningExp E2 env)

To get the value of an identifier, we simply look it up in the environment (which is the whole purpose of environments).

meaningExp [[I]] =
  \env . (env I)

To get the value of an expressed value (e.g., the sequence of characters 2, 1, 5), we use the ``predefined'' meaning function, meaningNum.

meaningExp [[N]] =
  \env . meaningNum N

Finally, parenthesized expressions are evaluated in the obvious way.

meaningExp [[(E)]] =
  meaningExp E

1.e. Assignment Expressions

In some languages (such as C and its descendants) it is possible to use an assignment statement as an expression, as in

while (x := x+1) do ... od

What changes would we have to make to the SIMPLE semantics to incorporate assignment expressions? Note that you need not describe changes to the concrete syntax, but just to the abstract syntax, semantic domains, and semantic functions.

Most of you did sufficiently badly on this question that (1) I did not count it in your homework grade and (2) we'll spend some time on it in class. Those of you who got it write received some extra credit on the homework (typically, raising your grade by 1/2 a letter grade).

The typical answer suggested that we need only extend the syntax and then add a new definition of meaningExp for the assignment expression, something like

meaningExp[[I = E]] = 
  \env .
  setenv env I (meaningExp E env)

However, this does not propagate the change to the environment. In effect, you're saying ``change the environment, but go on with the old environment''. Remember: this is mathematics, so there are no side-effects.

You'll need to begin with design decisions. First of all, do we allow two different kinds of assignment (one for assignment statements and one for assignment expressions). If we disallow assignment statements, we'll need to think about how to get the equivalent. Hence, it's probably best to just add the new assignment expressions and let the parser figure out the context.

There are a surprising number of changes we'll have to make. Clearly, we need to update the abstract syntax of expressions to add the rule

Exp => I = E

But the addition of assignment is more complex than that. In particular, assignment will usually update the environment. This means that the semantic function for expressions will need to take a continuation as an argument. What do we return if we take a continuation as an argument, though? One possibility is to take ``everything else'', including input, and return output.

meaningExp : Exp -> Env -> Input -> ExpCont -> Output
ExpCont = N -> Env -> Input -> Output

This also means that any place that we call meaningExp, we'll need to update the call to pass in a continuation.

Another option is to return a value/new-environment pair, and use those appropriately. For example,

meaningExp : Exp -> Env -> (Env x N)

Again, any place that we call meaningExp, we'll need to update the call to take two results and use them appropriately.

We can see this in the evaluation of expressions themselves. Before, we had not specified which argument to plus (for example) was evaluated first. Now, we need to think about whether we want it specified and, if not, how to avoid that ordering. Since it's easier to specify ordering, I'll do so in this example. You might want to consider how we might avoid doing so.

meaningExp [[E1 + E2]] =
  \env .
  (\(env1,val1) . (\(env2,val2) . (env2,val1+val2))
                  meaningExp E2 env1)
  (meaningExp E1 env)


meaningExp [[E1 + E2]] =
  \env .
    let (env1,val1) = (meaningExp E1 env) in
    let (env2,val2) = (meaningExp E2 env1) in

In the continuation version, it would be something like

meaningExp [[E1 + E2]] =
  \econt . \env . \inp .
  meaningExp E1 env inp (\n1 . \env1 . \inp1 .
  meaningExp E2 env1 inp1 (\n2 . \env2 \inp2 .
  econt (n1+n2) env2 inp2

Similarly, we'll need to make changes wherever meaningExp is called. One particularly important place is in the original assignment statement (which we've decided to keep). Another is in the write statement. We'll just look at the update to assignment.

meaningStat [[I = E]] =
  \cont . \env . \inp .
  meaningExp E env inp (\n . \env' . \inp' .
  cont (setEnv env' I n) inp'

That is, compute the meaning of the expression. Let the value be n and the updated environment and input be env' and inp'. Then continue with the program, after further updating the environment so that it maps I to n.

2. The Semantics of Scheme

2.a. The semantics of let

A close reading of the Scheme semantics suggests that there's no formal description of what happens with let. Why not?

let (in all its glorious forms) is really just a macro defined in terms of lambda and other ``primitives'' for which we've assigned semantics. For example,

(let ( (I1 E1) 
       (In En) )

is just a clearer shorthand for

( (lambda (I1 ... In) E)
  (E1 ... EN) )

The formal semantics gives a slightly different explanation.

2.b. The confusion of call/cc

Describe, in English, what's happening in the definition of cwcc in the left column of p. 43 of the Scheme semantics.

Note that there seem to be no paramters in the definition of cwcc. That's because none are needed yet. The function of two arguments is built from that

\ e k

This is then processed by onearg to make it take an E* as its first parameter. Why? Because the general model in Scheme is that you can apply any function to any argument, with the semantics telling you what happens.

What next? Starting at the top, we see that there's a test to make sure that the one argument is a function. This is to make sure that cwcc is called with the appropriate argument. Officially, the argument to call/cc (which cwcc models) is a function of one argument (with that argument being a continuation). call/cc then calls the argument, using the ``current continuation''.

What next? We grab the current store! This might mean that the ``continuation'' of call/cc includes not just the expression continuation, but also a store. (Later, we'll see that only the expression continuation seems to be used.)

Next, we need to make sure that there's room in memory. Why do we do so? Because we're creating a new function (a continuation), and need space for that function.

Next, we apply the argument (the function) to the continuation (which we still need to build) using applicate. The parameters to applicate are the function to apply, the argument list (in this case, the continuation shoved into a list), the expression continuation, and (in this case) the store. (As in previous cases, this is the hidden store from C.)

Note that there are two calls to new s. Both are expected to return the same thing. The second call updates the store so that the cell is used. The first is used in building the continuation.

We need to package up the continuation as a function (which we know that we can do because there's free memory). Recall that a function is a pair of location and ``function code''. Hence,

new s | L

allocates the location for the function.

Finally, the

\ e* k' . k e*

says exactly what you should expect. ``When you apply the encapsulated continuation (which, like everything else, we apply to a sequence of expressions with a following continuation), you throw away the new continuation (k'), and use the original continuation (k)''.


Disclaimer Often, these pages were created ``on the fly'' with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

This page may be found at

Source text last modified Mon Mar 15 10:11:59 1999.

This page generated on Wed Apr 7 16:41:05 1999 by SiteWeaver. Validate this page's HTML.

Contact our webmaster at