CSE 428: Lecture Notes 4


A mini-language of expressions

Nearly all programming languages support expressions, at least basic types like numbers and booleans. Therefore it is important to understand them well. To this purpose, we will consider a language of expressions and study their semantics. We do not intend to explore all possible operations that a programming language might support, but rather to understand the general principles. Therefore we will keep the language simple.

We will consider two different types of expressions: numerical and boolean. We will use grammars as simple as possible and will not worry about ambiguities (we know anyway how to eliminate them).

Numerical expressions

We want to have the numbers, the aritmetic operators +, -, *, and / , the possibility to use parentheses, and the conditional expression. For the sake of symplicity we will indicate all the numerical (aritmentic) operators by nop. A possible grammar for this language is:
NExp ::= Num | NExp nop NExp | (NExp) | if BExp then NExp else Nexp
where BExp is a boolean expression, defined below.

Boolean expressions

We want to have the boolean constants true and false, the possibility of confronting two numerical expressions, the logical connectives and, or, and not. For the sake of symplicity we will indicate the comparison operator (<>, = , etc.) by cop. A possible grammar for this language is:
BExp ::= true | false | NExp cop NExp | not BExp | BExp and BExp | BExp or BExp

Semantics

We will use semantic specifications in SOS style. SOS means "Structural Operational Semantics", where the word structural refers to the fact that the evaluation  of a compound construct is expressed in terms of the evaluation of its subcomponent. In particular we will use here the big-step SOS (also called natural semantics).

Inductive definitions

SOS specifications are given by means of inductive definitions. Let us briefly recall this concept from CSE 260. Inductive (or recursive) definitions are a way of defining infinite  sets, functions, relations. Examples of inductive definitions are:
 
Example 1 (The set of natural numbers)
N is the smallest subset of R (the real numbers) such that:
    1. 0 belongs to N
    2. if n belongs to N, then n+1 also belongs to N
Note that in an inductive definition it is essential to specify "the smallest". Otherwise in general it would not be a good definition since there might be more than one set which satisfy the given conditions (rules). In Example 1, for instance, the set
{-1 , 0 , 1 , 2 , ... }
also satisfies the conditions (1 and 2).
Note also that an inductive definition contains variables (sometime called meta-variables), i.e. symbols which denote arbitrary objects of a given domain. Sometimes the domain is implicit. In the example above, the variable is n and the domain is R.


Example 2 (The factorial function)
fact is the least function on N which satisfies:
    1. fact(0) = 1
    2. if  fact(n) = m  then  fact(n+1) = (n+1) * m
In Example 2, the variables are n and m and the domain is N. Exercise: Define the partial order on functions which provides the appropriate notion of "least function."
Example 3 (Transitive and reflexive closure)
Let X be a given set, and let -> be a relation on X (i.e. a subset of the cartesian product X * X). The transitive closure of ->, which we will denote by ->*, is the smallest relation on X which satisfy:
  1. x ->* x
  2. if  x -> y  then  x ->* y
  3. if  x -> y and  y ->* z  then  x ->* z
In Example 3, the variables are x , y, and  z  and the domain is  N.

As we can see from the examples above, in general an inductive definition is specified by a set of rules of the form

if  premises  then  conclusion
Sometimes there are also  side conditions  which specify whether the rule applies or not (if the side conditions are not verified then we can ignore the rule).

We will use a more compact representation of rules, namely we will use the notation

  premises
____________________________        [ side conditions ]
 conclusion
The meaning is exacly as above: if the premises are verified, then we can derive the conclusion.

The definitions above can be rewritten in this notation as follows:

Example 1.

                           n belongs to N
____________________               _________________________
0 belongs to N            n+1 belongs to N


Example 2.

                             fact(n) = m
_________________                ______________________________
 fact(0) = 1            fact(n+1) = (n+1) * m


Example 3.

                x -> y       x -> y   y ->* z
_____________      _____________      ___________________________
 x ->* x       x ->* y           x ->* z


Proof trees

Given a certain inductive definition specifying a set (or a function, or a relation), in order to prove that a certain element belongs to the set (or to compute the value of the function, or to prove that two elements are related), one technique is to construct the proof-tree.  This is a tree (with its root at the bottom) such that:
Example
Consider Example 3 above, and assume the domain is the set X = {a , b , c}, and that the relation -> holds between a and b, and between b and c. I.e.:  a -> b  and  b -> c hold.
The following proof tree proves that  a ->* c  holds:
             b -> c
           _________
a -> b      b ->* c
____________________
      a ->* c

Semantics for the language of expressions

Informally, the meaning of an expression should be its value. However, even in the very simple language of expressions introduced above, the meaning of some constructs is not at all clear. For instance, everybody would agree that the meaning (value) of  2+5  is  7 and that the meaning of  2 + 1/0  is "error" (division by 0). But what should be the meaning of
1 < 0   and   1/0 < 1
Some people will find it natural to consider this an error, while some other people will find it natural to consider it a correct expression with value F (false). Their reasoning is the following: "once I have seen that the first argument of a logical conjunction is false, it is useless to evaluate the second: the result will be false anyway".

Both points of view are legitimate, and there isn't a standard interpretation in programming languages either. Some languages (like Pascal) adopt the first interpretation, some other (like Algol, C and ML) adopt the second. Finally, some languages adopt both: they have two different operators corresponding to the two different interpretations.
The first interpretation is called strict (all arguments must be evaluated) and the second is called non-strict (in certain cases some arguments are not evaluated). In general, the non-strict and (and or) operator can be quite useful, for instance when writing the control condition of a while-statement which checks that all elements of an array A [1..n] satisfy a certain condition:

   while i < n+1 and A[i] = 0 do i := i+1
This would be correct with a non-strict  and, but with a strict  and  would generate an error when  i  becomes  n+1.

As this example shows, it is necessary to define formally (rigorously) the meaning of constructs. We will use here the big step SOS approach (natural semantics). In this approach, we describe the language by describing a (very) abstract interpreter for it. We will call this interpreter eval, and the typical statement for describing the semantics of expressions will be:

eval  v
meaning that the interpreter evaluates e to the value v. Of course, eval represents a relation between expressions and values. Actually, in the case of the mini-language of expressions, eval  is a function: we could have written eval(e) = v. But we prefer to think of it as a relation for the sake of generality.

We will now define the relation eval inductively over the mini-language of expressions. We will abstract from interpretation details about basic data, in the sense that we will assume that the interpreter has its internal representation of values (numbers,  booleans) and that it knows how to operate on them. In other words, we assume that for each syntactic representation  n  of a number the interpreter gives directly (its internal representation of) its value n~, and that for each syntactic representation  cop of a numerical operation the interpreter knows how to perform the (semantic) operation  cop~ Same for comparison operations, booleans and boolean operations. This abstraction is reasonable, since the algebraic properties of numerical and boolean operations are generally well known and understood, hence there is no risk of misinterpretation there.

Note that we are dealing here with two different levels:

Below we give the inductive rules defining the relation eval for the mini-language of expressions.  We will use the following notation: T  and  F for the boolean values true and false,  /\  and  \/  for the logical conjunction and disjunction, with the usual logical interpretation. We will not bother about syntactic ambiguity: we will assume that the parsing phase has already taken place and that the expressions which appear in the rules are actually parse trees.

Rules for numerical expressions

(1)  ___________   if n belongs to Num
      n eval n~
 

       e1 eval v1       e2 eval v2
(2)  _______________________________
      e1 nop e2   eval   v1 nop~ v2
 

      e eval v
(3)  ____________
      (e) eval v
 

       e eval T      e1 eval v
(4)  _________________________________
         if e then e1 else e2   eval   v
 

       e eval F      e2 eval v
(5)  _________________________________
         if e then e1 else e2   eval   v

If the formalism looks too criptic, just remember the formulation of inductive  rules. For instance, Rules (4) and (5) are to be read as follows:

If the evaluation of e gives T, and the evaluation of e1 gives v, then the evaluation of   if e then e1 else e2  gives v
and
If the evaluation of e gives F, and the evaluation of e2 gives v, then the evaluation of  if e then e1 else e2  gives v
Note that the interpretation of the construct  if-then-else  is non-strict: the second and third argument do not always get evaluated.

Rules for boolean expressions

(1)  _____________  
      true eval T
 

(2)  ______________  
      false  eval F
 

       e1 eval v1       e2 eval v2
(3)  _______________________________
      e1 cop e2   eval   v1 cop~ v2
 

           e eval v
(4)  _____________________
      noteval  not~ v
 

       e1 eval v1      e2 eval v2
(5)  _________________________________
           e1 and  e2   eval   v1 /\ v2
 

        e1 eval v1      e2 eval v2
(6)  _________________________________
           e1 or  e2   eval   v1 \/ v2

Note that the rules (5) and (6)  induce a strict interpretation of conjunction and disjunction. If we want to define  and  as non-strict (also called short-circuit) operator, we should replace (5) with the following rules:
 

               e1 eval F
(5a)  ________________________
               e1 and  e2   eval   F
 
 

         e1 eval T      e2 eval v
(5b)  ______________________________
                e1 and  e2   eval   v
 

Analogously, if we want to define  or  as non-strict operator, we should replace (6) with the following rules:
 

               e1 eval T
(6a)  ________________________
               e1 or  e2   eval   T
 
 

         e1 eval F      e2 eval v
(6b)  ______________________________
                e1 or  e2   eval   v
 

These rules define the relation eval in the usual sense: we can prove that  e eval v iff there is a proof-tree having   e eval v  at the root.
 

Identifiers and declarations

The mini-language of expressions considered so far is not very interesting from a computational point of view: Each expression in that language represents a constant, either numerical or boolean. Thus one might wonder why to make the effort of writing, for instance, an expression like  if  4 < 5  then  2 + 1  else  0 instead of writing directly 0 ! In general, we would like to be able at least to express some functional capability, i.e to have the possibility of writing expressions whose value can change depending on the value of some variables, like for example:
if  x > 0  then  x  else  -x
which represent the function "absolute value" of x.

To this end, we will enrich the mini-language of expression with identifiers (or names). Usually identifiers used in programming languages are sequences of letters or digit starting with a letter, but anyway we will not bother here about their syntax: we will assume they are generated by a syntactic category Ide.
Identifiers are intended to denote values. Of course, we must have a way to associate values to identifiers. The standard way to do it is via declarations, i.e. by writing explicitly in the program the association between identifiers and values. In an interactive environment, the associations can be established also dynamically, and independently from the program. However here we will consider only the first option.

A declaration is a construct of the form

x = e
where  x  is an identifier and  e  is an expression. The intended meaning is that, after this declaration, the name  x  will be interpreted as representing the value of  e. More in general, we will consider sequences of such associations, like
x1 = e1 ; x2 = e2 ; ...
which means that  x1 will be associated to the value of  e1x2 will be associated to the value of  e2, etc.

Block construct and scoping

In the last paragraph above, the sentence
"the name  x  will be interpreted as representing the value of  e"
is quite vague: where and for how long should this association be considered valid? One default answer could be: for all the program. But this answer is not very satisfactory from the point of view of a programming language. Every person who is a bit acquainted with high-level programming knows the advantages of re-using the same name for different values (one killer example is the formal parameter of a procedure: each time we call the procedure, we may associate to the parameter a different value). Therefore, usually declarations are done in the context of a block construct, which establishes the scope, i.e. the range in which the declaration is active (or valid, or effective).

A block construct (for expressions) is an expression of the form

let  x = e  in  e'  endlet
where x is an identifier, and e, e' are expressions.  x = e is called declaration part and  e' is called body of the block. For the sake of generality, we will also allow sequences of declarations in the declaration part.

Scoping rule

The scope of a declaration is established by the following rule:
In the block construct  let  x = e  in  e'  endlet, the domain of activity of the declaration x = e  is the body  e' , except for those sub-expressions of  e'  which contain new declarations for x (if any).
In other words, the declaration which counts, for an occurrence of an identifier, is always the one which is in the most internal block containing such occurrence.

Let us illustrate this concept with an example. Consider the following program:

let x = 1
   in let x = 2
          in  x + 1
      endlet
   + x
endlet
According to the scoping rule above, the meaning of this expression is  4. In the body of the internal block, in fact, the value of  is  2, while in the body of the external block is 1.

Syntax

The formal description of the new constructs introduced above can be done by enriching the mini-language of expressions with the following productions:
NExp ::= Ide | let Dec in NExp endlet

BExp ::= Ide | let Dec in BExp endlet

Dec ::= Ide = Exp | Dec ; Dec

Exp ::= NExp | BExp

Semantics

The semantics of an expression containing some identifiers depends of course on what values are associated to those identifiers at the moment in which the expression is evaluated. The associations identifier-value must therefore be taken into account when describing the meaning of an expression, and we have to take into account how these associations are modified by the declarations encountered during the evaluation.
The set of associations identifier-value is called environment, and we will denote a generic environment by env. (Note: during the lectures we have used the greek letter rho instead of env.) env is actually a function, in the sense that for each identifier x it will contain at most one active association for x. We will denote the value associated to x in env by env(x). In case there is no association for x, then we say that  env(x)  is  undefined, and the attempt of evaluating x in env will return an error.
The typical statement in the definition of the semantics of expressions containing identifiers will be:
env |-  eval  v
to be read as:
in the environment  env, the evaluation of expression e gives the value v
Usually at the root of the proof-tree the environment will be empty. This corresponds to the assumption that there is no default assignment of values to identifiers. Their value can only be extablished by declarations in the program.

Rule for evaluation of identifiers

As described above, an identifier in an environment env denotes the value env(x). Therefore the rule for the evaluation of  x  will be:
________________________
env  |- x   eval  env(x)

Rule for evaluation of block expressions

Intuitively, the evaluation of an expression  let  d  in  e  endlet  in a generic environment  env  amounts to the evaluation of  e  in the environment obtained by enriching  env with the associations expressed by  d.

Let us introduce the typical notation for the evaluation of declarations, which will be defined later:

env  |-  d  evalD  env'
to be read as:
in the environment  env, the evaluation of declaration  d  produces the new environment  env'
Note: during the lectures I used the same symbol for both the evaluation of expressions and declarations, but here I thought better to distinguish them for the sake of clarity.
We can now give the rule for the semantics of the block:
 env  |-  d   evalD   env'          env'  |-  e    eval   v
______________________________________________

     env  |-  let  d  in  e  endlet    eval  v

Note that this rule makes clear that the effect of a declaration in a block is purely local, i.e. limited to the body of the block: In fact, env' exists only during the evaluation of e. Furthermore, if there are sub-blocks in e, then the corresponding declarations might update some of the associations of env'. This describes exactly the scoping rule informally introduced above.
 

Rules for evaluation of other expressions

The rules that we have seen in LN 3 for the other productions must be enriched with the environment so to take into account the possible presence of identifiers in sub-expressions. Since the evaluation of identifiers requires knowing the environment, we will just have to propagate the environment in the evaluation of the sub-expressions. For instance, the rule for a generic numerical operator nop becomes:

 env  |- e1   eval   v1        env  |-  e2   eval   v2
__________________________________________

    env  |-  e1  nop  e2   eval   v1 nop~ v2

Analogously for all the other rules.

Implementation of the scoping

In real implementations, the nesting of environments and the scoping rule are dealt with by using a  stack (i.e. a data structure with a LIFO access strategy), in the following way:
  1. whenever a new block is opened (i.e. the  let  keyword is encountered), the declaration part is evaluated, and the corresponding associations are inserted in (the top of) the stack
  2. whenever a block is closed (i.e. the  endlet  keyword is encountered), all the associations relative to that block are removed from (the top of) the stack
  3. when an identifier  has to be evaluated, the interpreter determines its value by considering the first association for  x  in the stack, starting from the top.

Rules for evaluation of declarations

Single declarations

When a declaration  x = e  is  encountered, the interpreter evaluates  e, and the resulting value v is associated to x. More precisely, the current environment  env is enriched (or updated) by the association (x,v). We will concisely represent the updated environment with  the notation  env[v/x]. This represents the function which gives v on x, and behaves the same as  env on all the other identifiers. Formally:
env[v/x] (y) = v       if   y =x
env[v/x] (y) = env(y)  otherwise
We are  now ready to give the rule of the semantics of a single declaration:
        env |-  e    eval   v
_____________________________

 env  |-  x = e   evalD   env[v/x]

Sequences of declarations

When a sequence of declarations is encountered, the effect on the environment is simply the accumulation of the updates introduced by each declaration, considered one after the other in the order in which they are written. This is expressed formally by the following rule:
 
  env  |-  d1   evalD   env1           env1 |-  d2   evalD   env2
_____________________________________________________

                   env   |-  d1 ; d2    evalD   env2

Example

Consider again the expression:
let x = 1
   in let x = 2
          in  x + 1
      endlet
   + x
endlet
Let us prove formally, with a proof-tree,  that the value of this expression is  4 , in any environment env. The tree is the following (to see the figure correctly, use Courier (Adobe) 10pt as fixed width font)
                               ____________________     __________________   ___________________  
                                env' |- 2 eval 2~       env" |- x eval 2~    env" |- 1 eval 1~  
                            _________________________    _______________________________________  
                            env' |- x = 2  evalD env"             env" |- x+1 eval 3~  
    ___________________     _________________________________________________________         __________________  
     env |- 1 eval 1~                 env' |- let x = 2 in x+1 endlet eval 3~                  env' |- x eval 1~  
 ___________________________   __________________________________________________________________________________ 
  env |- x = 1 evalD env'                            env' |- let x = 2 in x+1 endlet + x eval 4~ 
 _________________________________________________________________________________________________  
                env |- let x = 1 in let x = 2 in x+1 endlet + x endlet   eval  4~ 

where env' = env[1/x] and env" = env'[2/x]