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:
|
<Var> ::=
'a | 'b | ... | 'z | 'A | 'B | ... | 'aa | ...
<Num> ::= see the first hand-out for how to define numerals
<Expression> ::=
<Var> | <Num> | - <Expression> |
<BooleanExpression> ::=
<Expression> < <Expression> |
<Program> ::=
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
od
On each pass through the loop, the value of 'x is
increased by 1, and the body of the loop is gone through three times
(while 'x has values 0, 1,
then 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
languages.
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
variables.
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
6.
We can think of a state as being a table that tells us the value
associated with any given variable (in our language,
variables aren't
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 -> ZZ
where Var is the set of variable names,
and ZZ is the set of numbers (in our language,
all variables store numbers).
For example, after running a program
'x := 8 ;
'y := 'x + 1 ;
'z := 'y + 2
we obtain a state s, with
'x) = 8, 'y) = 9, and 'z) = 11. 'x, 'y or '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 + 1
depends on the state of the computer before the assignment
is executed: the value assigned to 'y depends
on the value of 'x in this "initial" state
(e.g., if the value of 'x in the initial state
is 8, then 'y is assigned the value 9,
and if 'x has the value 23, then
'y is 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 'x and '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) ). if T then P1 else P2 ]](s) = [[ P1 ]](s)
if T then P1 else P2 fi ]](s) = [[ P2 ]](s)
while T do P od ]](s)
= s,
while T do P od ]](s)
= [[ while T do P od ]]( [[ P ]](s) ),
if-then construct
(without an else).
Extend the BNF definition with an if-then construct,
and give a semantics for it (i.e., extend the inductive definition
of the [[_]] denotation function).
Show that any program of the form
if T then P fi
is semantically equivalent to
if T then P
else skip fi.
I.e., show that for all states S,
[[ if T then P fi ]](S) =
[[ if T then P
else skip fi ]](S). assert T
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
do P return E,
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 := 'y
the variable 'a has the value 2, and the variable
'z has the value 3.
Specify the syntax and semantics of our programming language
extended with do-return expressions.
+ E2 ]](S) =
(S'', n1 + n2) ,