<<

COS 360 Programming Languages

Formal Semantics

Semantics has to do with meaning, and typically the meaning of a complex whole is a function of the meaning of its parts. Just as in an English sentence, we parse a sentence into subject, verb, objects, adjectives(noun qualifiers) and adverbs(verb qualifiers), so with a program we break it up into control structures, statements, expressions, each of which has a meaning. The meanings of the constituents contribute to the meaning of the program as a whole. Another way this is put is to say that the semantics is compositional en.wikipedia.org/wiki/Denotational_semantics#Compositionality

Consequently, the grammar of a language guides the exposition of its meaning. Each of the three approaches to formal semantics, axiomatic, operational, and denotational, will use the grammar as a structuring device to explain the meaning. The grammar used, however, is not the same one that the compiler’s parser uses. It eliminates the punctuation and just contains the relevant, meaning carrying pieces and is often ambiguous, but the notion is that the parsing has already taken place and the parse tree simplified to what is referred to as an abstract syntax tree.

Learning Objectives

1. to understand the difference between the abstract syntax trees used in expositions of semantics and the concrete syntax trees, or parse trees, used in parsing

2. to understand how the abstract syntax tree contributes to the explanation of program- ming language semantics

3. to understand the three major approaches to semantics: ax- iomatic, operational, and denotational

4. to understand how axiomatic semantics can be used to prove algorithms are correct

5. to understand how small step operational semantics define computation sequences for programs from abstract syntax trees 6. to understand how the semantic evaluation functions of define the meanings of abstract syntax trees in associated semantic domains and how to define such semantic evaluation functions

7. to appreciate the different roles with respect to a programming language: implementer(compiler writer), user (programmer), and designer

8. to appreciate how the three approaches have different emphases that favor one role over another

We make a few preliminary observations. We say “formal” semantics because the explication is intended to be very precise, complete, and based on formal notations(a programming language itself is a formal language), like the formal notations in and logic. The alternative would be “informal”, which could still be of value, but wouldn’t have the same degree of rigor.

Based as it is on mathematics and logic, the formal description of programming language semantics is one of the most technically sophisticated areas of . Many of the contributors to its development, such as Robert W. Floyd, C. A. R. Hoare, , , , and have been recipients of the ACM , the highest recognition for an academic computer scientist. We are not going to go into this area at great depth, which could easily occupy several graduate level courses in computer science, but will attempt to give you a sense of how the three main approaches proceed, using the same simple example in each.

For imperative languages, a key notion is that of an environment, which is an association of values to variables. The program’s meaning is a map from some specific initial environment to another environment, the environment the machine is left in when the program terminates. Suppose the program is C (for “Command”), and let E stand for the set of all possible environ- ments, augmented(“lifted” is the verb that is used) to include a special value, the “undefined environment”, which we symbolize with ⊥. This ⊥ value is needed to cover the case when a program does not terminate; it is the result image environment of an initial environment that induces the program to go into an infinite loop. It is just a way for us to make the definition of the program’s function into a total function, defined on all environments in E. The function C computes from E to E, which we name fC because it is induced by C, is defined to be, for e ∈E

fC (e) = ⊥ if e = ⊥ (a program cannot recover from undefined) ⊥ if e =6 ⊥ and C, when started in e, fails to terminate e′ if e =6 ⊥ and C terminates in e′ when started in e

Since the focus is on the computation, the i/o is typically ignored in these discussions. The models assume the variables have been loaded with their appropriate input values and then exposition confines itself to just environment changes wrought by assignments, expression evaluation, and flow of control.

As indicated above, the parse trees used in discussions of programming language semantics are not the same as the parse trees associated with a grammar for the language. Languages typically use parentheses to force certain orders of evaluation, but once the parse has taken place so that the higher priority subexpression is lower in the tree, the parentheses are not needed any more and just clutter up the tree. Like the Lone Ranger and Tonto at the end of an episode, their work is done and it’s time for them to vanish. The trees used in semantics are called abstract syntax trees and are usually very spare with ambiguous grammars. You can imagine that the real parsing has already been done and the resulting parse trees have been cleaned up.

Here’s a stripped back grammar for abstract syntax trees of an imperative programming lan- guage whose only type is the integers. The start symbol is .

::= ::= | | | skip | ::= id <- ::= while ::= if | if else ::= and | or | not | ::= < | <= | > | >= | = | != ::= id | c | + | - | * | / | mod | -

This language has terminals while if else and or not <- < <= > >= = != + - * / mod id c skip

The first three are reserved words to indicate the control statements and mark the second branch of an the if statement. The next three are reserved words for the boolean operators. The next one is the assignment operator. The next six are numerical comparison operators. The next five are the arithmetic operators that return integer results. Note that - is overloaded as a binary and a unary operator and that mod is for the integer remainder of a division. The last two, id and c, are for identifiers and integer constants. We will assume that these have attributes for the actual string of the variable identifier(id.s) and the actual value of the integer constant(c.v). The last terminal, skip, is a “do nothing” statement that has the same meaning as x <- x. It has a technical use below.

You will note the obvious ambiguity in the replacements for the expression syntactic category, . Similarly, the variable for boolean valued expressions has an ambiguous collection of replacements.

Expression evaluation in this language, for both boolean valued expressions and integer valued expressions, does not have side effects to the environment, which is unlike constructs such as ++n and similar expressions of C and its descendants. The only action that changes the envi- ronment is the execution of the assignment statement, and it changes only a single variable’s value. If we have an environment σ ∈E and a statement id <- that is executed in the environment σ and n is the value that evaluates to in σ (which we might indicate with σ() by extending σ to be a map from not just the variables, but from all expressions), it is common to use a notation like σ[id.s 7→ n] or σ[n/id.s] to indicate the new environment that results from the assignment. It is exactly like σ except at the variable id.s, which it maps to n.

We could lift the space of integer values to include ⊥ to accommodate the result of dividing by zero, or taking the remainder of a division by zero, and have the environment associate variables to values in the lifted space. We could then propagate the error to have the result of assigning ⊥ to a variable be the undefined environment. Those are design decisions for the language designer who is specifying the meaning of the language.

Here is a simple program in this language that calculates into result the value of base raised to the power exponent, if exponent is not negative, or just 1 if it is. result <- 1 while exponent > 0 if exponent mod 2 = 1 result <- result * base base <- base * base exponent <- exponent / 2 Because the punctuation is mostly thrown out, I have used indentation to indicate the subordi- nation of statements. The program has two statements, the first one an assignment statement, and the second one a while statement. The while loop body has three statements, the first one an if with no else branch, and then two assignment statements.

A It would be very difficult for me to draw the abstract syntax tree for this program in LTEX, but I did draw it by hand and it has 63 nodes. Since it has 28 terminals, it has 35 variable nodes. Once you accept that the while loop and if contain the statements given by the indentation, the only ambiguity comes from the ::= replacement, since we could group the three statements of the while loop body as two followed by one or one followed by two. It is advisable for you to also draw the abstract syntax tree for this program.

The three main approaches are, in rough historical order of development with some of the major contributors names given in parentheses, are

1. axiomatic(Floyd, Hoare)

2. operational(Jones, Plotkin)

3. denotational (Scott, Strachey)

We consider each in turn.

I. Axiomatic Semantics

This approach is used to verify that a program does agree with its specification, which is given as a precondition characterizing acceptable initial states (like x =6 0, say, before a division by x), and a postcondition that characterizes the goal state when the program terminates, if it terminates. The intention is that the precondition gives needed assumptions for the program to work correctly, and the postcondition describes the transformation that the programmer wants to achieve. Early contributors to this approach were R. W. Floyd, C. A. R. Hoare, and E. W. Dijkstra, each of whom won the Turing Award.

The original idea of annotating a flowchart program with boolean valued assertions at certain control points and then proving that if the state of the variables satisfy the assertion at the start of a path, then they must also satisfy the assertion at the end of the path was Floyd’s. Hoare basically adapted this idea to the syntax rules of a grammar for a structured programming language.

There is a fairly good account of Hoare’s work at https://en.wikipedia.org/wiki/Hoare_logic that you should consult. Hoare provided an inferencing system for assertions known as Hoare triples of the form

{P } C {Q} where P is an assertion, the precondition, Q is an assertion, the postcondition, and C is some executable statement or statement sequence. The intended meaning of such a triple is that if C begins execution in an environment that satisfies P and also C does terminate, then the final environment will be one that satisfies Q.

For our little example, we might have for P exponent = e ∧ base = b ∧ e ≥ 0 and for Q result = be and if C is our program, we would be able to use Hoare’s rules to prove that the triple for this P , Q, and C is true.

Before we continue with how that is done, I want to point out a couple of items.

First, you will note the use of b and e in the precondition and postcondition to refer to the initial values of base and exponent. Because the values of the variables are changing with the execution of the program, if we did not have a mechanism to refer to the initial input values, we could ignore them and change the variables to satisfy the postcondition in some simple way. The program base <- 1 exponent <- 0 result <- 1 always terminates in an environment satisfying result = baseexponent but the program has really cheated by ignoring the original input.

Second, the meaning of the Hoare triple is only for what is called partial correctness, in that the implication includes in its assumption that the statements terminate. Any program that fails to terminate is automatically partially correct with respect to any P and Q, because false implies anything. So, while 1 = 1 n <- n is partially correct for P = true and Q = false because this program does not terminate.

The reason for this oddity is that the techniques used for proving termination are different and so a proof of termination is conducted separately. You can review all of the inference rules for Hoare logic at the Wikipedia site. We’ll just mention two of them here, the rule for assignment and the rule for the while statement. Note, inference rules are given by putting needed premisses above a line and then the deducible conclusion of the rule below the line. Typically the rules are given as patterns, and the idea is that in a proof, if you have triples that are instances of the patterns above the line, then you can add the corresponding instance of the pattern below the line. The inference rule for the assignment statement is

{P [e/id.s]} id <- e {P } where P is any assertion whatsoever, id is any variable identifier whatsoever, and e is any integer expression whatsoever.

Since the inference rule has no required formulas above the line, it is an axiom. The notation P [v/x] means that the (free) occurrences of the variable x in P are replaced by the expression v. This is a purely syntactic substitution, not the same as the modification made to an environment by the assignment statement in that the expression e is not evaluated. The meaning of the entire triple is that if the initial environment satisfies the formula {P [e/id.s]}, then after the assignment id <- e, the environment will satisfy P . This has to be true, because after id <- e the variable id.s will have the value of e in that prior environment. A simple example may make this clear. Consider the triple {z + 10 > 5} x <- z + 10 {x > 5} If I want x to be greater than 5 after the assignment, I better be in an environment where z + 10 is greater than 5 before the assignment, since z + 10 is the new value for x.

The really critical rule is the one for the while loop; the while statement is the crux of any exposition of semantics because it is the most powerful statement. In axiomatic semantics, the important concept is that of a loop invariant assertion, I. If I is an assertion that when it and the loop test are both true, the execution of the loop body will maintain it as true, then one can conclude that if the loop is started in an environment in which I is true and the loop terminates, it will be in an environment where I is still true and the loop test is false. That’s the essence of the following rule.

{I ∧ test} C {I} {I} while test C {I ∧ ¬test}

This will lead to reasoning about loops in a way that is very much related to a proof by induction. If you establish that I is a loop invariant for a loop body and the loop test, the basis case is to show that when control first reaches the loop, I is true. That it is a loop invariant is the induction step that shows if I is true after n turns through the loop and the loop is entered again, it will be true after n + 1 turns through the loop.

For our little example, the critical loop invariant is 0 ≤ exponent ∧ result × baseexponent = be When the loop exits, the negation of the test, exponent ≤ 0, and the invariant will give us 0= exponent ∧ result × baseexponent = be and since exponent is zero, baseexponent is 1, and that gives us result = be I will not do the rest of the proof, but except for the loop invariants, whose discovery sometimes requires a deep understanding of the loop, the backward propagation of the postcondition backwards through the code to obtain what is termed the weakest precondition, is algorithmic. If someone were to provide the right loop invariant assertions, one could automatically generate what is called the verification condition for the program and its pre- and postconditions. The verification condition is a formula of logic that is NOT a Hoare triple, but a plain old predicate calculus formula involving integers whose truth is equivalent to the partial correctness of the program with respect to the pre- and postconditions. The Manna book cited in the reference list below gives the details of this backsubstitution operation.

Although I said that the meaning of the entire program is the function on the lifted E that it computes, this approach does not really give us a function. To turn the axiomatic approach into a way of characterizing the meaning of the entire program, the statements are regarded as predicate transformers. We will use P for the powerset operator. Suppose we have a postcondition Q and a program C. We would like to identify the subset S of E such that {currentEnv ∈ S} C {Q} is true, and that S is the largest such set, that is, if T ⊆E is a proper superset of S, then {currentEnv ∈ T } C {Q} is false, where I have use currentEnv ∈ ... to mean that the initial environment is an environ- ment in .... Clearly, any environment e that C does not terminate on is in S, but also in S are any environments that C does terminate on with the final environment satisfying Q.

For any statements without loops, we can by backsubstitution obtain from the predicate Q a predicate P that exactly characterizes this set S, and that is the so called weakest precondition of Q with respect to C. There may be a result that says that such a P always exists for any Q and any program C. I haven’t read this literature for a while and don’t recall all its nuances. There is a rich and subtle connection between programming and logic that goes back to G¨odel. In any event, the axiomatic approach can characterize the meaning of a program independently of specific pre- and postconditions by considering it as a defining a function from a postcondition assertion to its weakest precondition.

In my view, the reasoning techniques are a more valuable contribution of this approach than the characterization of programs as predicate transformers. In normal practice, the postcon- dition spec would precede coding, and coding would aim at realizing the relation given in the postcondition, that is, in practice we start with the postcondition and try to develop the program. We are not given the spec and the program and tasked with developing the pre- condition. David Gries’s book The Science of Programming may have some techniques for using these ideas to help with program development from a postcondition, and the reasoning techniques illustrate how one can make a compelling argument that a program does satisfy its specification. Here is a reference list for the axiomatic approach.

1. Dijkstra, A Discipline of Programming

2. Dijkstra, Guarded Commands, Nondeterminacy and Formal Derivation of Programs at www.cs.unm.edu/~cris/481/dijkstra-guarded.pdf

3. Floyd, Assigning Meanings to Programs at classes.soe.ucsc.edu/cmps290g/Fall09/Papers/AssigningMeanings1967.pdf

4. Gries, The Science of Programming

5. Hoare, An Axiomatic Basis for Computer Programming at www.cs.cmu.edu/~crary/819-f09/Hoare69.pdf

6. Manna, Mathematical Theory of Computation

II. Operational Semantics

Operational semantics typically works by characterizing the execution sequence of a program’s operations. I think this is the way we first learn a programming language: its components are described as having some meaning in a conceptual model of the machine. For years the operational approach was the step child of formal semantics because it is so low level, but , in his paper A Structural Approach to Operational Semantics, available at homepages.inf.ed.ac.uk/gdp/publications/sos_jlap.pdf introduced a version of operational semantics more closely tied to the abstract syntax trees. Another, much shorter paper by Andrew Myers that gives the gist of this approach is given at www.cs.cornell.edu/courses/cs6110/2009sp/lectures/lec05-fa07.pdf

You should definitely take a look at that paper, which explains both “small step” and “big step” semantics. We will look at small step and follow Myers’ discussion there.

A configuration is a pair, (C,σ), where C is the list of the syntax trees of the statements of a program and σ is an environment. A deterministic transition relation, −→, is defined on the set of configurations, based on two kinds of transitions.

The first kind, (C,σ) −→ (C′,σ), changes the program without changing the environment. One such change is when the leading statement of the program is an assignment statement and the expression is being evaluated. For example, (n<-2+3,σ) −→ (n <- 5,σ) If the expression were more complicated, then a unique, specific subexpression within it would be simplified. The rules are written to force a specific, deterministic order of simplification. The rules for simplifying integer and boolean expressions are specified so that each kind of expression can be gradually evaluated with respect to the current environment, σ, to some specific integer constant c, or some boolean constant b.

Once the assignment statement had been reduced to n <- c for some constant c, the second kind of transition would modify the environment σ to reflect the assignment of c to the variable, and replace the assignment statement with skip. For example, (n <- 5,σ) −→ (skip,σ[n 7→ 5]) If skip is the first statement of the program and there are statements after it, it can be discarded with no change to the environment. If it is the last then it is retained and the pair is a terminal configuration. That is how Myers uses it. An alternative to this use of skip would be to accept an empty list as the first component of a configuration and regard that as a terminal configuration.

The so called “small step semantics” has transitions for each subexpression evaluation. You can imagine the whole abstract syntax tree for the first statement of the list of statement in the first component of a configuration. If the next statement is an assignment statement with a complicated expression, or an if statement with a complicated test expression, the expression’s part of the abstract syntax tree is gradually simplified until it is eventually reduced to a constant. Then the assignment can take place and modify the environment as illustrated above, or the if statement can be reduced to the appropriate one of its branches (skip when it has no else and the boolean constant is false).

You can see more of the transition rules in the Myers paper referenced above. We want to look at how the while statement is dealt with. The rule for the while statement is

(while ,σ) −→ (if while else skip,σ)

This is a syntactic replacement that embeds a copy of the while loop in an if statement, and you can see how it works. If the test expression evaluates to true, then the true branch of the if, while will be executed. It will execute the loop body statement, , and then the loop will come up again. If the test expression evaluates to false, then the false branch of the if, skip, will be executed, but it does not change the environment. By embedding a copy of the loop this way, the configuration can allow for as many iterations as the loop needs, and, of course, if an infinite loop were taking place, the skip branch would never be chosen.

It is cumbersome to show an entire execution sequence, but I will show a few snapshots of the our little program for the initial environment where base is 2, exponent is 7, and result is 10. I will show the environment as an ordered triple, (base, exponent, result). The first configuration is

(result <- 1 while exponent > 0 ..., (2, 7, 10))

where I have put an ellipsis in the program part of the configuration because I cannot fit the whole program on the line. Using the rule for an assignment whose expression has been reduced to a constant, the very next configuration is

(skip while exponent > 0 ..., (2, 7, 1))

in which you note the environment has been modified so that result’s component is now 1. Then we have

(while exponent > 0 ..., (2, 7, 1))

which by the while transition becomes

(if exponent > 0 if exponent mod 2 = 1 ... while exponent > 0 ... else skip, (2, 7, 1))

then, after evaluating the first if test and selecting the true branch, which would require three steps, we reach

(if exponent mod 2 = 1 result <- result * base ... while exponent > 0 ... , (2, 7, 1))

after evaluating the test to true and selecting the true branch, another four steps, we reach

(result <- result * base base <- base * base exponent <- exponent / 2 while ... , (2, 7, 1)) after another three steps we reach (result <- 2 base <- base * base exponent <- exponent / 2 while ... , (2, 7, 1)) and then(note the change to result in the environment) (skip base <- base * base exponent <- exponent / 2 while exponent > 0 ... , (2, 7, 2)) and then (base <- base * base exponent <- exponent / 2 while exponent > 0 ... , (2, 7, 2)) The execution of the next two assignment statements would eventually take us to (while exponent > 0 if exponent mod 2 = 1 ... , (4, 3, 2)) which completes the first turn through the loop. At this point the while transition yields (if exponent > 0 if exponent mod 2 = 1 ... while ... else skip, (4, 3, 2)) Because exponent is still positive, we will select the true branch and execute another turn through the loop, yielding eventually, (while exponent > 0 if exponent mod 2 = 1 ... , (16, 1, 8)) and as exponent is still positive, we do another turn through the loop leading to (while exponent > 0 if exponent mod 2 = 1 ... , (256, 0, 128)) this time when the while loop expands (if exponent > 0 if exponent mod 2 = 1 ... while ... else skip, (256, 0, 128)) the if test evaluates to false, so eventually the false branch will be chosen, and we reach (skip, (256, 0, 128)) a terminal configuration.

There is an alternative “big step” version of structural operational semantics that defines transitions to the final values: integer constants for numerical expressions, boolean constants for boolean expressions, and terminal configurations for initial configurations. It has the same kind of recursive, rule-based definition.

You should not be disappointed if this characterization seems obvious to you. That just means you understand well the meaning of programs in such a language. Think instead of this as a way to specify the meanings of new features we might add to a language. Certainly, by explaining in minute detail how the language executes we would nail down their meanings. Even so, grasping the whole meaning of a program composed of many statements would be difficult. The rules of big step semantics can define the transition to a terminal configuration, but they do not actually provide a high level description of what the program does. The big step semantics do not reduce the complexity of a large program.

Apart from the papers I referenced above, I see there are books devoted to structural oper- ational semantics by Maribel Fern´andez and also Hans H¨uttel listed on Amazon. I have not read these, so I cannot say more than their publishers are well known for good technical books.

It is my understanding that structural operational semantics was very helpful in solving some problems in the semantics of concurrent programming languages.

III. Denotational Semantics

The most elegant exposition is the denotational approach, pioneered by Dana Scott and . Scott received the Turing award in part for this work. The general idea is that each syntactic category A and terminal T has an associated set of possible mean- ings, A and T , say, and for each rule R

A ::= X1 ··· Xn there will be an associated interpretation function, fR,

fR : X1 ×···Xn −→ A that takes the meanings of the Xi in their associated semantic domains, Xi, to the meaning for the node labeled with A in its semantic domain A. The meaning of an A instance derived via this rule is determined from the specification of fR. Denotational semantics uses a special double square brackets for notation to refer to the specific meaning of a syntactic fragment (variable or right hand side of a replacement), as on page 274 of http://homepage.divms.uiowa.edu/~slonnegr/plf/Book/Chapter9.pdf

A but LTEX does not have that symbol so I will use just the double square brackets, [[]]. The notation then is fR[[X1, ··· , Xn]].

The meaning of an entire tree propagates from the meanings of the leaves to the root, just like it would in an expression tree.

I do not want to go into all of the details of this as the mathematics gets pretty esoteric, but I will mention a few features. First, the domains are often function spaces, with the notation [A −→ B] used for the set of functions from a set A to a set B. In particular, the semantic domain for and is [E −→ E] as we described above. The semantic domain for of our little language would be [E −→Z] and for it would be [E −→B] where Z and B are the sets of integers and boolean values respectively. The semantic domain for id is string (or variable, if you wish), and for c, Z.

Second, and this might be familiar to those with some experience with JavaScript, there is a notation for function valued expressions involving λ that goes back to Church’s . Essentially, λx : T.e is a function that takes an input value v of type T in the parameter x to the value of e with x replaced in e by v. For example, (λx : Z.2x + 7)3 evaluates to 2 · 3 + 7, or 13. This is a very handy notation for defining anonymous functions and we will see it again when we look at the programming language ML. Here is how it could be used for the production ::= and in our little language(the multiple occurrences of the same grammar variables have been labeled to distinguish the occurrences). Suppose T is the semantic evaluation function for . We have T [[ and ]] = λσ : E.if σ = ⊥ then false else

T [[]]σ ∧ T [[]]σ

There are a couple of observations to make about this definition. 1. Because so many of the domains involve function spaces or higher order function spaces, to reduce the parentheses it is common to treat the application of a function to a value as a high priority operation that is indicated by juxtaposition, associate it left to right, and use fewer parentheses. So, f(x) is written as fx, and Hxy would be construed as (Hx)y. If Hxy is well-typed, it implies that there are sets A, B, and C such that H is of type [A −→ [B −→ C]], x is of type A, and y is of type B, so that Hxy is of type C. In this example, T [[]]σ is a boolean value.

2. It uses a conditional expression if ... then ... else ..., like the (test? trueExp : falseExp) of C and its descendants.

3. Because E is lifted, the definition has to deal with ⊥. Since we can arrange the semantic evaluation function for to never use the environment in expression evaluation if it is ⊥, we can just have the functional value here return false when the input environment is ⊥. An alternative would be to lift B, and return the undefined boolean value. 4. Because the associated semantic domain is [E −→ B], before we can use the binary boolean operator ∧, we have to evaluate the meanings of the subexpressions as functions in [E −→ B], in the environment σ. The σ is the formal parameter to the meaning of the right hand side as a functional value in [E −→B].

5. The overall rule translates the syntactic and to the familiar boolean operation ∧. Gen- erally, that is the way the rules proceed: some syntactic component is given a more abstract meaning in some mathematical domain.

Third, the breakthrough that Scott achieved was to establish semantic domains where cer- tain recursive definitions were guaranteed to always have unique, minimal solutions. This was a stunning result and it ensured that the translation of the while statement, which is similar to the syntactic recursion described above in the operational semantics section, would have a unique value in the associated semantic domain [E −→ E]. Essentially, if the loop is while , and t is the meaning of and f is the meaning of , then the meaning of the whole while statement is the unique, minimal g in [E −→ E] that satisfies

g = λσ : E.if σ = ⊥ then ⊥ else (if tσ then (g ◦ f)σ else σ) thus g is a fixed point of the function, Φ : [E −→E] −→ [E −→E],

Φ= λh :[E −→E].λσ : E.if σ = ⊥ then ⊥ else (if tσ then (h ◦ f)σ else σ) because Φ(g) = g. Note the similarity to the operational definition. If the test evaluates to true, then one turn through the loop, f, is applied, and then the meaning of the while loop, g, is applied to the result. Denotational semantics is sometimes referred to as fixed point semantics because of its reliance on the existence of a unique, minimal solution to such recurrence relations.

Here is a reference list for denotational semantics.

1. Schhmidt, Denotational Semantics (highly recommended; out of print but available on the internet at

people.cs.ksu.edu/ schmidt/text/DenSem-full-book.pdf

)

2. deBakker, Mathematical Theory of Program Correctness

3. Gunter, Semantics of Programming Languages

IV. Concluding Remarks The different approaches here all are aiming at the same target. It is not as if a program has one meaning if you use the operational approach and an entirely different one if you use the axiomatic approach. The different approaches take a slightly different perspective on the same single meaning.

There are several distinct constituencies involved in programming language definition: the designer constructing the language, the programmers who will use it, and the compiler writers who must write the translator programs for it. Each constituency needs to understand what the constructs of the language mean and they should all agree if the programming language is going to work well in practice.

In my view, the operational approach is the most help to the compiler writer for the language, the axiomatic approach is the most help to a programmer who wants to reason about the correctness of the code, and the denotational approach is most useful to the language designer, as it encourages the designer to use constructs with a straightforward, grammar based ex- plication. The techniques of denotational semantics are robust enough to provide a precise mathematical semantics for a language with goto’s, but the complexity of the definitions that are needed is a clear red flag that the goto is feature that would be hard to use correctly.

In practice, formal discussions of the semantics of a language are very tedious to read, but can be useful in program design because they force you to fully explicate what you think a construct means. I always advise students to not use constructs they do not fully understand.

Practice exercises

1. Consider the program

sum <- 0 i <- 1 while i <= n sum <- sum + i i<-i+1

Prove the program partially correct with respect to the precondition n = m and the postcondition

(m< 0 ⇒ sum = 0) ∧ (m ≥ 0 ⇒ sum = X j) 1≤j≤m

The key, of course, is to determine an appropriate loop invariant. 2. Consider the program

product <- 1 i <- 2; while i <= n product <- product * i i<-i+1

Prove the program partially correct with respect to the precondition n = m and the postcondition

(m< 2 ⇒ product = 1) ∧ (m ≥ 2 ⇒ product = m!)

3. Consider the program

divisor <- 2 while divisor < n and n mod divisor != 0 divisor <- divisor + 1

Prove this program partially correct with respect to the precondition n = m and post- condition

divisor = m ⇒ m is prime

and the precondition true. For the definition of prime, you can use

m ≥ 2 ∧ ∀d (2 ≤ d

Hint: part of the invariant is that for all d, if 2 ≤ d < divisor, then d does not divide n evenly.

4. Show the first 25 configurations of the small step computation sequences for

a. the loop of the first problem and an n value of 2. b. the loop of the second problem and an n value of 2. c. the loop of the third problem and an n value of 3.

5. Give appropriate denotational semantic evaluation rules for the following productions. I have labeled the multiple occurrences of the grammar variables.

a. ::= c b. ::= id c. ::= + d. ::= > e. ::= id <- f. ::= skip g. ::= Hint: use functional composition. h. ::= if then else

For the statement productions, you should first test if the initial environment, σ is ⊥, and if it is, the result should be ⊥ as well. This is alluded to in the discussion of the semantics of and , above.

6. Add a grammar rule to replace with a conditional expression like what is available in C and its descendants, and define a semantic evaluation function for this new construct.

7. Add a grammar rule to replace with a conditional expression like what is available in C and its descendants, and define operational transitions in the style of the Myers paper for this construct.