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).
NExp ::= Num | NExp nop NExp | (NExp) | if BExp then NExp else Nexpwhere BExp is a boolean expression, defined below.
BExp ::= true | false | NExp cop NExp | not BExp | BExp and BExp | BExp or BExp
{-1 , 0 , 1 , 2 , ... }
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 conclusionSometimes 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
premisesThe meaning is exacly as above: if the premises are verified, then we can derive the conclusion.
____________________________ [ side conditions ]
conclusion
The definitions above can be rewritten in this notation as follows:
- b -> c
- _________
- a -> b b ->* c
- ____________________
- a ->* c
1 < 0 and 1/0 < 1Some 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+1This 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 vmeaning 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:
^{
}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 vNote that the interpretation of the construct if-then-else is non-strict: the second and third argument do not always get evaluated.
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
(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.
if x > 0 then x else -xwhich 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 = ewhere 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.
"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' endletwhere 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.
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 = 1According 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.
in let x = 2
in x + 1
endlet
+ x
endlet
NExp ::= Ide | let Dec in NExp endletBExp ::= Ide | let Dec in BExp endlet
Dec ::= Ide = Exp | Dec ; Dec
Exp ::= NExp | BExp
env |- e eval vto be read as:
in the environment env, the evaluation of expression e gives the value vUsually 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.
________________________
env |- x eval env(x)
Let us introduce the typical notation for the evaluation of declarations, which will be defined later:
env |- d eval_{D}_{ } 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.
env |- d eval_{D}_{ } env' env' |- e eval vNote 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.
______________________________________________env |- let d in e endlet eval v
env
|- e1 eval
v1 env
|- e2 eval
v2
__________________________________________
env |- e1 nop e2 eval v1 nop^{~} v2
Analogously for all the other rules.
env[v/x] (y) = v if y =xWe are now ready to give the rule of the semantics of a single declaration:
env[v/x] (y) = env(y) otherwise
env |- e eval v
_____________________________env |- x = e eval_{D}_{ } env[v/x]
env |- d1 eval_{D}_{ } env1 env1 |- d2 eval_{D}_{ } env2
_____________________________________________________env |- d1 ; d2 eval_{D}_{ } env2
let x = 1Let 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)
in let x = 2
in x + 1
endlet
+ x
endlet
____________________ __________________
___________________
env' |- 2 eval 2^{~} env" |- x eval 2^{~} env" |- 1 eval 1^{~} _________________________ _______________________________________ env' |- x = 2 eval_{D} 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 eval_{D} 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^{~} |