CSE 428: Lecture Notes 4 A mini-language of expressions 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. rule 1 ------------------- 0 belongs to N rule 2) n belongs to N ------------------ n+1 belongs to N Example 2. rule 1 -------------- fact(0) = 1 rule 2 fact(n) = m ----------------------- fact(n+1) = (n+1) * m Example 3. rule 1) ----------- x ->* x rule 2) x -> y ----------- x ->* y rule 3) x -> y y ->* z ------------------ 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: 1 each intermediate node is labeled with an assertion (like: 0 belongs to N, or a -> b) 2 the leaves are labeled by assertions that we know already to hold (either because are axioms orassumptions, or because we have proved them already) 3 the root is labeled with the assertion we want to prove 4 if an intermediate node is labeled with assertion A, then its children are labeled with the premises of an instance of a rule in which A is the conclusion (and the side conditions, if any, are verified). The instance is obtained by intantiating the variables of the rule to appropriate elements of the domain. 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: e 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: Syntactical level: Entities are strings with no algebraic property. They are just the representation of numbers and operations. For instance, 5 + 2 (as a string) is not equal to 7 (as a string), although they represent the same value. Semantic level: Entities are values and operations with certain algebraic properties, and 5~ +~ 2~ is exactly the same as 7~. 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) ---------------------- not e eval 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 e1, x2 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 x 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 |- e 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 x 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 x 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 ------------------ -------------------- ------------------ 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]