CSI 311 Principles of Programming Languages
Here are four programs from four different languages. All four programs compute the Nth Fibonacci number. The first two do so in essentially identical ways: using loops in QBASIC and in LISP. The third program is Prolog and is recursive because ordinary iteration is impossible in Prolog. But the recursive style is about as close to iteration as can be done in Prolog. The complexity is linear. The fourth program is C++. It is recursive, and it is also inefficient, having exponential complexity. Note the drastic differences in the syntax of the four languages. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Compute the N-th Fibonacci Number (QBASIC) SUB fibonacci (N) IF N=0 THEN PRINT 0 IF N=1 THEN PRINT 1 IF N>1 THEN N1 = 0 N2 = 1 S = 1 I = 1 DO S = N1 + N2 N1 = N2 N2 = S I = I + 1 UNTIL I = N PRINT S END IF END SUB Compute the N-th Fibonacci Number (LISP) (defun fibonacci (n) (cond ((eq n 0) 0) ((eq n 1) 1) (T (prog ((nextlast 0) (last 1) (index 1) ) A (setf index (+ index 1)) (setf fib (+ last nextlast)) (setf last fib) (setf nextlast last) (cond ((< index N) (go A))) )) ) fib ) Compute the N-th Fibonacci Number (Prolog) fib(0, 0) :- !. fib(1, 1) :- !. fib(N, FIB) :- fastfib(0, 1, 1, 1, N, FIB). fastfib(N2, F2, N1, F1, N1, F1) :- !. fastfib(N2, F2, N1, F1, N, FIB) :- NewF is F2+F1, NewN is N1+1, fastfib(N1, F1, NewN, NewF, N, FIB). Compute the N-th Fibonacci Number (C++) int fibonacci (int n) { if (n == 0 || n == 1) return 1; else return fibonacci(n-2) + fibonacci(n-1); }
Notes on Formal Languages, Grammars, and Backus-Naur Form A VOCABULARY is a finite set of symbols. A SENTENCE is a finite sequence of symbols taken from a vocabulary. A LANGUAGE is a (possibly infinite) set of sentences. A GRAMMAR G is a 4-tuple G = <P, V, v, S> where P = a finite set of rules (called production rules) V = a vocabulary of non-terminal symbols v = a vocabulary of terminal symbols S = a distinguished symbol called the start symbol A production rule is of the form: L ::= R, where L and R are finite sequences of symbols and the symbol "::=" means "can be rewritten as." A production rule L ::= R is CONTEXT-FREE if L is a single non-terminal. A grammar G is context-free if all it's production rules are context-free. A DERIVATION using grammar G is a finite sequence of sentences, D(G) = S0, S1, S2, ... , Sn, such that: 1. S0 is the start symbol of G 2. Each Si is obtained from S(i-1) by application of a rule from G. I.e., S(i-1) is a sentence of the form aLb, Si is a sentence of the form aRb, and G contains the rule L ::= R. We say that D(G) is a derivation of Sn. D(G) is a CANONICAL derivation if each application of a rule is performed on the leftmost non-terminal in S(i-1) to produce Si. Notation: To indicate that sentence Sj can be derived from sentence Si, * we write Si ===> Sj G If Sn consists of only terminal symbols, then Sn is in the language generated by G. The language generated by G is denoted by L(G); * L(G) = { w | S ===> w, w consists of terminal symbols only } G The Language of Balanced Parentheses Examples: (()()) ()((())) ((()())(()))() A grammar that defines the language whose sentences are strings of balanced parentheses. Terminals: { (, ) } Non-terminals: {S, X} (S = start symbol) Production Rules: S ::= X X ::= () X ::= (X) X ::= XX Derive ( ( ( ) ( ) ) ( ( ) ) ) ( ) | | |__| |__| | | |__| | | |__| | |______________| |________| | |________________________________| S X X X X ( ) ( X ) ( ) ( X X ) ( ) ( X ( X ) ) ( ) ( X ( ( ) ) ) ( ) ( ( X ) ( ( ) ) ) ( ) ( ( X X ) ( ( ) ) ) ( ) ( ( ( ) X ) ( ( ) ) ) ( ) ( ( ( ) ( ) ) ( ( ) ) ) ( ) Example: Integers without leading zeros Examples: 712, +44, -8787, 900801 Terminals: {+, -, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Non-terminals: {S, X, D, Y, Z} Grammar: S ::= 0 S ::= X S ::= +X S ::= -X Y ::= Z Y ::= ZY X ::= D X ::= DY D ::= 1 . . . D ::= 9 Z ::= 0 Z ::= D S .......... Start Sym. \- all integer constants X .......... Any integer without leading zeros Y .......... all strings of digits D .......... all non-zero digits Z .......... all digits Derivation S +X +DY +4Y +4Z +4D +44 Parse Trees --------------------- A PARSE TREE for a sentence Sn in L(G), where G is a context-free grammar, is a tree in which: 1. Each node in the tree is a symbol from G 2. The root is the start symbol for G 3. If node L has children R1, R2, ... , Rn, then G contains the rule L ::= R where R = R1 R2 ... Rn 4. If node L is a leaf, then L is a terminal symbol. 5. If the leaves of the tree are L1, L2, ... , Ln in left-to-right order, then Sn = L1 L2 ... Ln A context-free grammar G is UNAMBIGUOUS if for every sentence w in L(G), there is a unique parse tree for w (hence a unique canonical derivation). A given language will in general be generated by many grammars; some may be ambiguous and some may not. An INHERENTLY AMBIGUOUS language is one for which there does not exist an unambiguous grammar. Canonical Derivation Parse Tree S S +X / \ +DY + X +4Y / \ +4Z D Y +4D | | +44 4 Z | D | 4 Backus-Naur Form (BNF) ----------------------- This is just some convenient notation for describing a context-free grammar more succinctly. Non-terminal symbols are recognized by being enclosed in angle-brackets. Terminal symbols are recognized by NOT being enclosed in angle-brackets. The "|" symbol is used on the right hand side of rules to indicate alternative choices. There is no special declaration of the start symbol; it is usually obvious. Example: G = <P, V, v, S> where P = {X ::= A, V = {X} S = X X ::= B, X ::= C} v = {A, B, C} In BNF, we can specify G by the one rule: <X> ::= A | B | C Integers without leading zeros using BNF <S> ::= <X> | +<X> | -<X> | 0 <X> ::= <D> | <D><Y> <Y> ::= <Z> | <Z><Y> <D> ::= 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 <Z> ::= 0 | <D> Context-Sensitive Grammars The language L = { ww | w is in {a,b}+ } is not context-free: No context-free grammar generates L. If we allow rules in which the left hand side consists of several symbols, and the right hand sides are at least as big as the left, we get CONTEXT-SENSITIVE grammars that generate context-sensitive languages. The language L is a context-sensitive language. Consider the grammar G: <S> ::= <C> <M> <E> <C><M> ::= a<M><A> | b<M><B> | a<C><M><A> | b<C><M><B> <A><E> ::= a<E> <B><E> ::= b<E> <A>a ::= a<A> <B>a ::= a<B> <A>b ::= b<A> <B>b ::= b<B> <E> ::= empty <M> ::= empty G is almost context-sensitive; the last two rules violate the restriction on the size of the right hand sides of rules. But G can be easily converted to an equivalent grammar that is strictly context-sensitive. (Left as an exercise.)
BASIC, FORTRAN, & ALGOLW Simulations
I = 1 : J = 2 : K = 3
D := FNW(I, J-2)
PRINT I
DEF FNW(L, M)
L = 10 : J = 6
PRINT M : FNW = 0
FNEND
END output -> 1
-> 0
program basic(input, output);
var I, J, K : integer; D : real;
function W(L, M : integer) : real;
begin
L := 10; J := 6;
writeln(M); { executable Pascal }
W := 0
end;
begin
I := 1; J := 2; K := 3;
D := W(I, J-2);
writeln(I)
end.
J = 2 L = 10
K = 3 J = 6
D = W(I, J-2) WRITE(4, 15)
15 WRITE(4, 15) I W = 0
FORMAT(1X, I7) END
END output -> 10
0
program fortran(input, output);
var I, J, K : integer; D : real;
temp : integer;
function W(var L, M : integer) : real;
var J : integer;
begin
L := 10; J := 6;
writeln(M); { executable Pascal }
W := 0
end;
begin
I := 1; J := 2; K := 3;
temp := J-2; D := W(I, temp);
writeln(I)
end.
I := 1; J := 2; K := 3;
D := W(I, J-2);
WRITE I;
REAL PROCEDURE W(INTEGER L, M);
BEGIN
L := 10; J := 6;
WRITE M;
0
END; output -> 10
4
program algolw(input, output);
var I, J, K : integer; D : real;
function W(L, M : integer) : real;
begin
{L is replaced by I} I := 10; J := 6;
writeln({M is replaced by J-2} J-2);
W := 0 { executable Pascal )
end;
begin
I := 1; J := 2; K := 3;
D := W(I, J-2);
writeln(I)
end.
Consider the language L = {abc, aabbcc, aaabbbccc, ...}. L is not a context free language and to specify it, we need either a context-sensitive, or an attribute, grammar. We will develop an attribute grammar such that any parse tree for a sentence of L has a synthesized attribute that equals the number of a's (and thus also the #b's and #c's) in the sentence.
Clearly, we need a start symbol and a rule to get us going. We use <S> for the start symbol; the rule must have a synthesized attribute on the left side that reflects the number of a's. This is like a formal parameter in the declaration of a procedure that is used as an output parameter. The right side of the rule is like the body of a procedure definition, where we do computation and invoke other procedures (here, the other procedures are syntactic categories, i.e., non-terminals that, like <S>, are defined through rules). We'll call our synthesized attribute n, and THROUGHOUT THIS RULE, wherever we mention n, it is the SAME n. We're not sure about all the details of the rule for <S> just yet, but the right side must at least generate 3 strings composed of a's, b's, and c's respectively. So what appears below is just a first cut, NOT THE FINAL RULE for <S>.
<S> ^n ::= <A> <B> <C>
OK, so now what? Well, let's think about the attributes. Somewhere in the sub-parse-tree below <S>, we must synthesize n. In fact, it had better be synthesized in the sub-parse-tree below <A>, because we'll need it to be sure that the strings generated from <B> and <C> are acceptable. That means that n will be an INHERITED attribute in the rules for <B> and <C>.
<S> ^n ::= <A> ^n <B> !n <C> !n
What about having synthesized attributes for <B> and <C> - do we need them? Not here; we have enough information available once we have synthesized n. But somehow the rules for <B> and for <C> will need to check the length of THEIR OWN generated strings to ensure that the lengths are correct. This could be done by having the rules for <B> and <C> synthesize an attribute that represents this length, and then compare this synthesized attribute with n, the inherited one representing the number of a's. If we were to do this, the synthesized attributes for <B> and <C> would have to be shown in the rule for <s>, even though we don't use them here As it turns out, I will manage to construct the rules for <B> and <C> without returning a synthesized attribute; this is done by postponing the test until the rules for <B> and <C>are generating a single b or c.
Can we use the attribute n as both a synthesized and inherited attribute in the same rule? Yes, but the key word is USE; remember that n is like a declared formal parameter on the left, but we're merely USING it in the "procedure body" on the right. It can be thought of as an actual parameter or argument to the "procedures" for <A>, <B>, and <C> on the right. Just as parameters in a procedure call must match those in the declaration, the "procedure" or rule for <A> must specify a single SYNTHESIZED attribute on the left, while the rules for <B> and <C> must specify a single INHERITED attribute on the left. I.e., since we "call" the rule for <B> with an inherited attribute (one specified with !), the "definition" of <B> must expect an inherited attribute. We are free to use any attribute we have on hand in specifying the "call", but if the called rule expects an inherited value, then the attribute we use must be already assigned a value. If the called rule expects a synthesized attribute, then the attribute used in the call may be undefined, or may have its value changed. The situation is very much like that involving the use of var versus ordinary parameters in Pascal.
If these rules really were Pascal-like procedures, we would have something like:
procedure S(var n:integer); procedure A(var aval:integer); begin begin A(n); B(n); C(n); {generate a string of a's} end; aval := {number of a's} end; procedure B(bval:integer); procedure C(cval:integer); begin begin {generate string of bval b's} {generate string of cval c's} end; end;
Notice that n is a var parameter of S and is used in the call to A where it matches the var parameter aval. It is used in the calls to B and C only after being assigned a value where it matches the VALUE parameters bval and cval respectively. This suggests how we might start writing the rules for <A>, <B>, and <C>:
<A> ^aval ::= a ^1 | a <A> ^n ADD1 !n ^aval
ADD1 !in ^out ::= out := in + 1;
The synthesized attribute aval in this rule matches the corresponding attribute n in the body of the rule for <s>, where <A> is "called". The recursive call to <A> determines the attribute n in this rule which has no connection to attribute n in the rule for <s> - it's like a local variable.
There's another little shortcut taken here that is potentially confusing. Look at the very first alternative in the rule for <A>. It says that the string "a" is generated and the attribute value 1 is synthesized. But what happens to the 1? Most people would correctly guess that it is used as the synthesized value returned for aval; aval is, after all, the only attribute synthesized by the rule itself. But to be completely formal, we should show aval being assigned the value 1:
<A> ^aval ::= a ASSIGN ^aval !1 | a <A> ^n ADD1 !n ^aval ASSIGN ^id !exp ::= id := exp; ADD1 !in ^out ::= out := in + 1; Now we have shown explicitly how the attribute aval gets its value in the first alternative of the rule; this was already explicit in the second alternative, through the ADD1 evaluation rule. (The previous version of this rule is perfectly acceptable and is considered correct.) Now the rules for <B> and <C>: <B> !bval ::= b CONDITION !bval bval = 1 | b SUB1 !bval ^newb <B> !newb SUB1 !in ^out ::= out := in - 1;
The rule for <B> is supplied with the inherited attribute bval and is supposed to generate only strings of exactly bval b's. A single b is allowed if the inherited bval equals 1. Or a single b followed by a string of (bval-1) b's is ok, so we generate one b, compute the new attribute newb, and hand it off to the recursive call on the <B> rule. Notice that CONDITIONs and attribute evaluation rules like ADD1 and SUB1 must have their inherited and synthesized attributes specified just like non-terminals. These computations can be regarded as non-terminals that always generate the empty string, but that also test and compute attribute values. At this point, the rule for <C> is obvious: <C> !cval ::= c CONDITION !cval cval = 1 | c SUB1 !cval ^newc <C> !newc Below we show a partially completed parse tree for the sentence "aabbcc". Please excuse the absence of artistic proficiency. <s> ^n
. . .
. . .
. . .
<A> ^2 <B> !2 <C> !2
. . . | |
. . . etc. etc.
a ^1 <A> ^1 .
. ADD1 !1 ^2
.
a ^1
The Double Word Example
Here is an attribute grammar for the language L = { W | W=ww, where w is in {a,b}* }
<dwd>^n1::= <wd>^n2 <ckwd>!n2 ^n3 CONC!n2 !n3 ^n1
<wd>^n1 ::= <let>^n1 |<wd>^n2 <let>^n3 CONC!n2 !n3 ^n1
<ckwd>!n2 ^n1 ::= <wd>^n3 COND: n3=n2
<let>^n1 ::= a^"a" | b^"b"
CONC!n2 !n3 ^n1 {strings n2 and n3 are concatenated producing n1}
Question: Can we show a derivation of a string using an attribute grammar, rather than showing a parse tree for that string? The answer is yes. Below is a derivation of "abab". Normally a parse tree would be preferred because attribute value relationships and computations are displayed much more clearly. A derivation contains all this information (provided that you always know precisely which non-terminal is selected at each step as in, e.g., a canonical derivation), but that information is rather well hidden. Anyway, here goes:
<dwd>^__
<wd>^__ <ckwd>!__ ^__CONC!__ !__ ^__
<wd>^__<let> ^__CONC!__ !__ ^__<ckwd>!__ ^__CONC!__ !__ ^__
<let>^__<let>^__CONC!__ !__ ^__<ckwd>!__ ^__CONC!__ !__ ^__
a^"a" <let>^__CONC!"a"!__ ^__<ckwd>!__ ^__CONC!__ !__ ^__
a^"a" b^"b" CONC!"a"!"b"^__<ckwd>!__ ^__CONC!__ !__ ^__
a^"a" b^"b" CONC!"a"!"b"^"ab"<ckwd>!"ab"^__CONC!"ab"!__ ^__
Notice that I have tried to align corresponding attribute slots vertically (think of the arguments in a call to a procedure corresponding to the formal parameters in its definition). This explains why the non-terminal <dwd>^__ is over on the right in the first line of the derivation: its synthesized attribute gets its value from the third attribute of CONC in the next step.
Note also that this is a canonical derivation, and that the concatenation of "a" and "b" is shown as a separate step. Remember that CONC!"a"!"b"^"ab" is just an evaluation rule, so, while we show it in the derivation, it is NOT part of the actual string of terminal symbols that we derive.
Since the lines of the derivation are becoming too long, we will continue by omitting the part on the left in which no non-terminals remain and all attributes are computed (this omitted part is ` a^"a" b^"b" CONC!"a"!"b"^"ab" ').
<ckwd>!"ab"^__CONC!"ab"!__ ^__
<wd>^__COND:"ab"=__CONC!"ab"!__ ^__
<wd>^__<let> ^__CONC!__ !__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__
<let>^__<let> ^__CONC!__ !__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__
a^"a" <let> ^__CONC!"a"!__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__
a^"a" b^"b" CONC!"a"!"b"^__ COND:"ab" = __ CONC!"ab"!__ ^__ a^"a" b^"b" CONC!"a"!"b"^"ab"COND:"ab"="ab" CONC!"ab"!"ab"^__
a^"a" b^"b"CONC!"a"!"b"^"ab" COND:"ab"="ab" CONC!"ab"!"ab"^"abab"
Formally, the omitted part is carried along at the left in each of the
above 7 steps. It's now clear that although derivations are perfectly
well defined entities for attribute grammars, parse trees are by far
preferable.
Here's the simple example done in class on expressions of mixed type.
Names, l-values, r-values
var I, J : integer;
A : array[1..100] of integer;
begin
J := 3; I := J + 1;
A[I] := I;
.
.
.
name l-value r-value
(environment) (store)
===================================================
____________
I 05834726 | 4 |
------------
____________
J 05834727 | 3 |
------------
____________
A ([1]) 05834728 | |
-----------
____________
| |
------------
____________
| |
-------------
____________
A ([4]) 05834731 | 4 |
------------
____________
3 00000001 | 3 |
------------
PASS BY VALUE
* One-way communication
* Provides input to the called procedure
* Protects the actual parameters om change
When a procedure is called:
1. An environment and a store are allocated for the called
procedure, including the formal parameters and locally
declared variables.
2. The r-values of the actual parameters are copied into
locations specified by the l-values of the formal
parameters.
When control returns from a called procedure:
1. The environment and store for the called procedure is
deallocated.
PASS BY VALUE
program
I : integer;
A : array[1..100] of integer;
procedure VAL_SWAP(value X, Y : integer);
var temp : integer;
begin
temp := X; X := Y; Y := temp;
end;
begin {main program}
I := 3;
A[I] := 6;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
VAL_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;}
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
end.
OUTPUT
I = 3 A[3] = 6
I = 3 A[3] = 6
PASS BY RESULT
* One-way communication
* Provides output to the calling procedure
* Protects the formal parameters om initialization
by the calling routine
When a procedure is called:
1. An environment and a store are allocated for the called
procedure, including the formal parameters and locally
declared variables.
2. The l-values of the actual parameters are saved, one
for each of the formal parameters.
When control returns from a called procedure:
1. The r-values of the formal parameters are copied into
locations specified by the saved l-values of the actual
parameters.
2. The environment and store for the called procedure is
deallocated.
PASS BY RESULT
program
I : integer;
A : array[1..100] of integer;
procedure RES_SWAP(result X, Y : integer);
var temp : integer;
begin
temp := X; X := Y; Y := temp;
end;
begin {main program}
I := 3;
A[I] := 6;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
RES_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;}
X:=Y; Y:=temp;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
end.
OUTPUT
I = 3 A[3] = 6
* * * ERROR : UNDEFINED VARIABLE X
PASS BY VALUE-RESULT
* Two-way communication
* Provides both input to, and output from the called
procedure
* No protection for formal or actual parameters
When a procedure is called:
1. An environment and a store are allocated for the called
procedure, including the formal parameters and locally
declared variables.
2. The l-values of the actual parameters are saved, one
for each of the formal parameters.
3. The r-values of the actual parameters are copied into
locations specified by the l-values of the formal
parameters.
When control returns from a called procedure:
1. The r-values of the formal parameters are copied into
locations specified by the saved l-values of the actual
parameters.
2. The environment and store for the called procedure is
deallocated.
PASS BY VALUE-RESULT
program
I : integer;
A : array[1..100] of integer;
procedure V_R_SWAP(val_res X, Y : integer);
var temp : integer;
begin
temp := X; X := Y; Y := temp;
end;
begin {main program}
I := 3;
A[I] := 6;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
V-R_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;}
X:=Y; Y:=temp;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
end.
OUTPUT
I = 3 A[3] = 6
I = 6 A[3] = 3
PASS BY REFERENCE
* Two-way communication
* Provides both input to, and output from the
called procedure
* No protection for formal or actual parameters
* Almost equivalent to value-result parameters;
discrepancies show up in the presence of aliasing.
When a procedure is called:
1. An environment and a store are allocated for the called
procedure, including the formal parameters and locally
declared variables.
2. The l-values of the formal parameters are set equal to
the l-values of the actual parameters.
When control returns from a called procedure:
1. The environment and store for the called procedure is
deallocated.
PASS BY REFERENCE
program
I : integer;
A : array[1..100] of integer;
procedure REF_SWAP(var X, Y : integer);
var temp : integer;
begin
temp := X; X := Y; Y := temp;
end;
begin {main program}
I := 3;
A[I] := 6;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
REF_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;}
X:=Y; Y:=temp;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
end.
OUTPUT
I = 3 A[3] = 6
I = 6 A[3] = 3
PASS BY NAME
* One or two-way communication. If an actual parameter
has no l-value, then assignment to the corresponding
formal parameter is disallowed.
* Delayed evaluation of actual parameters - by need only.
* No protection for formal or actual parameters
When a procedure is called:
1. An environment and a store are allocated for the called
procedure, including the locally declared variables.
2. If necessary, local variables are given new names not
appearing elsewhere in the program.
2. The literal text of the actual parameters replaces each
occurrence of the corresponding formal parameters.
Variables mentioned in these expressions are global to
the called procedure.
When control returns "from" a called procedure:
1. The environment and store for the called procedure is
deallocated.
PASS BY NAME
program
I : integer;
A : array[1..100] of integer;
procedure NAM_SWAP(name X, Y : integer);
var temp : integer;
begin
temp := X; X := Y; Y := temp;
end;
begin {main program}
I := 3;
A[I] := 6;
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
NAM_SWAP(I, A[I]); { --->>> temp:=I; I:=A[I]; A[I]:=temp; }
write('I =', I); A[3]);
writeln('A[3] =', A[3]);
end.
OUTPUT
I = 3 A[3] = 6
I = 6 A[3] = 6
The following three programs in BASIC, FORTRAN, and AlgolW look
quite similar. But they all produce different output. Why?
The answer lies in subtle differences in the semantics of the three
languages. In particular, the conventions involving the scopes of
variables and the parameter passing mechanisms are different in the
various languages. If we simulate all three programs in Pascal,
then the differences become apparent.
In Pascal, variables must be declared local;
they are taken from the "nearest" global context
in which they were declared local otherwise.
Parameters must be declared as "var" parameters if the pass by
reference mechanism is to be used. Otherwise, they default to
pass by value. Pass by name is not directly expressible in Pascal,
but the effect is simulated with code specific to the AlgolW example.
Axiomatic semantics is based on logic. The logical expressions are
called assertions; an assertion is a true-false statement about
the state of the program. An assertion will typically be an equality
or inequality relating the values of various identifiers in the code.
The main goal of axiomatic semantics is to construct an assertion {C}
that defines the correct behavior of a segment of code S. We want C
to be true AFTER executing S; so we must discover the preconditions {P}
that must hold PRIOR to executing S, in order to guarantee that {C},
the desired postconditions, will hold AFTER executing S.
The preconditions {P} describe those constraints that must be
satisfied in order for S to behave correctly.
When we are lucky, {C} will always be true after executing S;
in this case, we will discover that the preconditions {P} are trivially
true. This means that the code is correct under all circumstances.
So we need axioms and rules that relate postconditions to preconditions
and vice versa. Since we want to be able to do this for arbitrary
segments of code, we need such rules, in principle, for every kind
of statement in the programming language under consideration.
However, if we have rules for a small number of primitive statements,
and if we can define other statements in terms of those primitives,
then in principle we have enough rules.
For example, the assignment statement:
{P}[exp/id] id := exp {P}
This axiom means that for {P} to be true after execution of "id := exp",
then {P}[exp/id] must be true prior to execution of "id := exp",
where {P}[exp/id] means substitute exp for all occurrences of id in {P}.
For instance, suppose we want {sum > 1} to be true after executing
"sum := 2*x + 1". What must be true prior to executing the assignment in
order to guarantee {sum > 1} after? Well, from the assignment axiom,
{sum > 1}[2*x + 1 / sum] sum := 2*x + 1 {sum > 1}
and
{sum > 1}[2*x + 1 / sum] = {2*x + 1 > 1}
= {2*x > 0}
= {x > 0}
so if {x > 0} is true and we execute sum := 2*x + 1, then {sum > 1} is true.
NOTE: if we know beforehand that {x > 0}, and we want to know what is
true after executing "sum := 2*x + 1", then we can apply the
substitution in reverse if we're careful. So we look for occurrences
"2*x + 1" in {x > 0} and we don't see any. But
{x > 0} = {2*x > 2*0}
= {2*x + 1 > 2*0 + 1}
= {2*x + 1 > 1}
so {2*x + 1 > 1}[sum / 2*x + 1] = {sum > 1}
and we know that {sum > 1} will hold afterward.
Now, we talked about code segments, not single statements. What if we have
a sequence of statements? Well, informally we can apply axioms or rules
for single statements and successively generate assertions further and
further back. The relationship of one assertion being guaranteed true
if another is also true is TRANSITIVE. More formally we have the
COMPOSITION RULE:
{P} S1 {Q} and {Q} S2 {R}
-----------------------------
{P} S1 ; S2 {R}
This notation means that if we can prove everything in the upper
part of the rule, then we can conclude everything in the lower part.
So suppose we have statements S1 = s := s + i; and S2 = i := i+1;
and suppose we want to know the precondition {P} that, when true
before BOTH S1 and S2 guarantees {R} to be true afterward, where
{R} = {s = 1/2(i)(i-1)}. We use the axiom for assignment and the rule
for composition:
if {R} = {s = 1/2(i)(i-1)}
then {Q} = {s = 1/2(i)(i-1)}[i+1 / i] = {s = 1/2(i+1)(i+1-1)}
so {Q} = {s = 1/2(i+1)(i)}
so we have {Q} S2 {R}, now try for {P} S1 {Q}:
if {Q} = {s = 1/2(i+1)(i)}
then {P} = {s = 1/2(i+1)(i)}[s+i / s] = {s+i = 1/2(i+1)(i)}
= {s = 1/2(i+1)(i) - i}
= {s = 1/2((i+1)(i) - 2*i)}
= {s = 1/2((i^2 + i - 2*i)}
= {s = 1/2(i^2 - i)}
so {P} = {s = 1/2(i)(i-1)} = {R}!
We have now established both of ({P} S1 {Q}) and ({Q} S2 {R}).
Notice that this is the upper part of the composition rule, so
we can conclude the lower part:
{P} S1 ; S2 {R}
But notice also that in deriving {Q} and then {P}, we discovered
that {P} = {R}, so we can in fact conclude that
{P} S1 ; S2 {P}
If S1 and S2 comprised the body of a while loop, we would have
discovered that {P} is a LOOP INVARIANT, an assertion that is
true both before and after executing the loop body,
and will therefore be true after the last execution of the loop,
no matter how many times the loop did execute.
So if our loop were: while B do (S, where S=S1;S2, example above)
and if we now know: {P} S {P}
we MIGHT want to conclude: {P} while B do S {P}
However, we may have to account for the loop test: since the test
is performed each trip through the loop, we must be sure that its
being true does not interfere with the truth of {P}.
So we may need to know: {P & B} S {P}.
The truth of both B and P may actually be NECESSARY in order for
{P} to be true at the end of the loop body. This is the case in the
example below.
Or the loop test may be essentially independent of P,
and it may suffice to show that {P} S {P}, which is fine.
Also, we do know a little more than just {P} after the loop:
Since the loop terminated, we know that B must be false.
So we can conclude that after the loop, {P & not B} holds.
The formal rule of inference for while loops accounts for all these things:
{P & B} S {P}
--------------------------------
{P} while B do S {P & not B}
So consider an example from lecture, the linear search algorithm.
{P1}
S1 A[0] := x;
S2 i := n;
while x <> A[i] do begin
S3 i := i-1
end;
{P2}
return(i)
First compare the while loop with the rule of inference for while loops.
The loop test B is x<>A[i]. So the assertion {P & not B} from the rule
corresponds to {P2} near the end of the code - we want to discover {P2}.
(The loop body S is just statement S3, "i := i-1")
Ok, how do we discover {P2}? It should be something that
asserts the correctness of the algorithm.
Then, given {P2}, we want to derive {P1} - the preconditions
that must hold in order to guarantee the correctness
of the algorithm.
Well, if the algorithm is correct, then the value returned
is the first i <= n such that x = A[i].
Therefore, x <> A[j], where i+1 <= j <= n.
Also, x = A[0]; this guarantees termination, and we
include this fact although we will not deal with termination formally.
So, {P2} = {x=A[0] & x <> A[i+1, i+2, ... , n] & x=A[i]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^
P not B
The inference rule says, if we prove that
{P} will hold after S whenever {P & B} holds before S,
--------------- THEN WE CAN CONCLUDE THAT ------------------
{P & not B} will hold after the the while loop,
whenever {P} holds before the while loop.
Well, S is just the statement: i := i-1;
P is the assertion {x=A[0] & x <> A[i+1, i+2, ... , n]},
and B is the assertion {x <> A[i]}
so, by the assignment axiom,
{P}[i-1 / i] i := i-1; {P}
{P}[i-1 / i] = {x=A[0] & x <> A[i+1, i+2, ... , n]}[i-1 / i]
= {x=A[0] & x <> A[i-1+1, i-1+2, ... , n]}
= {x=A[0] & x <> A[i, i+1, ... , n]}
= {x=A[0] & x <> A[i] & x <> A[i+1, ... , n]}
= {x=A[0] & x <> A[i+1, ... , n] & x <> A[i]}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^
so {P}[i-1 / i] = {P} & B
So we have shown that: {P & B} S {P}
But by the rule for the while loop, we now know:
{P} while B do S {P & not B}
OK, so if {P} holds before the while loop, then {P & not B} = {P2}
holds at the end of the algorithm, which asserts the correctness of
the algorithm. Now we need to find the preconditions {P1} that must
hold at the beginning of the algorithm, in order for {P} to hold just
prior to the while loop. This part is easy using the assignment axiom!
We just take {P} and back it up through statements S2 and S1.
We know that {P}[n / i] i := n; {P}
and {P}[n / i] = {x=A[0] & x <> A[i+1, i+2, ... , n]}[n / i]
= {x=A[0] & x <> A[n+1, ... , n]}
= {x=A[0] & true}
= {x=A[0]} (call this {Q})
Also (assignment axiom again) we know that:
{Q}[x / A[0]] A[0] := x; {Q}
{x=A[0]}[x / A[0]] A[0] := x; {x=A[0]}
and {x=A[0]}[x / A[0]] = {x = x}
= {true} (but this is {P1}!!)
Now we have shown that:
{true} S1 {Q} and {Q} S2 {P}
So by the composition rule we have: {true} S1; S2 {P}
And now to be absolutely thorough, we have:
{true} (S1; S2) {P} and {P} while B do S {P & not B}
--------------------------------------------------------------
{true} (S1; S2); while B do S {P & not B}
from the composition rule.
This states that the precondition required at the beginning of the algorithm
for guaranteeing the postcondition (correctness) {P & not B}
is simply that {true} holds!
But {true} ALWAYS holds.
So the algorithm ALWAYS behaves correctly.
In class, we may also discuss the rule of inference
for conditional statements:
Given the conditional: if B then S
we would like to discover assertions P and Q such that
if P holds prior to the conditional,
then Q holds after the conditional, i.e.,
{P} if B then S {Q}
But S will execute exactly when B holds, so we need to establish
{P & B} S {Q}
But if (not B) holds, S does not execute, so we also need to establish
{P & not B} ==> {Q}
The formal rule of inference for conditionals
accounts for all these things:
{P & B} S {Q}, {P & not B} ==> {Q}
-------------------------------------
{P} if B then S {Q}
We need this rule (and the WHILE axiom) for the following example
in which we prove the correctness of a routine for finding the
maximum value in an array.
Here, all the assertions are filled in between the lines of code.
To actually do the proof, start with the assertion of correctness
at the end of the algorithm (P /\ ~Bloop) and establish step by
step the assertion required at the beginning. Fortunately, the
assertion needed at the beginning is merely {A[1]=A[1]} = {true}.
1) {A[1]=A[1]} = {true}
2) x := A[1];
3) {j<2 ==< x=max(A[1], ...A[j])} = {x=A[1]}
4) i := 2;
5) {j<i ==< x=max(A[1], ...A[j])} = P
6) while (i <= N) (i <= N) = Bloop
7) {j<i ==< x=max(A[1], ...A[j])} = P
8) if (A[i] < x) then (A[i] < x) = Bcond
x := A[i];
9) {j<i+1 ==< x=max(A[1], ...A[j])} = P'
10) i := i+1;
11) {j<i ==< x=max(A[1], ...A[j])} = P
12) end while
13) {j<i ==< x=max(A[1], ...A[j])} = P /\ {i<N} = ~Bloop
Note that (P /\ ~Bloop) ==< x=max(A[1], ...A[N]) = correctness!
You should be able to start with the assertions at 13) and,
under the assumption that the premise of the while axiom holds,
know that assertion P at 5) guarentees 13). Then from 5) we
can, using the assignment axiom, conclude that 3), and then 1)
are the necessary conditions at those points in the program.
Showing that P is indeed a loop invariant of the while loop is
difficult and requires using the if-then axiom and a bit of
clever reasoning. This may be shown in class, but is more difficult
than what will be expected of students.
BASIC, FORTRAN, & ALGOLW Simulations
BASIC
10 I = 1 : J = 2 : K = 3
20 D := FNW(I, J-2)
30 PRINT I
100 DEF FNW(L, M)
110 L = 10 : J = 6
120 PRINT M : FNW = 0
140 FNEND
999 END output -> 0
1
program basic(input, output);
var I, J, K : integer; D : real;
function W(L, M : integer) : real;
begin
L := 10; J := 6;
writeln(M); { executable Pascal }
W := 0
end;
begin
I := 1; J := 2; K := 3;
D := W(I, J-2);
writeln(I)
end.
FORTRAN
I = 1 FUNCTION W(L, M)
J = 2 L = 10
K = 3 J = 6
D = W(I, J-2) WRITE(4, 15) M
15 WRITE(4, 15) I W = 0
FORMAT(1X, I7) END
END output -> 0
10
program fortran(input, output);
var I, J, K : integer; D : real;
temp : integer;
function W(var L, M : integer) : real;
var J : integer;
begin
L := 10; J := 6;
writeln(M); { executable Pascal }
W := 0
end;
begin
I := 1; J := 2; K := 3;
temp := J-2; D := W(I, temp);
writeln(I)
end.
ALGOL W
INTEGER I,J,K; REAL D;
I := 1; J := 2; K := 3;
D := W(I, J-2);
WRITE I;
REAL PROCEDURE W(INTEGER L, M);
BEGIN
L := 10; J := 6;
WRITE M;
0
END; output -> 4
10
program algolw(input, output);
var I, J, K : integer; D : real;
function W(L, M : integer) : real;
begin
{L is replaced by I} I := 10; J := 6;
writeln({M is replaced by J-2} J-2);
W := 0 { executable Pascal }
end;
begin
I := 1; J := 2; K := 3;
D := W(I, J-2);
writeln(I)
end.
(defun pfacts1 (L)
(setf SL (isort L))
(setf BIG (car SL))
(setf SMALL (lastel SL))
(print L) (terpri) (print SL) (terpri)
(print BIG) (princ " ") (princ SMALL) T)
Imperative Style
Problems
- destructive assignment
- SL, BIG, SMALL, are global
(defun pfacts2 (L)
(let* ((SL (isort L))
(BIG (car SL))
(SMALL (lastel SL)) )
(print L) (terpri) (print SL)
(terpri) (print BIG)
(princ " ") (princ SMALL) T) )
Imperative Style, but
SL, BIG, SMALL, are local
(defun pfacts3 (L)
(print L) (terpri) (print (isort L))
(terpri) (print (car (isort L)))
(princ " ") (princ (lastel (isort L)))) T) )
Functional Style
Problem
- sorted the list 3 times!
(defun pfacts4 (L) (aux L (isort L)))
(defun aux (L SL)
(print L) (terpri) (print SL) (terpri)
(print (car SL)) (princ " ")
(princ (lastel SL)) T)
Functional Style and Efficient
(defun lastel (L)
(cond ((null (cdr L)) (car L))
(t (lastel (cdr L))) ) )
(defun isort (L)
(cond ((or (null L) (null (cdr L))) L)
(t (place (car L)
(isort (cdr L)) )) ) )
(defun place (e L)
(cond ((null L) (list e))
((<= e (car L)) (cons e L))
(t (cons (car L)
(place e (cdr L)) )) ) )
QUICKSORT (functionally)
(defun partition (sp l)
(cond ((null l) (list nil nil))
; ((> (car l) sp)
; (list (cons (car l)
; (car (partition sp (cdr l))) )
; (cadr (partition sp (cdr l))) ) )
; (t (list (car (partition sp (cdr l)))
; (cons (car l)
; (car (partition
; sp (cdr l) )) ) )) ) )
;
; Would have computed "(partition sp (cdr l))" 4 times!
(t (build sp
(car l)
(partition sp (cdr l)) )) ) )
(defun build (split carL part)
(cond ((> carL split)
(list (cons carL (car part))
(cadr part) ) )
(t (list (car part)
(cons carL (cadr part)) ) ) ) )
(defun quicksort (L)
(cond ((null L) nil)
; (t (append (quicksort
; (car (partition (car L) (cdr L)))
; (cons (car L)
; (quicksort
; (cadr
; (partition
; (car L)
; (cdr L) ) ) ) ) ))) ) )
;
; Would have computed "(partition (car L) (cdr L)))" twice!
(t (combine (car L)
(partition (car L) (cdr L)) )) ) )
(defun combine (split part)
(append (quicksort (car part))
(cons split
(quicksort (cadr part)) ) ) )
Return Back to CSI 311 Home Page