Denotational semantics describes the meaning of a program as some mathematical object: this is the program's denotation. Typically, the denotation of a program is a function that maps states to states. Given a program P, its denotation, [[ P ]], is the function that takes a state s as argument and maps it to the state that results from running P in the state s, provided that the program P terminates in the state s.
The syntax of our language is described by the following BNF definitions:
'a | 'b | ... | 'z | 'A | 'B | ... | 'aa | ...
<Num> ::= see the first hand-out for how to define numerals
<Var> | <Num> | - <Expression> |
<Expression> < <Expression> |
skip | <Var> := <Expression> |
We first give a denotational semantics for arithmetic expressions, then for Boolean expressions, and finally for programs. Before all that, we first define what we mean by a state.
In any imperative language, assignment is the most basic form of program: the other linguistic constructs (conditionals and loops) are simply ways of organising assignments into series that are executed in a particular order. For example, think of how (according to our intuitions) the following program is evaluated:
'x := 0 ; 'f := 1 ; while 'x <= 2 do 'x := 'x + 1 ; 'f := 'f * 'x odOn each pass through the loop, the value of
'xis increased by 1, and the body of the loop is gone through three times (while
2). Thus, we could "unfold" the program above to the following series of assignments:
'x := 0 ; 'f := 1 ; 'x := 'x + 1 ; 'f := 'f * 'x ; 'x := 'x + 1 ; 'f := 'f * 'x ; 'x := 'x + 1 ; 'f := 'f * 'x(one of the benefits of a formal semantics is that we could give a rigorous argument that these two programs are indeed equivalent).
Assignments, therefore, lie at the heart of imperative programming
They also lie at the heart of the semantics of imperative languages.
In particular, they suggest that any semantics should be based on
the notion of state (or storage), by which we
mean the particular values that are associated with a program's
When a program is being executed, we can think of the computer
that is running the program as being in a certain state.
This state is
determined by the values stored by the program's variables, and
these values are updated by assignments to the variables.
For example, after running the program above, the computer will
be in a state where the variable
'x has the value
3, and the variable
'f has the value
We can think of a state as being a table that tells us the value
associated with any given variable (in our language,
declared, and have no scope, so the state should tell us the value
associated with any variable.
This would give us an infinitely long table, and it is more
convenient to think of a state as a function that takes a variable
as argument and returns the value stored in that variable.
More formally, a state is a function
Var -> ZZwhere
Varis the set of variable names, and
ZZis the set of numbers (in our language, all variables store numbers). For example, after running a program
'x := 8 ; 'y := 'x + 1 ; 'z := 'y + 2we obtain a state s, with
'x) = 8,
'y) = 9, and
'z) = 11.
'z, but presumably those values aren't changed by the program.)
The semantics we will give will describe how assignments (and other programs) update states. Before we do this, note that an assignment like
'y := 'x + 1depends on the state of the computer before the assignment is executed: the value assigned to
'ydepends on the value of
'xin this "initial" state (e.g., if the value of
'xin the initial state is 8, then
'yis assigned the value 9, and if
'xhas the value 23, then
'yis assigned the value 24).
Given an arithmetic expression such as
2 * ('x + 'y)we want to describe its denotation as a number, but clearly the value of such an expression depends on the values stored in the variables
'y. In other words, the value of an arithmetic expression depends on the state of the computer when the expression is evaluated. We therefore describe the denotation of an arithmetic expression e as a function [[ e ]] : State -> ZZ. This function is defined inductively as follows:
+e2, the value should be the sum of the values of e1 and e2:
+e2 ]](s) = [[ e1 ]](s) + [[ e2 ]](s).
This is left as an exercise!
The denotational semantics for programs is given by defining, for a program P, its denotation [[ P ]]. From our intuitive understanding of programs (strengthened by having seen the operational semantics), we know that executiong a program has the effect of updating the state in which execution of the program begins. Thus, the denotation [[ P ]] will be a function that takes a state s as argument and returns the state that results from executing P in the state s. Of course, there is the possibility that P fails to terminate when it is executed in s, in which case [[ P ]](s) shouldn't return any state: in other words, [[ P ]](s) is undefined. This means that, in general, [[ P ]] is a partial function from states to states. (The denotation functions [[ e ]] and [[ t ]] for expressions and tests are both total functions from states to numbers and truth-values, respectively, as evaluating arithmetic and Boolean expressions always terminates.)
The function [[ P ]] is defined inductively as follows:
skip]](s) = s
:=e ]](s) = s[n/x],
;P2 ]](s) = [[ P2 ]]( [[ P1 ]](s) ).
elseP2 ]](s) = [[ P1 ]](s)
fi]](s) = [[ P2 ]](s)
od]](s) = s,
od]](s) = [[
od]]( [[ P ]](s) ),
if-thenconstruct (without an
else). Extend the BNF definition with an
if-thenconstruct, and give a semantics for it (i.e., extend the inductive definition of the [[
_]] denotation function). Show that any program of the form
fiis semantically equivalent to
else skip fi. I.e., show that for all states S, [[
fi]](S) = [[
else skip fi]](S).
assertT for some Boolean expression T. Add assertions to our language by specifying their syntax in BNF, and by giving them a denotational semantics.
do-return-construct for expressions (it could also be used for describing the semantics of subroutines or methods with non-void return types).
do-return-expressions are of the form
returnE, where P is a program, and E is an expression (and the whole thing is an expression, not a program). Such an expression is evaluated as follows: the program P is executed, giving a new state; the expression E is then evaluated in that state, and that gives the result of the whole
return-expression. Any further computation proceeds in the new state. For example, after executing the program
'z := do 'x := 1 ; 'y := 2 * 'x return 'y + 'x ; 'a := 'ythe variable
'ahas the value 2, and the variable
'zhas the value 3. Specify the syntax and semantics of our programming language extended with
+E2 ]](S) = (S'', n1 + n2) ,