Compilers
Compilers
DRAFT
D. Vermeir
Dept. of Computer Science
Vrij Universiteit Brussel, VUB
[email protected]
S R
E
V I
N
U
ITE
IT
E
J
I
R
V
B
R
U
S
S
E
L
E C
N I
V
RE
T
E
N
E
B
R
A
S
A
I
T
N
E
I
C
S
February 4, 2009
Contents
1 Introduction 6
1.1 Compilers and languages . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Applications of compilers . . . . . . . . . . . . . . . . . . . . . . 7
1.3 Overview of the compilation process . . . . . . . . . . . . . . . . 9
1.3.1 Micro . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.2 x86 code . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3.3 Lexical analysis . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.4 Syntax analysis . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.5 Semantic analysis . . . . . . . . . . . . . . . . . . . . . . 14
1.3.6 Intermediate code generation . . . . . . . . . . . . . . . . 15
1.3.7 Optimization . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.8 Code generation . . . . . . . . . . . . . . . . . . . . . . 17
2 Lexical analysis 18
2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Regular expressions . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.3 Finite state automata . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3.1 Deterministic nite automata . . . . . . . . . . . . . . . . 26
2.3.2 Nondeterministic nite automata . . . . . . . . . . . . . . 28
2.4 Regular expressions vs nite state automata . . . . . . . . . . . . 31
2.5 A scanner generator . . . . . . . . . . . . . . . . . . . . . . . . . 32
1
VUB-DINF/2009/2 2
3 Parsing 35
3.1 Context-free grammars . . . . . . . . . . . . . . . . . . . . . . . 35
3.2 Top-down parsing . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.2.2 Eliminating left recursion in a grammar . . . . . . . . . . 41
3.2.3 Avoiding backtracking: LL(1) grammars . . . . . . . . . 43
3.2.4 Predictive parsers . . . . . . . . . . . . . . . . . . . . . . 44
3.2.5 Construction of rst and follow . . . . . . . . . . . . . . 48
3.3 Bottom-up parsing . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.3.1 Shift-reduce parsers . . . . . . . . . . . . . . . . . . . . 50
3.3.2 LR(1) parsing . . . . . . . . . . . . . . . . . . . . . . . . 54
3.3.3 LALR parsers and yacc/bison . . . . . . . . . . . . . . . 62
4 Checking static semantics 65
4.1 Attribute grammars and syntax-directed translation . . . . . . . . 65
4.2 Symbol tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.2.1 String pool . . . . . . . . . . . . . . . . . . . . . . . . . 69
4.2.2 Symbol tables and scope rules . . . . . . . . . . . . . . . 69
4.3 Type checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5 Intermediate code generation 74
5.1 Postx notation . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.2 Abstract syntax trees . . . . . . . . . . . . . . . . . . . . . . . . 76
5.3 Three-address code . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.4 Translating assignment statements . . . . . . . . . . . . . . . . . 79
5.5 Translating boolean expressions . . . . . . . . . . . . . . . . . . 81
5.6 Translating control ow statements . . . . . . . . . . . . . . . . . 85
5.7 Translating procedure calls . . . . . . . . . . . . . . . . . . . . . 86
5.8 Translating array references . . . . . . . . . . . . . . . . . . . . . 88
VUB-DINF/2009/2 3
6 Optimization of intermediate code 92
6.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
6.2 Local optimization of basic blocks . . . . . . . . . . . . . . . . . 94
6.2.1 DAG representation of basic blocks . . . . . . . . . . . . 95
6.2.2 Code simplication . . . . . . . . . . . . . . . . . . . . . 99
6.2.3 Array and pointer assignments . . . . . . . . . . . . . . . 100
6.2.4 Algebraic identities . . . . . . . . . . . . . . . . . . . . . 101
6.3 Global ow graph information . . . . . . . . . . . . . . . . . . . 101
6.3.1 Reaching denitions . . . . . . . . . . . . . . . . . . . . 103
6.3.2 Reaching denitions using datalog . . . . . . . . . . . . . 105
6.3.3 Available expressions . . . . . . . . . . . . . . . . . . . . 106
6.3.4 Available expressions using datalog . . . . . . . . . . . . 109
6.3.5 Live variable analysis . . . . . . . . . . . . . . . . . . . . 110
6.3.6 Denition-use chaining . . . . . . . . . . . . . . . . . . . 112
6.3.7 Application: uninitialized variables . . . . . . . . . . . . 113
6.4 Global optimization . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.4.1 Elimination of global common subexpressions . . . . . . 113
6.4.2 Copy propagation . . . . . . . . . . . . . . . . . . . . . . 114
6.4.3 Constant folding and elimination of useless variables . . . 116
6.4.4 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
6.4.5 Moving loop invariants . . . . . . . . . . . . . . . . . . . 120
6.4.6 Loop induction variables . . . . . . . . . . . . . . . . . . 123
6.5 Aliasing: pointers and procedure calls . . . . . . . . . . . . . . . 126
6.5.1 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
6.5.2 Procedures . . . . . . . . . . . . . . . . . . . . . . . . . 128
7 Code generation 130
7.1 Run-time storage management . . . . . . . . . . . . . . . . . . . 131
7.1.1 Global data . . . . . . . . . . . . . . . . . . . . . . . . . 131
7.1.2 Stack-based local data . . . . . . . . . . . . . . . . . . . 132
7.2 Instruction selection . . . . . . . . . . . . . . . . . . . . . . . . . 134
VUB-DINF/2009/2 4
7.3 Register allocation . . . . . . . . . . . . . . . . . . . . . . . . . 136
7.4 Peephole optimization . . . . . . . . . . . . . . . . . . . . . . . 137
A A Short Introduction to x86 Assembler Programming under Linux 139
A.1 Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
A.2 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
A.2.1 Operands . . . . . . . . . . . . . . . . . . . . . . . . . . 140
A.2.2 Addressing Modes . . . . . . . . . . . . . . . . . . . . . 140
A.2.3 Moving Data . . . . . . . . . . . . . . . . . . . . . . . . 141
A.2.4 Integer Arithmetic . . . . . . . . . . . . . . . . . . . . . 142
A.2.5 Logical Operations . . . . . . . . . . . . . . . . . . . . . 142
A.2.6 Control Flow Instructions . . . . . . . . . . . . . . . . . 142
A.3 Assembler Directives . . . . . . . . . . . . . . . . . . . . . . . . 143
A.4 Calling a function . . . . . . . . . . . . . . . . . . . . . . . . . . 144
A.5 System calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
A.6 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
B Mc: the Micro-x86 Compiler 149
B.1 Lexical analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
B.2 Symbol table management . . . . . . . . . . . . . . . . . . . . . 151
B.3 Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
B.4 Driver script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
B.5 Makele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
B.6 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
B.6.1 Source program . . . . . . . . . . . . . . . . . . . . . . . 157
B.6.2 Assembly language program . . . . . . . . . . . . . . . . 157
C Minic parser and type checker 159
C.1 Lexical analyzer . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
C.2 String pool management . . . . . . . . . . . . . . . . . . . . . . 161
C.3 Symbol table management . . . . . . . . . . . . . . . . . . . . . 163
VUB-DINF/2009/2 5
C.4 Types library . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
C.5 Type checking routines . . . . . . . . . . . . . . . . . . . . . . . 172
C.6 Parser with semantic actions . . . . . . . . . . . . . . . . . . . . 175
C.7 Utilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
C.8 Driver script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
C.9 Makele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Index 181
Bibliography 186
Chapter 1
Introduction
1.1 Compilers and languages
A compiler is a program that translates a source language text into an equivalent
target language text.
E.g. for a C compiler, the source language is C while the target language may be
Sparc assembly language.
Of course, one expects a compiler to do a faithful translation, i.e. the meaning of
the translated text should be the same as the meaning of the source text.
One would not be pleased to see the C program in Figure 1.1
1 #include <stdio.h>
2
3 int
4 main(int,char
**
)
5 {
6 int x = 34;
7 x = x
*
24;
8 printf("%d\n",x);
9 }
Figure 1.1: A source text in the C language
translated to an assembler program that, when executed, printed Goodbye world
on the standard output.
So we want the translation performed by a compiler to be semantics preserving.
This implies that the compiler is able to understand (compute the semantics of)
6
VUB-DINF/2009/2 7
the source text. The compiler must also understand the target language in order
to be able to generate a semantically equivalent target text.
Thus, in order to develop a compiler, we need a precise denition of both the
source and the target language. This means that both source and target language
must be formal.
A language has two aspects: a syntax and a semantics. The syntax prescribes
which texts are grammatically correct and the semantics species how to derive
the meaning from a syntactically correct text. For the C language, the syntax
species e.g. that
the body of a function must be enclosed between matching braces ().
The semantics says that the meaning of the second statement in Figure 1.1 is that
the value of the variable x is multiplied by 24 and the result becomes
the new value of the variable x
It turns out that there exist excellent formalisms and tools to describe the syntax
of a formal language. For the description of the semantics, the situation is less
clear in that existing semantics specication formalisms are not nearly as simple
and easy to use as syntax specications.
1.2 Applications of compilers
Traditionally, a compiler is thought of as translating a so-called high level lan-
guage such as C
1
or Modula2 into assembly language. Since assembly language
cannot be directly executed, a further translation between assembly language and
(relocatable) machine language is necessary. Such programs are usually called
assemblers but it is clear that an assembler is just a special (easier) case of a com-
piler.
Sometimes, a compiler translates between high level languages. E.g. the rst C++
implementations used a compiler called cfront which translated C++ code to C
code. Such a compiler is often called a cross-compiler.
On the other hand, a compiler need not target a real assembly (or machine) lan-
guage. E.g. Java compilers generate code for a virtual machine called the Java
1
If you want to call C a high-level language
VUB-DINF/2009/2 8
Virtual Machine (JVM). The JVM interpreter then interprets JVM instructions
without any further translation.
In general, an interpreter needs to understand only the source language. Instead
of translating the source text, an interpreter immediately executes the instructions
in the source text. Many languages are usually interpreted, either directly, or
after a compilation to some virtual machine code: Lisp, Smalltalk, Prolog, SQL
are among those. The advantages of using an interpreter are that is easy to port
a language to a new machine: all one has to do is to implement the virtual ma-
chine on the new hardware. Also, since instructions are evaluated and examined
at run-time, it becomes possible to implement very exible languages. E.g. for an
interpreter it is not a problem to support variables that have a dynamic type, some-
thing which is hard to do in a traditional compiler. Interpreters can even construct
programs at run time and interpret those without difculties, a capability that is
available e.g. for Lisp or Prolog.
Finally, compilers (and interpreters) have wider applications than just translating
programming languages. Conceivably any large and complex application might
dene its own command language which can be translated to a virtual machine
associated with the application. Using compiler generating tools, dening and
implementing such a language need not be difcult. Hence SQL can be regarded
as such a language associated with a database management system. Other so-
called little languages provide a convenient interface to specialized libraries.
E.g. the language (n)awk is a language that is very convenient to do powerful
pattern matching and extraction operations on large text les.
VUB-DINF/2009/2 9
1.3 Overview of the compilation process
In this section we will illustrate the main phases of the compilation process through
a simple compiler for a toy programming language. The source for an implemen-
tation of this compiler can be found in Appendix B and on the web site of the
course.
program : declaration list statement list
;
declaration list : declaration ; declaration list
[
;
declaration : declare var
;
statement list : statement ; statement list
[
;
statement : assignment
[ read statement
[ write statement
;
assignment : var = expression
;
read statement : read var
;
write statement : write expression
;
expression : term
[ term + term
[ term term
;
term : NUMBER
[ var
[ ( expression )
;
var : NAME
;
Figure 1.2: The syntax of the Micro language
1.3.1 Micro
The source language Micro is very simple. It is based on the toy language
described in [FL91].
VUB-DINF/2009/2 10
The syntax of Micro is described by the rules in Figure 1.2. We will see in Chap-
ter 3 that such rules can be formalized into what is called a grammar.
Note that NUMBER and NAME have not been further dened. The idea is, of
course, that NUMBER represents a sequence of digits and that NAME represents
a string of letters and digits, starting with a letter.
A simple Micro program is shown in Figure 1.3
declare xyz;
xyz = (33+3)-35;
write xyz;
The closure L
of a language L is dened by
L
=
iN
L
i
(where, of course, L
0
= and L
i+1
= L.L
i
).
Denition 1 The following table, where r and s denote arbitrary regular expres-
sions, recursively denes all regular expressions over a given alphabet , together
with the language L
x
each expression x represents.
Regular expression Language
a a
(r + s) L
r
L
s
(rs) L
r
L
s
(r
) L
r
In the table, r and s denote arbitrary regular expressions, and a is an arbi-
trary symbol from .
A language L for which there exists a regular r expression such that L
r
= L is
called a regular language.
VUB-DINF/2009/2 25
We assume that the operators +, concatenation and
have increasing precedence,
allowing us to drop many parentheses without risking confusion. Thus, ((0(1
)) + 0)
may be written as 01
+ 0.
From Figure 2.2 we can deduce regular expressions for each token type, as shown
in Figure 2.1. We assume that
= a, . . . , z, A, . . . , Z, 0, . . . , 9, SP, NL, (, ), +, =, , , ; ,
Token type or abbreviation Regular expression
letter a + . . . + z + A + . . . + Z
digit 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9
NUMBER digit(digit)
LBRACE
.. ..
Table 2.1: Regular expressions describing Micro tokens
A full specication, such as the one in Section B.1, page 149, then consists of a
set of (extended) regular expressions, plus C code for each expression. The idea
is that the generated scanner will
Process input characters, trying to nd a longest string that matches any of
the regular expressions
2
.
Execute the code associated with the selected regular expression. This code
can, e.g. install something in the symbol table, return a token type or what-
ever.
In the next section we will see how a regular expression can be converted to a so-
called deterministic nite automaton that can be regarded as an abstract machine
to recognize strings described by regular expressions. Automatic translation of
such an automaton to actual code will turn out to be straightforward.
2
If two expressions match the same longest string, the one that was declared rst is chosen.
VUB-DINF/2009/2 26
2.3 Finite state automata
2.3.1 Deterministic nite automata
Denition 2 A deterministic nite automaton (DFA) is a tuple
(Q, , , q
0
, F)
where
Q is a nite set of states,
is a nite input alphabet
: Q Q is a (total) transition function
q
0
Q is the initial state
F Q is the set of nal states
Denition 3 Let M = (Q, , , q
0
, F) be a DFA. A conguration of M is a pair
(q, w) Q
, w)
just when (q, a) = q
3
. The reexive and transitive closure of the binary relation
M
is denoted as
M
. A sequence
c
0
M
c
1
M
. . .
M
c
n
is called a computation of n 0 steps by M.
The language accepted by M is dened by
L(M) = w [ q F (q
0
, w)
M
(q, )
We will often write
M
(q
, ).
3
We will drop the subscript M in
M
if M is clear from the context.
VUB-DINF/2009/2 27
Example 1 Assuming an alphabet = l, d, o (where l stands for letter,
d stands for digit and o stands for other), a DFA recognizing Micro
NAMEs can be dened as follows:
M = (q
0
, q
e
, q
1
, l, d, o, , q
0
, q
1
)
where is dened by
(q
0
, l) = q
1
(q
0
, d) = q
e
(q
0
, o) = q
e
(q
1
, l) = q
1
(q
1
, d) = q
1
(q
1
, o) = q
e
(q
e
, l) = q
e
(q
e
, d) = q
e
(q
e
, o) = q
e
M is shown in Figure 2.3 (the initial state has a small incoming arrow, nal states
are in bold):
q
0
q
1
q
e
l
l
d
o
o
d
l
d
o
Figure 2.3: A DFA for NAME
Clearly, a DFA can be efciently implemented, e.g. by encoding the states as
numbers and using an array to represent the transition function. This is illustrated
in Figure 2.4. The next state array can be automatically generated from the
DFA description.
What is not clear is how to translate regular expressions to DFAs. To show how
this can be done, we need the more general concept of a nondeterministic nite
automaton (NFA).
VUB-DINF/2009/2 28
1 typedef int STATE;
2 typedef char SYMBOL;
3 typedef enum {false,true} BOOL;
4
5 STATE next_state[SYMBOL][STATE];
6 BOOL final[STATE];
7
8 BOOL
9 dfa(SYMBOL
*
input,STATE q)
10 {
11 SYMBOl c;
12
13 while (c=
*
input++)
14 q = next_state[c,q];
15 return final[q];
16 }
Figure 2.4: DFA implementation
2.3.2 Nondeterministic nite automata
A nondeterministic nite automaton is much like a deterministic one except that
we now allow several possibilities for a transition on the same symbol from a
given state. The idea is that the automaton can arbitrarily (nondeterministically)
choose one of the possibilities. In addition, we will also allow -moves where the
automaton makes a state transition (labeled by ) without reading an input symbol.
Denition 4 A nondeterministic nite automaton (NFA) is a tuple
(Q, , , q
0
, F)
where
Q is a nite set of states,
is a nite input alphabet
: Q( ) 2
Q
is a (total) transition function
4
q
0
Q is the initial state
F Q is the set of nal states
VUB-DINF/2009/2 29
b
a
q
2
q
1
q
0
a
b
Figure 2.5: M
1
It should be noted that 2
Q
and thus Denition 4 sanctions the possibility of
there not being any transition from a state q on a given symbol a.
Example 2 Consider M
1
= (q
0
, q
1
, q
2
,
1
, q
0
, q
0
) as depicted in Figure 2.5.
The table below denes
1
:
q Q
1
(q, )
q
0
a q
1
q
0
b
q
1
a
q
1
b q
0
, q
2
q
2
a q
0
q
2
b
The following denition formalizes our intuition about the behavior of nondeter-
ministic nite automata.
Denition 5 Let M = (Q, , , q
0
, F) be a NFA. A conguration of M is a pair
(q, w) Q
, w)
just when q
(q, a)
5
. The reexive and transitive closure of the binary relation
M
is denoted as
M
. The language accepted by M is dened by
L(M) = w [ q F (q
0
, w)
M
(q, )
4
For any set X, we use 2
X
to denote its power set, i.e. the set of all subsets of X.
5
We will drop the subscript M in
M
if M is clear from the context.
VUB-DINF/2009/2 30
Example 3 The following sequence shows how M
1
from Example 2 can accept
the string abaab:
(q
0
, abaab)
M
1
(q
1
, baab)
M
1
(q
2
, aab)
M
1
(q
0
, ab)
M
1
(q
1
, b)
M
1
(q
0
, )
L(M
1
) = w
0
w
1
. . . w
n
[ n N 0 i n w
i
ab, aba
Although nondeterministic nite automata are more general than deterministic
ones, it turns out that they are not more powerful in the sense that any NFA can
be simulated by a DFA.
Theorem 1 Let M be a NFA. There exists a DFA M
) = L(M).
Proof: (sketch) Let M = (Q, , , q
0
, F) be a NFA. We will construct a DFA M
(S) = p Q [ q S (q, )
M
(p, ) (2.1)
Now we dene
M
= (2
Q
, ,
, s
0
, F
)
where
is dened by
s 2
Q
, a
(s, a) =
qs
(
(q
0
), i.e. M
= s 2
Q
[ s F ,= , i.e. if M could end up in a nal state, then M
will do so.
It can then be shown that L(M
) = L(M). 2
VUB-DINF/2009/2 31
2.4 Regular expressions vs nite state automata
In this section we show how a regular expression can be translated to a nondeter-
ministic nite automata that denes the same language. Using Theorem 1, we can
then translate regular expressions to DFAs and hence to a program that accepts
exactly the strings conforming to the regular expression.
Theorem 2 Let r be a regular expression. Then there exists a NFA M
r
such that
L(M
r
) = L
r
.
Proof:
We show by induction on the number of operators used in a regular expression r
that L
r
is accepted by an NFA
M
r
= (Q, , , q
0
, q
f
)
(where is the alphabet of L
r
) which has exactly one nal state q
f
satisfying
a (q
f
, a) = (2.3)
Base case
Assume that r does not contain any operator. Then r is one of , or a .
We then dene M
, M
and M
a
as shown in Figure 2.6.
a
M
a
M
Figure 2.6: M
, M
and M
a
Induction step
More complex regular expressions must be of one of the forms r
1
+r
2
, r
1
r
2
or r
1
.
In each case, we can construct a NFA M
r
1
+r
2
, M
r
1
r
2
or M
r
1
, based on M
r
1
and
M
r
2
, as shown in Figure 2.7.
VUB-DINF/2009/2 32
M
r
1
M
r
1
r
2
M
r
1
+r
2
M
r
1
M
r
2
q
0
q
f
q
0
q
f
M
r
1
q
0
q
f
M
r
2
M
r
1
Figure 2.7: M
r
1
+r
2
, M
r
1
r
2
and M
r
1
It can then be shown that
L(M
r
1
+r
2
) = L(M
r
1
) L(M
r
2
)
L(M
r
1
r
2
) = L(M
r
1
) L(M
r
2
)
L(M
r
1
) = L(M
r
1
)
2
2.5 A scanner generator
We can now be more specic on the design and operation of a scanner generator
such as lex(1) or ex(1L), which was sketched on page 25.
First we introduce the concept of a dead state in a DFA.
Denition 6 Let M = (Q, , , q
0
, F) be a DFA. A state q Q is called dead if
there does not exist a string w
M
(q
f
, ) for some q
f
F.
VUB-DINF/2009/2 33
Example 4 The state q
e
in Example 1 is dead.
It is easy to determine the set of dead states for a DFA, e.g. using a marking
algorithm which initially marks all states as dead and then recursively works
backwards from the nal states, unmarking any states reached.
The generator takes as input a set of regular expressions, R = r
1
, . . . , r
n
each of
which is associated with some code c
r
i
to be executed when a token corresponding
to r
i
is recognized.
The generator will convert the regular expression
r
1
+ r
2
+ . . . + r
n
to a DFA M = (Q, , , q
0
, F), as shown in Section 2.4, with one addition: when
constructing M, it will remember which nal state of the DFA corresponds with
which regular expression. This can easily be done by remembering the nal states
in the NFAs corresponding to each of the r
i
while constructing the combined DFA
M. It may be that a nal state in the DFA corresponds to several patterns (regular
expressions). In this case, we select the one that was dened rst.
Thus we have a mapping
pattern : F R
which associates the rst (in the order of denition) pattern to which a certain nal
state corresponds. We also compute the set of dead states of M.
The code in Figure 2.8 illustrates the operation of the generated scanner.
The scanner reads input characters, remembering the last nal state seen and the
associated regular expression, until it hits a dead state from where it is impossible
to reach a nal state. It then backs up to the last nal state and executes the code
associated with that pattern. Clearly, this will nd the longest possible token on
the input.
VUB-DINF/2009/2 34
1 typedef int STATE;
2 typedef char SYMBOL;
3 typedef enum {false,true} BOOL;
4
5 typedef struct { /
*
what we need to know about a user defined pattern
*
/
6 TOKEN
*
(
*
code)(); /
*
user-defined action
*
/
7 BOOL do_return; /
*
whether action returns from lex() or not
*
/
8 } PATTERN;
9
10 static STATE next_state[SYMBOL][STATE];
11 static BOOL dead[STATE];
12 static BOOL final[STATE];
13 static PATTERN
*
pattern[STATE]; /
*
first regexp for this final state
*
/
14 static SYMBOL
*
last_input = 0; /
*
input pointer at last final state
*
/
15 static STATE last_state, q = 0; /
*
assuming 0 is initial state
*
/
16 static SYMBOL
*
input; /
*
source text
*
/
17
18 TOKEN
*
19 lex()
20 {
21 SYMBOl c;
22 PATTERN
*
last_pattern = 0;
23
24 while (c=
*
input++) {
25 q = next_state[c,q];
26 if (final[q]) {
27 last_pattern = pattern[q];
28 last_input = input;
29 last_state = q;
30 }
31 if (dead[q]) {
32 if (last_pattern) {
33 input = last_input;
34 q = 0;
35 if (last_pattern->do_return)
36 return pattern->code();
37 else
38 pattern->code();
39 }
40 else /
*
error
*
/
41 ;
42 }
43 }
44
45 return (TOKEN
*
)0;
46 }
Figure 2.8: A generated scanner
Chapter 3
Parsing
3.1 Context-free grammars
As mentioned in Section 1.3.1, page 9, the rules (or railroad diagrams) used to
specify the syntax of a programming language can be formalized using the concept
of context-free grammar.
Denition 7 A context-free grammar (cfg) is a tuple
G = (V, , P, S)
where
V is a nite set of nonterminal symbols
is a nite set of terminal symbols, disjoint from V : V = .
P is a nite set of productions of the form A where A V and
(V )
,
we say that x derives y in one step, denoted x =
G
y iff x = x
1
Ax
2
, y = x
1
x
2
and A P. Thus =
G
is a binary relation on (V )
. The relation=
G
is the reexive and transitive closure of =
G
. The language L(G) generated by
G is dened by
L(G) = w
[ S =
G
w
A language is called context-free if it is generated by some context-free grammar.
A derivation in G of w
n
from w
0
is any sequence of the form
w
0
=
G
w
1
=
G
. . . =
G
w
n
where n 0 (we say that the derivation has n steps) and 1 i n w
i
(V )
We write v =
n
G
w (n 0) when w can be derived from v in n steps.
Thus a context-free grammar species precisely which sequences of tokens are
valid sentences (programs) in the language.
Example 6 Consider the grammar G
0
from Example 5. The following is a deriva-
tion in G where at each step, the symbol to be rewritten is underlined.
S =
G
0
E E
=
G
0
(E) E
=
G
0
(E + E) E
=
G
0
(E +id) E
=
G
0
(E +id) id
=
G
0
(id +id) id
VUB-DINF/2009/2 37
A derivation in a context-free grammar is conveniently represented by a parse
tree.
Denition 9 Let G = (V, , P, S) be a context-free grammar. A parse tree cor-
responding to G is a labeled tree where each node is labeled by a symbol from
V in such a way that, if A is the label of a node and A
1
A
2
. . . A
n
(n > 0) are
the labels of its children (in left-to-right order), then
A A
1
A
1
. . . A
n
is a rule in P. Note that a rule A gives rise to a leaf node labeled .
As mentioned in Section 1.3.4, it is the job of the parser to convert a string of
tokens into a parse tree that has precisely this string as yield. The idea is that the
parse tree describes the syntactical structure of the source text.
However, sometimes, there are several parse trees possible for a single string of
tokens, as can be seen in Figure 3.1.
E
E
S
E
E
id
id
id
S
+
E
E
E
E
id id
id
+
Figure 3.1: Parse trees in the ambiguous context-free grammar from Example 5
Note that the two parse trees intuitively correspond to two evaluation strategies
for the expression. Clearly, we do not want a source language that is specied
using an ambiguous grammar (that is, a grammar where a legal string of tokens
may have different parse trees).
Example 7 Fortunately, we can x the grammar from Example 5 to avoid such
ambiguities.
G
1
= (E, T, F, +, , (, ), id, P
, E)
VUB-DINF/2009/2 38
where P
consists of
E E + T [ T
T T F [ F
F (E) [ id
is an unambiguous grammar generating the same language as the grammar from
Example 5.
Still, there are context-free languages such as a
i
b
j
c
k
[ i = j j = k for which
only ambiguous grammars can be given. Such languages are called inherently
ambiguous. Worse still, checking whether an arbitrary context-free grammar al-
lows ambiguity is an unsolvable problem[HU69].
3.2 Top-down parsing
3.2.1 Introduction
When using a top-down (also called predictive) parsing method, the parser tries
to nd a leftmost derivation (and associated parse tree) of the source text. A left-
most derivation is a derivation where, during each step, the leftmost nonterminal
symbol is rewritten.
Denition 10 Let G = (V, , P, S) be a context-free grammar. For strings x, y
(V )
G
is the reexive and transitive closure of
L
=
G
. A derivation
y
0
L
=
G
y
1
L
=
G
. . .
L
=
G
y
n
is called a leftmost derivation. If y
0
= S (the start symbol) then we call each y
i
in such a derivation a left sentential form.
Is is not hard to see that restricting to leftmost derivations does not alter the lan-
guage of a context-free grammar.
VUB-DINF/2009/2 39
S
c
d
c a d
A
a
b
S
c a d
c a d
Try S cAd
Match c: OK.
Try A ab for A.
S
c
A
d
Match a: OK.
a
b
Try next predicted symbol b in tree.
No match: BACKTRACK.
Try next rule A a for A.
S
c
d
c a d
A
Match a: OK.
Try next predicted symbol d in tree.
S
c
d
c a d
A
a
S
c
d
c a d
A
a
Match d: OK.
Parse succeeded.
Figure 3.2: A simple top-down parse
Theorem 3 Let G = (V, , P, S) be a context-free grammar. If A V then
A =
G
w
iff A
L
=
G
w
G
Ax for
some x (V )
.
Indeed, even for a natural rule such as
E E + E
it is clear that the parse E() function would start by calling itself (in an
innite recursion), before reading any input.
We will see that it is possible to eliminate left recursion from a grammar
without changing the generated language.
Using backtracking is both expensive and difcult. It is difcult because it
usually does not sufce to simply restore the input pointer: all actions taken
by the compiler since the backtrack point (e.g. symbol table updates) must
also be undone.
The solution will be to apply top-down parsing only for a certain class of
restricted grammars for which it can be shown that backtracking will never
be necessary.
3.2.2 Eliminating left recursion in a grammar
First we look at the elimination of immediate left recursion where we have rules
of the form
A A [
where does not start with A.
The idea is to reorganize the rules in such a way that derivations are simulated as
shown in Figure 3.3
This leads to the following algorithm to remove immediate left recursion.
VUB-DINF/2009/2 42
A
A
A
A
A
[ . . . [
n
A
1
A
[ . . . [
m
A
[
2
Example 9 Consider the grammar G
1
from Example 7. Applying Algorithm 1
results in the grammar G
2
= (E, E
, T, T
, F, +, , (, ), id, P
G
2
, E) where
P
G
2
contains
E TE
+TE
[
T FT
FT
[
F (E) [ id
It is also possible (see e.g. [ASU86], page 177) to eliminate general (also indirect)
left recursion from a grammar.
VUB-DINF/2009/2 43
3.2.3 Avoiding backtracking: LL(1) grammars
When inspecting Example 8, it appears that in order to avoid backtracking, one
should be able to predict with certainty, given the nonterminal X of the current
function parse X() and the next input token a, which production X x was used
as a rst step to derive a string of tokens that starts with the input token a from X.
The problem is graphically represented in Figure 3.4
a
S
X
x
Figure 3.4: The prediction problem
The following denition denes exactly the class of context-free grammars for
which this is possible.
Denition 11 An LL(1) grammar is a context-free grammar G = (V, , P, S)
such that if
S
L
=
G
w
1
Xx
3
L
=
G
w
1
x
2
x
3
L
=
G
w
1
aw
4
and
S
L
=
G
w
1
X x
3
L
=
G
w
1
x
2
x
3
L
=
G
w
1
a w
4
where a , X V , w
1
, w
4
, w
4
then
x
2
= x
2
A language L is called an LL(1) language if there exists a LL(1) grammar G such
that L(G) = L.
VUB-DINF/2009/2 44
Intuitively, Denition 11 just says that if there are two possible choices for a pro-
duction, these choices are identical.
Thus for LL(1) grammars
1
, we know for sure that we can write functions as we
did in Example 8, without having to backtrack. In the next section, we will see
how to automatically generate parsers based on the same principle but without the
overhead of (possibly recursive) function calls.
3.2.4 Predictive parsers
Predictive parsers use a stack (representing strings in (V )
2
({})
is dened by
rst() = a [ =
G
aw
where
X
if =
G
otherwise
The function follow : V 2
({$})
is dened by
follow(A) = a [ S =
G
Aa Y
where
Y
$ if S =
G
A
otherwise
Intuitively, rst() contains the set of terminal symbols that can appear at the
start of a (terminal) string derived from , including if can derive the empty
string.
On the other hand, follow(A) consists of those terminal symbols that may follow
a string derived from A in a terminal string from L(G).
The construction of the LL(1) parse table can then be done using the following
algorithm.
VUB-DINF/2009/2 46
1 PRODUCTION
*
parse_table[NONTERMINAL,TOKEN];
2 SYMBOL End, S; /
*
marker and start symbol
*
/
3 SYMBOL
*
stack;
4 SYMBOL
*
top_of_stack;
5
6 TOKEN
*
input;
7
8 BOOL
9 parse() { /
*
LL(1) predictive parser
*
/
10 SYMBOL X;
11 push(End); push(S);
12
13 while (
*
top_of_stack!=End) {
14 X =
*
top_of_stack;
15 if (is_terminal(X)) {
16 if (X==
*
input) { /
*
predicted outcome
*
/
17 ++input; /
*
advance input
*
/
18 pop(); /
*
pop X from stack
*
/
19 }
20 else
21 error("Expected %s, got %s", X,
*
input);
22 }
23 else { /
*
X is nonterminal
*
/
24 PRODUCTION
*
p = parse_table[X,
*
input];
25 if (p) {
26 pop(); /
*
pop X from stack
*
/
27 for (i=length_rhs(p)-1;(i>=0);--i)
28 push(p->rhs[i]); /
*
push symbols of rhs, last first
*
/
29 }
30 else
31 error("Unexpected %s",
*
input);
32 }
33 }
34 if (
*
input==End)
35 return true;
36 else
37 error("Redundant input: %s",
*
input);
38 }
Figure 3.6: Predictive parser operation
Algorithm 3 [Construction of LL(1) parse table]
Let G = (V, , P, S) be a context-free grammar.
1. Initialize the table: A V, b M[A, b] =
2. For each production A from P
VUB-DINF/2009/2 47
(a) For each b rst() , add A to M[A, b].
(b) If rst() then
i. For each c follow(A) , add A to M[A, c].
ii. If $ follow(A) then add A to M[A, $].
3. If each entry in M contains at most a single production then return success,
else return failure.
2
It can be shown that, if the parse table for a grammar G was successfully con-
structed using Algorithm 3, then the parser of Algorithm 2 accepts exactly the
strings of L(G). Also, if Algorithm 3 fails, then G is not an LL(1) grammar.
Example 10 Consider grammar G
2
from Example 9. The rst and follow func-
tions are computed in Examples 11 and 12. The results are summarized below.
E E
T T
F
rst (, id +, (, id , (, id
follow $, ) $, ) +, $, ) $, ), + , $, ), +
Applying Algorithm 3 yields the following LL(1) parse table.
E E
T T
F
id E TE
T FT
F id
+ E
+TE
FT
( E TE
T FT
F (E)
) E
$ E
The operation of a predictive parser using the above table is illustrated below for
VUB-DINF/2009/2 48
the input string id +id id.
stack input rule
$E id +id id$
$E
T id +id id$ E TE
$E
F id +id id$ T FT
$E
id id +id id$ F id
$E
+id id$
$E
+id id$ T
$E
T+ +id id$ E
+TE
$E
T id id$
$E
F id id$ T FT
$E
id id id$ F id
$E
id$
$E
F id$ T
FT
$E
F id$
$E
id id$ F id
$E
$
$E
$ T
$ $ E
3.2.5 Construction of rst and follow
Algorithm 4 [Construction of rst]
Let G = (V, , P, S) be a context-free grammar. We construct an array F : V 2
({})
and then show a function to compute rst() for arbitrary (V )
.
1. F is constructed via a xpoint computation:
(a) for each X V , initialize F[X] a [ X a, a
(b) for each a , initialize F[a] a
2. repeat the following steps
(a) F
F
(b) for each rule X , add to F[X].
(c) for each rule X Y
1
. . . Y
k
(k > 0) do
i. for each 1 i k such that Y
1
. . . Y
i1
V
= F
Dene
rst(X
1
. . . X
n
) =
1j(k+1)
(F[X
j
] ) if k(X
1
. . . X
n
) < n
1jn
F[X
j
] ) if k(X
1
. . . X
n
) = n
where k(X
1
. . . X
n
) is the largest index k such that X
1
. . . X
k
=
G
, i.e.
k(X
1
. . . X
n
) = max1 i n [ 1 j i F[X
j
2
Example 11 Consider grammar G
2
from Example 9.
The construction of F is illustrated in the following table.
E E
T T
F
+ (, id initialization
rule E
(, id rule T FT
rule T
(, id rule E TE
(, id +, (, id , (, id
Algorithm 5 [Construction of follow]
Let G = (V, , P, S) be a context-free grammar. The follow set of all symbols in
V can be computed as follows:
1. follow(S) $
2. Apply the following rules until nothing can be added to follow(A) for any
A V .
(a) If there is a production A B in P then
follow(B) follow(B) (rst() )
(b) If there is a production A B or a production A B where
rst() then follow(B) follow(B) follow(A).
2
VUB-DINF/2009/2 50
Example 12 Consider again grammar G
2
from Example 9.
The construction of follow is illustrated in the following table.
E E
T T
F
$ initialization
+ rule E TE
) rule F (E)
rule T FT
$, ) rule E TE
$, ) rule E TE
$, ), + rule T FT
$, ), + rule T FT
$, ) $, ) +, $, ) $, ), + , $, ), +
3.3 Bottom-up parsing
3.3.1 Shift-reduce parsers
As described in Section 3.2, a top-down parser simulates a leftmost derivation in a
top-down fashion. This simulation predicts the next production to be used, based
on the nonterminal to be rewritten and the next input symbol.
Abottom-up parser simulates a rightmost derivation in a bottom-up fashion, where
a rightmost derivation is dened as follows.
Denition 13 Let G = (V, , P, S) be a context-free grammar. For strings x, y (V )
,
we say that x derives y in a rightmost fashion and in one step, denoted
x
R
=
G
y
iff x = x
1
Ax
2
, y = x
1
x
2
, A is a rule in P and x
2
G
is the reexive and transitive closure of
R
=
G
. A derivation
x
0
R
=
G
x
1
R
=
G
. . .
R
=
G
x
n
is called a rightmost derivation. If x
0
= S (the start symbol) then we call each x
i
in such a derivation a right sentential form.
VUB-DINF/2009/2 51
A phrase of a right sentential form is a sequence of symbols that have been de-
rived from a single nonterminal symbol occurrence (in an earlier right sentential
form of the derivation). A simple phrase is a phrase that contains no smaller
phrase. The handle of a right sentential form is its leftmost simple phrase (which
is unique). A prex of a right sentential form that does not extend past its handle
is called a viable prex of G.
Example 13 Consider the grammar
G
3
= (S, E, T, F, +, , (, ), id, P
G
3
, S)
where P
G
3
consists of
S E
E E + T [ T
T T F [ F
F (E) [ id
G
3
is simply G
1
from Example 7, augmented by a new start symbol whose only
use is at the left hand side of a single production that has the old start symbol as
its right hand side.
The following table shows a rightmost derivation of id+idid; where the handle
in each right sentential form is underlined.
S
E
E + T
E + T F
E + T id
E + F id
E +id id
T +id id
F +id id
id +id id
When doing a bottom-up parse, the parser will use a stack to store the right senten-
tial form up to the handle, using shift operations to push symbols from the input
onto the stack. When the handle is completely on the stack, it will be popped and
replaced by the left hand side of the production, using a reduce operation.
VUB-DINF/2009/2 52
Example 14 The table below illustrates the operation of a shift-reduce parser to
simulate the rightmost derivation from Example 13. Note that the parsing has to
be read from the bottom to the top of the gure and that both the stack and the
input contain the end marker $.
derivation stack input operation
S $S $ accept
E $E $ reduce S E
E + T $E + T $ reduce E E + T
E + T F $E + T F $ reduce T T F
$E + T id $ reduce F id
$E + T id$ shift
E + T id $E + T id$ shift
E + F id $E + F id$ reduce T F
$E +id id$ reduce F id
$E+ id id$ shift
E +id id $E +id id$ shift
T +id id $T +id id$ reduce E T
F +id id $F +id id$ reduce T F
$id +id id$ reduce F id
id +id id $ id +id id$ shift
Clearly, the main problem when designing a shift-reduce parser is to decide when
to shift and when to reduce. Specically, the parser should reduce only when the
handle is on the top of the stack. Put in another way, the stack should always
contain a viable prex.
We will see in Section 3.3.2 that the language of all viable prexes of a context-
free grammar Gis regular, and thus there exists a DFAM
G
= (Q, V , , q
0
, F)
accepting exactly the viable prexes of G. The operation of a shift-reduce parser
is outlined in Figure 3.7
2
.
However, rather than checking each time the contents of the stack (and the next
input symbol) vs. the DFA M
G
, it is more efcient to store the states together with
the symbols on the stack in such a way that for each prex xX of (the contents
of) the stack, we store the state
(q
0
, xX) together with X on the stack.
In this way, we can simply check
M
G
(q, a) where q Q is the state on the top of
the stack and a is the current input symbol, to nd out whether shifting the input
would still yield a viable prex on the stack. Similarly, upon a reduce(A )
2
We assume, without losing generality, that the start symbol of G does not occur on the right
hand side of any production
VUB-DINF/2009/2 53
1 while (true) {
2 if ((the top of the stack contains the start symbol) &&
3 (the current input symbol is the endmarker $))
4 accept;
5 else if (the contents of the stack concatenated with the next
6 input symbol is a viable prefix)
7 shift the input onto the top of the stack;
8 else
9 if (there is an appropriate producion)
10 reduce by this production;
11 else
12 error;
13 }
Figure 3.7: Naive shift-reduce parsing procedure
operation, both A and (q, A) are pushed where q is the state on the top of the
stack after popping .
In practice, a shift-reduce parser is driven by two tables:
An action table that associates a pair consisting of a state q Q and a
symbol a (the input symbol) with an action of one of the following
types:
accept, indicating that the parse nished successfully
shift(q
.
reduce(A ) where A is a production from G. This in-
structs the parser to pop (and associated states) from the top of
the stack, then pushing A and the state q
given by q
= goto(q
, A)
where q
is the state left on the top of the stack after popping . Here
goto(q
, A) =
M
G
(q
, A).
error, indicating that
M
G
(q, a) yields a dead state.
A goto table:
goto : QV Q
which is used during a reduce operation.
The architecture of a shift-reduce parser is shown in Figure 3.8.
Using the action and goto tables, we can rene the shift-reduce parsing algorithm
as follows.
VUB-DINF/2009/2 54
input
$
$
E
+
T
+ id id
q
0
q
1
q
2
q
3
q
4
goto: V Q Q
stack
SHIFT-REDUCE
PARSER
id
action : Q ACTION
Figure 3.8: A shift-reduce parser
Algorithm 6 [Shift-reduce parsing]
See Figure 3.9 on page 55. 2
3.3.2 LR(1) parsing
Denition 14 Let G = (V, , P, S
S
is the only production for S
and S
S], items
G
)
where is dened by
[A X , a] ([A X, a], X) for all X V
[X , b] ([A X, a], ) if X is in P and b rst(a)
VUB-DINF/2009/2 55
1 TOKEN
*
input; /
*
array of input tokens
*
/
2 STATE
*
states; /
*
(top of) stack of states
*
/
3 SYMBOL
*
symbols; /
*
(top of) stack of symbols
*
/
4
5 BOOL
6 parse() {
7 while (true)
8 switch (action[
*
states,
*
input]) {
9 case shift(s):
10
*
++symbols =
*
input++; /
*
push symbol
*
/
11
*
++states = s; /
*
push state
*
/
12 break;
13 case reduce(X->x):
14 symbols -= length(x); /
*
pop rsh
*
/
15 states -= length(x); /
*
also for states
*
/
16 STATE s = goto[
*
states,X];
17
*
++states = s;
*
++symbols = X;
18 break;
19 case accept:
20 return true;
21 case error:
22 return false;
23 }
24 }
Figure 3.9: Shift-reduce parser algorithm
Note that all states in N
G
are nal. This does not mean that N
G
accepts any string
since there may be no transitions possible on certain inputs from certain states.
Intuitively, an item [A , a] can be regarded as describing the state where
the parser has on the top of the stack and next expects to process the result of a
derivation from , followed by an input symbol a.
To see that L(N
G
) accepts all viable prexes. Consider a (partial)
3
rightmost
derivation
S = X
0
R
=
G
x
0
X
1
y
0
R
=
G
x
0
x
1
X
2
y
1
R
=
G
x
0
. . . x
n1
X
n
y
n
R
=
G
x
0
. . . x
n1
x
n
y
n
where y
i
X
0
, $] the initial state
[X
0
x
0
X
1
z
0
, b
0
] using an -move, b
0
rst($) = $
[X
0
x
0
X
1
z
0
, b
0
] reading x
0
[X
1
x
1
X
2
z
1
, b
1
] using an -move, b
1
rst(z
0
b
0
)
[X
1
x
1
X
2
z
1
, b
1
] reading x
1
. . .
[X
n1
x
n1
X
n
z
n1
, b
n1
]
[X
n1
x
n1
X
n
z
n1
, b
n1
] reading x
n1
[X
n
x
n
, b
n
] using an -move, b
n
rst(z
n1
b
n1
)
. . . any prex of x
n
can be read
Note that all b
i
exist since each z
i
derives a terminal string.
To see the reverse, it sufces to note that each accepting sequence in M
G
is of
the form sketched above and thus, a partial rightmost derivation like the one in
Figure 3.10 can be inferred from it.
VUB-DINF/2009/2 57
Theorem 4 Let G = (V, , P, S
([S
S, $]), 2
items
G
)
be the DFA corresponding to the viable prex automaton N
G
of G.
Add an action accept to action[s, $] whenever [S
S, $] s.
Add an action shift(s
.
Add an action reduce(A ) to action[s, a] whenever s contains an item
[A , a] where A ,= S
.
For any X V , s a state of M
G
, dene goto(s, X) = (s, X).
After the above rules have been exhaustively applied, add error to all entries in
action that remain empty.
The algorithm succeeds if action[s, a] contains exactly one action for each reach-
able s 2
items
G
, a . 2
Note that it follows from Theorem 4 and Algorithm 7 that the resulting parser will
announce an error at the latest possible moment, that is only when shifting would
result in a stack that does not represent a viable prex.
If Algorithm 7 does not succeed, there exists either a shift-reduce conict,
when an entry in the action table contains both a shift and a reduce operation, or
a reduce-reduce conict, when an entry in the action table contains two or more
reduce actions.
Example 15 Consider G
3
from Example 13. The DFA accepting the viable pre-
xes of G is shown in Figures 3.11 and 3.12 Note that [A , abc] is used as
shorthand for [A , a], [A , b], [A , c]
Theorem 5 Let G = (V, , P, S
S $ 11 E E + T +$
S E $ T T F + $
E E + T +$ 12 F (E) + $
E T +$ E E +T )+
T T F + $ 13 F (E) + $
T F + $ 14 T F ) +
F id + $ 15 F (E) ) +
F (E) + $ 16 F (E) ) +
1 S E $ E E +T )+
E E +T +$ 17 E E +T )+
2 S
S $ T T F ) +
3 E E +T +$ T F ) +
T T F + $ F id ) +
T F + $ F (E) ) +
F id + $ 18 F (E) ) +
F (E) + $ E E + T )+
4 T F + $ E T )+
5 F id + $ T T F ) +
6 F (E) + $ T F ) +
E E + T )+ F id ) +
E T )+ F (E) ) +
T T F ) + 19 E T )+
T F ) + T T F ) +
F id ) + 20 E E + T )+
F (E) ) + T T F ) +
7 E T +$ 21 T T F ) +
T T F + $ F id ) +
8 T T F + $ F (E) ) +
F id + $ 22 T T F ) +
F (E) + $
9 T T F + $
10 F id ) +
Figure 3.11: States of the viable prex DFA of G
3
Example 16 The action and goto tables of the LR(1) parser for G
3
are shown in
Figure 3.13. The rules of G
3
have been numbered as follows:
1 S E 5 T F
2 E E + T 6 F id
3 E T 7 F (E)
4 T T F
A parse of
id +id id
VUB-DINF/2009/2 59
M
G
3
S E T F id ( ) +
0 2 1 7 4 5 6
1 3
2
3 11 4 5 6
4
5
6 19 14 10 18
7 8
8 9 5 6
9
10
11 8
12 13 17
13
14
15
16 17
17 20 14 10 18
18 19 14 10 18
19 21
20 21
21 22 10
22
Figure 3.12: Transition function of the viable prex DFA of G
3
is shown below (states on the stack are between []).
stack input operation
$[0] id +id id$ shift
$[0]id[5] +id id$ reduce F id
$[0]F[4] +id id$ reduce T F
$[0]T[7] +id id$ reduce E T
$[0]E[1] +id id$ shift
$[0]E[1] + [3] id id$ shift
$[0]E[1] + [3]id[5] id$ reduce F id
$[0]E[1] + [3]F[4] id$ reduce T F
$[0]E[1] + [3]T[11] id$ shift
$[0]E[1] + [3]T[11] [8] id$ shift
$[0]E[1] + [3]T[11] [8]id[5] $ reduce F id
$[0]E[1] + [3]T[11] [8]F[9] $ reduce T T F
$[0]E[1] + [3]T[11] $ reduce E E + T
$[0]E[1] $ reduce S E
$[0]S[2] $ accept
It will turn out that Algorithm 7 succeeds exactly for so-called LR(1) grammars.
VUB-DINF/2009/2 60
goto action
state S E T F id ( ) + $
0 2 1 7 4 s5 s6
1 s3 r1
2 a
3 11 4 s5 s6
4 r5 r5 r5
5 r6 r6 r6
6 19 14 s10 s18
7 r3 s8 r3
8 9 s5 s6
9 r4 r4 r4
10 r6 r6 r6
11 r2 s8 r2
12 s13 s17
13 r7 r7 r7
14 r5 r5 r5
15 r7 r7 r7
16 s17
17 20 14 s10 s18
18 19 14 s10 s18
19 r3 r3 s21
20 r2 r2 s21
21 22 s10
22 r4 r4 r4
Figure 3.13: LR(1) tables for G
3
VUB-DINF/2009/2 61
Denition 15 An LR(1) grammar is a context-free grammar G = (V, , , P, S)
such that S does not appear at the right hand side of any production. Moreover, if
S
R
=
G
x
1
Xw
3
R
=
G
x
1
x
2
w
3
and
S
R
=
G
x
1
X w
3
R
=
G
x
1
x
2
w
3
and
pref
l+1
(x
1
x
2
w
3
) = pref
l+1
( x
1
x
2
w
3
)
where X V , w
3
w
3
, l = [x
1
x
2
[, then
x
1
= x
1
X = X
A language L is called LR(1) if there exists an LR(1) context-free grammar Gsuch
that L(G) = L.
Intuitively, in the above denition, x
1
x
2
represents the stack just before a reduce
operation in a shift-reduce parser. The denition then says that there is only a
single choice for when to reduce, based only on the next input symbol
4
.
Theorem 6 A context-free grammar G = (V, , , P, S), where S does not ap-
pear on the right hand side of any production, is LR(1) iff Algorithm 7 succeeds.
One may wonder about the relationship between LL(k) and LR(k) grammars and
languages. It turns out (see e.g. [Sal73]) that every LR(k) (k 0) language
can be generated by an LR(1) grammar. On the other hand, there are context
free languages, e.g. the inherently ambiguous language of Section 3.1, page 38,
that are not LR(1). In fact, it can be shown that all deterministic context-free
languages
5
are LR(1) (and, obviously, also the reverse). As for LL(k) languages,
they form a proper hierarchy in that for every k > 1 there is an LL(k) language
L
k
for which no LL(k 1) grammar exists. Still, every LL(k) language is LR(1).
Summarizing, as far as parsing power is concerned, bottom-up (LR(1)) parsers
are provably superior to top-down (LL(1)) parsers.
4
One can also dene LR(k) grammars and languages for any k 0. It sufces to replace l +1
by l + k in Denition 15
5
A context-free language is deterministic if it can be accepted by a deterministic pushdown
automaton.
VUB-DINF/2009/2 62
3.3.3 LALR parsers and yacc/bison
LR(1) parsing tables can become quite large due to the large number of states in
the viable prex DFA. A DFA for a language like Pascal may have thousands of
states.
In an LALR parser, one tries to reduce the number of states by merging states
that have the same (extended) productions in their items. E.g. in the DFA of
Example 15, one may merge, among others, state 3 and state 17.
It is then not hard to see that, due to the way states are constructed using -
closures, if s
1
and s
2
can be merged, then so can (s
1
, X) and (s
2
, X). This
yields a more compact DFA which is almost equivalent to the original one in the
sense that compaction cannot produce shift-reduce conicts although new reduce-
reduce conicts are possible.
It can be shown that if the LALR compaction succeeds (i.e. does not lead to new
reduce-reduce conicts), the resulting parser accepts the same language as the
original LR(1) parser.
The parser-generator yacc (and bison) generates an LALR parser based on an
input grammar. In addition, yacc (and bison) provide facilities for resolving shift-
reduce conicts based on the declared precedence of operators.
Bison will resolve a shift reduce conict on a state like
E Eo
1
E , o
2
E E o
2
E
by comparing the declared precedences of o
1
and o
2
: if o
1
has the higher prece-
dence, then reduce will be selected. If o
2
has a higher precedence than o
1
then shift
will be selected. If o
1
and o
2
have the same precedence, associativity information
on o
1
and o
2
will be used: if both symbols (operators) are left-associative, reduce
will be selected; if both symbols are right-associative, shift will be selected; if
both symbols are non-associative an error will be reported
6
. If a symbol (such as
MINUS in Example 17 below) is used with two precedences, bison allows you to
dene the precedence of the rule using a %prec directive
7
The default conict resolution strategy of bison (and yacc) is as follows:
6
Note that operators with the same precedence must have the same associativity.
7
The above description is made more concrete in the bison manual:
The rst effect of the precedence declarations is to assign precedence levels to the terminal
symbols declared. The second effect is to assign precedence levels to certain rules: each rule gets
its precedence from the last terminal symbol mentioned in the components (o
1
in the example).
(You can also specify explicitly the precedence of a rule.)
VUB-DINF/2009/2 63
Choose shift when resolving a shift-reduce conict.
Choose reduce by the rst production (in the order of declaration) when
resolving a reduce-reduce conict.
Example 17 The following is the bison input le for a highly ambiguous gram-
mar, where ambiguities are resolved using the precedence/associativity dening
facilities of bison/yacc.
1
2 %token NAME NUMBER LPAREN RPAREN EQUAL PLUS MINUS
3 %token TIMES DIVIDE IF THEN ELSE
4
5 /
*
associativity and precedence: in order of increasing precedence
*
/
6
7 %nonassoc LOW /
*
dummy token to suggest shift on ELSE
*
/
8 %nonassoc ELSE /
*
higher than LOW
*
/
9
10 %nonassoc EQUAL
11 %left PLUS MINUS
12 %left TIMES DIVIDE
13 %left UMINUS /
*
dummy token to use as precedence marker
*
/
14
15 %%
16
17 stmt : IF exp THEN stmt %prec LOW
18 | IF exp THEN stmt ELSE stmt /
*
shift will be selected
*
/
19 | /
*
other types of statements would come here
*
/
20 ;
21
22 exp : exp PLUS exp
23 | exp MINUS exp
24 | exp TIMES exp
25 | exp DIVIDE exp
26 | exp EQUAL exp
27 | LPAREN exp RPAREN
28 | MINUS exp %prec UMINUS /
*
this will force a reduce
*
/
29 | NAME
30 | NUMBER
Finally, the resolution of conicts works by comparing the precedence of the rule (of o
1
in the
example, unless that rules precedence has been explicitly dened) being considered (for reduce)
with that of the look-ahead token (o
2
in the example). If the tokens precedence is higher, the
choice is to shift. If the rules precedence is higher, the choice is to reduce. If they have equal
precedence, the choice is made based on the associativity of that precedence level. The verbose
output le made by -v (see section Invoking Bison) says how each conict was resolved.
Not all rules and not all tokens have precedence. If either the rule or the look-ahead token has
no precedence, then the default is to shift.
VUB-DINF/2009/2 64
31 ;
32 %%
Chapter 4
Checking static semantics
4.1 Attribute grammars and syntax-directed trans-
lation
Attribute grammars extend context-free grammars by associating a set of attributes
with each grammar symbol. Such attributes can be used to present information
that cannot easily (if at all) be expressed using only syntactical constructs (e.g.
productions). Thus each node in a parse tree has values for each of the attributes
associated with the corresponding symbol.
For example, we could associate an attribute type with the nonterminal expression
to represent the type of the expression.
Attributes for nodes are computed using functions that take attribute values of
other nodes as input. The computation is specied by semantic actions which
are associated with each production. Since such actions can only reference the
symbols in the production, the value of an attribute of a node depends only on the
attribute values of its parent, children and siblings.
Example 18 Consider the grammar
G
4
= (E, +, , , /, (, ), number, P, E)
where P consists of
E number [ (E) [ E + E [ E E [ E/E [ E E [ E
We dene an attribute value for E and number. The semantic actions are as
follow:
65
VUB-DINF/2009/2 66
E.value = 12
E.value = 24
+
E.value = 4
number.value = 4
number.value = 3
number.value = 12
E.value = 12
E.value = 3
Figure 4.1: Computing synthesized attribute values
E number E
0
.value = number
1
.value
E (E) E
0
.value = E
1
.value
E E + E E
0
.value = E
1
.value + E
3
.value
E E E E
0
.value = E
1
.value E
3
.value
E E E E
0
.value = E
1
.value E
3
.value
E E/E E
0
.value = E
1
.value/E
3
.value
E E E
0
.value = E
1
.value
Note that we use subscripts to refer to a particular occurrence of a symbol in the
rule (subscript 0 is used for the left-hand side of the rule).
The attribute values for a parse tree of 12 + 3 4 are shown in Figure 4.1. Note
that the number.value is supposed to have been provided by the lexical analyzer.
The actions in Example 18 all compute an attribute value for a node based on the
attribute values of its children. Attributes that are always computed in this way
are called synthesized attributes. Attributes whose value depends on values of its
parent and/or siblings are called inherited attributes.
The following example shows a grammar with both synthesized and inherited
attributes.
VUB-DINF/2009/2 67
Example 19 Consider the grammar
G
5
= (D, T, L,
.
2. Construct the dependency graph g
t
as described above.
1
A topological sort of a directed (acyclic) graph is any total ordering
x
1
< x
2
< . . . < x
n
< . . . of the nodes of the graph that satises x
i
< x
j
whenever there
is an edge from x
i
to x
j
.
VUB-DINF/2009/2 68
x
,
L.type = int
y
id.type = int
L.type = int
D
;
id.type = int
z
,
L.type = int
id.type = int
T.type = int
Figure 4.2: Computing inherited attribute values
3. G() is the value corresponding to some selected attribute of the root of t
.
In practice, the full parse tree for an input program is often not explicitly con-
structed. Rather, attributes are computed and stored on a stack that mirrors the
parsing stack (just like the stack containing the states of the viable prex DFA)
and actions are performed upon a reduce operations. This allows computation
of synthesized attributes but inherited attributes are not easily supported although
in certain cases it is possible to also evaluate inherited attributes (see [ASU86],
pages 310-311).
In yacc and bison, each symbol (nonterminal or token) can have only one at-
tribute, not larger than a single word. The type of an attribute is xed per symbol
using %type declarations, as can be seen in the example parser in Appendix C.6,
page 175. The set of used types must also be declared in a %union declaration.
The restriction to a single word is not limiting: usually the attribute is declared to
be a pointer to a structure.
4.2 Symbol tables
The restriction of shift-reduce parsers to use only synthesized attributes can be
overcome by using a global symbol table to share information between various
VUB-DINF/2009/2 69
parts of the parse tree. In general, a symbol table maps names (of variables, func-
tions, types, . . . ) to information on this name which has been accumulated by the
parser and semantic actions.
We discuss two aspects of symbol tables:
The management of names (the domain of the mapping).
The organization of the symbol table to take into account the scope rules of
the language.
4.2.1 String pool
Since a symbol table maps names to information, one could implement a symbol
table entry as a structure where the rst eld is the name and the other elds
contain the information associated with the name
1 struct symtab_entry {
2 char name[MAX_NAME_LENGTH];
3 INFO info;
4 };
However, because modern programming languages do not impose a small limit
on the length of a name, it is wasteful or impossible to keep this organization.
Hence one typically stores names contiguously in a so-called string pool array,
declaring a symbol table entry as follows:
1 struct symtab_entry {
2 char
*
name; /
*
points into string pool
*
/
3 INFO info;
4 };
For fast retrieval, entries may be organized into a hash table, as illustrated in
Figure 4.3 where separate chaining is used to link entries that correspond to the
same hash value.
4.2.2 Symbol tables and scope rules
Many programming languages impose scope rules that determine which deni-
tion of a name corresponds to a particular occurrence of that name in a program
text. E.g. in Pascal, and in C, scopes may be nested to an arbitrary depth, as is
illustrated in the following fragment.
VUB-DINF/2009/2 70
a b c \0 a l o n g i s h n a m e \0 x y z u \0
symbol table entry
hash table
Figure 4.3: A simple symbol table using a string pool and hashing
1 float x; /
*
def 1
*
/
2
3 int
4 f(int a)
5 {
6 while (a>0) {
7 double x = a
*
a,i; /
*
def 2
*
/
8
9 for (i=0;(i<a);++i) {
10 int x = a+i,b; /
*
def 3
*
/
11 b = x+2; /
*
refers to def 3
*
/
12 }
13
14 }
15 }
In the above examples, the symbol table will contain three entries (denitions) for
x at the time the inner loop is processed. Which denition is meant is determined
using the most closely nested scope rule.
In a compiler for a language with scope rules, when a semantic action wants to
retrieve the information associated with a certain occurrence of a name, it has to
know which denition of the name corresponds to the given occurrence. If the
language allows only nested scopes for which the most closely nested scope
rule holds, this can be solved by replacing the symbol table by a stack of simple
VUB-DINF/2009/2 71
scope
current scope
string pool
parent
Figure 4.4: A stacked symbol table
symbol tables, one for each scope. Moreover, each simple table refers to its parent
in the stack which contains symbol denitions from the enclosing scope. When
looking up a name, it sufces to search the simple table at the top of the stack. If
the name is not found, the parent table (corresponding to the enclosing scope) is
searched and so on.
When the parser nishes a scope it pops the current scope from the stack and
continues with the parent scope as the current one. Of course, if the scope is still
needed, as is the case e.g. in C++ with scopes that correspond to class declara-
tions, the scope will not be thrown away but a reference to it will be kept, e.g. in
the symbol table entry corresponding to the class name.
This organization is depicted in Figure 4.4.
An simple implementation of this scheme, where hashing is replaced by linear
search (rst in the current scope, then in the parent scope), may be found in Ap-
pendix C.3, page 163.
4.3 Type checking
Informally, the so-called static semantics of a programming language describes
the properties of a program that can be veried without actually executing it. Typ-
ically, for statically-types languages, this consists of verifying the well-typed
property where a program is well-typed if all its constructs (statements, expres-
sions, . . . ) obey the type rules of the language.
The type system of a programming language consists of
A denition of the set of all possible types.
VUB-DINF/2009/2 72
Type rules that determine the type of a language construct.
The language from Appendix C, Minic, has two primitive types: integers and
oats and a few type constructors, as shown by the following domain
2
denition
3
:
type :: int
:: oat
:: array(type)
:: struct(tenv)
:: type
type
tenv :: name type
Note that the above denition is more general than is actually needed in Minic,
which has no higher order functions. E.g. it is not possible to dene a function
with type
int (int int)
in Minic.
The type rules of Minic can be symbolically represented as follows (we use x : t to
denote that x has type t). In the table below we use + to use any of the arithmetic
operators +, , /, .
t x; x : t // declaration of x
x : int y : int (x + y) : int
x : oat y : oat (x + y) : oat
x : t y : t (x == y) : int // support = test on any type
x : array(t) i : int x[i] : t
x : struct(s) n dom(s) x.n : s(n)
x = e x : t e : t
t ,= t
error
1 i n x
i
: t
i
f : t
1
t
2
. . . t
n
t f(x
1
, . . . , x
n
) : t
return x x : t
f : t
1
t
2
. . . t
n
t t
1
e
2
. . . e
where e
i
, 1 i k, is the postx form of e
i
.
Evaluation of postx expressions is trivial, provided one uses an operand stack, as
shown below.
1 operand
2 eval_operator(OPERATOR o,item args[]);
3
4 operand
5 eval_postfix(item code[],int code_len)
6 {
7 operand stack[MAX_STACK];
8 int tos = 0; // top of stack
9
10 for (int i=0;(i<code_len);++i)
11 if (is_operand(code[i]))
12 stack[tos++] = code[i]; // push on stack
13 else
14 { // operator: pop args, eval, push result
15 k = arity(code[i]);
16 stack[tos-k] = eval_operator(code[i].oprtr, stack-k);
17 tos -= (k-1);
18 }
19 return stack[0];
20 }
By introducing labels which refer to a position in the postx expression, arbitrary
control structures can be implemented using instructions like
label jump
e
1
e
2
label jumplt
VUB-DINF/2009/2 76
In order to interpret labels and jump instructions, the above algorithm must of
course be extended to use a program counter (an index in the code array),
Postx notation is attractive because it can be easily generated using syntax-
directed translation. Below, we use an synthesized attribute code representing
the postx notation of the corresponding symbol occurrence. The statements
associated with a rule dene the attribute of the left hand side of the rule (denoted
$$), based on the attribute values of the symbols in its right hand side ($i refers to
the ith symbol in the rules right hand side).
1 exp : exp binop exp {
2 $$.code = concat($1.code, $3.code, operator($2));
3 }
4 exp : ( exp) {
5 $$.code = $1.code;
6 }
7 exp : var { $$.code = $1; /
*
probably a symtab ref
*
/ }
This scheme has the so-called simple postx property which means that for a pro-
duction
A A
1
A
2
. . . A
n
the generated code is such that
2
code(A) = code(A
1
) + . . . + code(A
n
) + tail
which implies that code can be emitted as soon as a reduction occurs.
Postx code is often used for stack-oriented virtual machines without general pur-
pose registers, such as the Java virtual machine (see e.g. the micro compiler in
Appendix B. It is not particularly suitable for optimization (code movement is
hard).
5.2 Abstract syntax trees
Abstract syntax trees are pruned parse trees where all nodes that contain re-
dundant (e.g. with respect to the tree structure) information have been removed.
Indeed, many nodes in the parse tree result from syntactic sugar in the grammar
which is needed to ensure that the language can be efciently parsed. Once we
have parsed the text, there is no need to keep these superuous nodes
3
. E.g. the
abstract syntax tree corresponding to the parse tree of Figure 1.6 on page 14 is
shown in Figure 5.1.
2
We use + to denote concatenation.
3
It should be noted that a formal denition of a languages semantics is often based on a so-
called abstract syntax which only species the structure of the languages constructs, rather than
VUB-DINF/2009/2 77
<program>
<statement> <statement_list>
<statement> <statement_list>
<assignment>
(xyz)
<NAME>
<statement>
<write_statement> (xyz)
<NAME>
<declaration>
<MINUS>
<PLUS>
<NUMBER>
(33)
<NUMBER>
(3)
<NUMBER>
(35)
<statement_list>
<>
<var>
<NAME>
(xyz)
Figure 5.1: Abstract syntax tree corresponding to the program in Figure 1.3
Abstract syntax trees (and directed acyclic graphs), particularly of expressions,
are useful for certain optimizations such as common subexpression elimination
and optimal register allocation (for expression computation).
An abstract syntax tree can be generated using synthesized attributes.
1 exp : exp binop exp {
2 $$.tree = new_node_2($2,$1.tree,$3,tree);
3 }
4 %
*
5 exp : ( exp ) {
6 $.tree = $2.tree;
7 }
8 %
*
9 exp : unop exp {
10 $$.tree = new_node_1($1,$2.tree);
11 }
12 %
*
13 exp : var {
14 $$.tree = new_node_0($1);
15 }
Abstract syntax trees can also be constructed for complete source texts. They then
the concrete syntax. E.g. in the abstract syntax specication, one would have a rule
assignment var expression
rather than
assignment var = expression
VUB-DINF/2009/2 78
form the input for code generators that are based on pattern matching (in trees)
and tree transformations. This strategy is employed by so-called code-generator
generators (see e.g. Section 9.12 in [ASU86]) which take as input tree-rewriting
rules of the form
replacement template action
where the template species a tree pattern that must be matched, the replacement
is a tree (usually a single node) that is to replace the matched template and the
action is a code fragment, as in syntax-directed translation. The code fragment
may check additional constraints and generates target instructions. Figure 5.2
illustrates such tree-rewriting rules. The idea is that to retarget the compiler to
reg
i
var
a
var
a
:=
var
a
reg
i
MOV r
i
, a
MOV a, r
i
.1 10
Q .1 1.5
As the table shows, while compiler optimization can result in a tenfold speedup,
as the size of the problem grows, an unoptimized superior algorithm easily catches
up with the optimized version of a slow algorithm. Hence, if speed is an issue,
there is no excuse for not designing an efcient algorithm before relying on the
compiler for further optimization.
Finally, one should be aware that machine-independent code improvement tech-
niques, such as the ones discussed in this chapter, are only one source of optimiza-
tion that can be performed by the compiler. Indeed, during the code generation
phase, there are additional opportunities for the compiler to dramatically improve
program running times, e.g. by judicious allocation of variables to registers.
VUB-DINF/2009/2 94
6.2 Local optimization of basic blocks
A sequence of intermediate code instructions is converted to a ow graph where
the nodes consist of basic blocks as follows.
Denition 16 Let s be a sequence of intermediate code instructions. A leader
instruction is any instruction s[i] such that
i = 0, i.e. s[i] is the rst instruction; or
s[i] is the target of a (conditional or unconditional) jump instruction; or
s[i 1] is a (conditional or unconditional) jump instruction
A basic block is a maximal subsequence s[i], . . . , s[i + k] such that s[i] is a leader
and none of the instructions s[i + j], i < j k is a leader.
Thus, a basic block is a sequence of consecutive instructions in which ow of
control enters at the beginning and leaves at the end.
Denition 17 Let s be a sequence of intermediate code instructions. The ow
graph of s is a directed graph where the nodes are the basic blocks in s and there
is an edge from B
1
to B
2
iff
The last instruction of B
1
is a (conditional or unconditional) jump instruc-
tion to the rst instruction of B
2
; or
B
2
follows B
1
in s and the last instruction of B
1
is not an unconditional
jump instruction.
As an example, consider the following fragment which computes the inner product
of two vectors.
1 {
2 product = 0;
3 for (i=0;i<20;i=i+1)
4 product = product + a[i]
*
b[i];
5 }
The intermediate code is shown below. It contains two basic blocks, B
1
and B
2
,
comprising the instructions 1-2 and 3-16, respectively. The ow graph is shown
in Figure 6.1
VUB-DINF/2009/2 95
1 product = 0
2 i = 1
3 t1 = 4
*
i
4 t2 = addr a
5 t3 = t2 -4
6 t4 = t3[t1]
7 t5 = addr b
8 t6 = t5 - 4
9 t7 = 4
*
i
10 t8 = t6[t7]
11 t9 = t4
*
t8
12 t10 = product + t9
13 product = t10
14 t11 = i+1
15 i = t11
16 if (i<20) goto 3
B
2
B
1
Figure 6.1: Flow graph of example code
6.2.1 DAG representation of basic blocks
A basic block can be represented as a directed acyclic graph (dag) which abstracts
away from the non-essential ordering of instructions in the block. On the other
hand, the dag will be constructed such that expressions are evaluated only once,
since they correspond to single nodes in the graph.
Pseudo-code for the algorithm to construct the dag for a basic block is shown
below.
Algorithm 8 [Constructing a DAG from a basic block]
VUB-DINF/2009/2 96
1 /
*
2
*
node(SYM_INFO
*
) and set_node(SYM_INFO
*
,NODE
*
) maintain a mapping:
3
*
4
*
node: SYM_INFO
*
-> NODE
*
5
*
6
*
which associates a (at most 1) node with a symbol (the node containing
7
*
the "current value" of the symbol).
8
*
/
9 extern NODE
*
node(SYM_INFO
*
); /
*
get node associated with symbol
*
/
10 extern void set_node(SYM_INFO
*
,NODE
*
); /
*
(re)set node associated with symbol
*
/
11
12 /
*
13
*
leaf(SYM_INFO
*
) and set_leaf(SYM_INFO
*
,NODE
*
) maintain a mapping:
14
*
15
*
leaf: SYM_INFO
*
-> LEAF
*
16
*
17
*
which associates a (at most 1) leaf node with a symbol (the node
18
*
containing the "initial value" of the symbol)
19
*
20
*
set_leaf() is called only once for a symbol, by the leaf
21
*
creating routine new_leaf(SYM_INFO
*
)
22
*
/
23 extern NODE
*
leaf(SYM_INFO
*
); /
*
get leaf associated with symbol
*
/
24 extern void set_leaf(SYM_INFO
*
,NODE
*
); /
*
set leaf associated with symbol
*
/
25
26 /
*
27
*
node creation functions. Each node has an opcode and a set
28
*
of symbols associated with it. A node may have up to 2 children.
29
*
/
30 extern new_0_node(SYM_INFO
*
);
31 extern new_1_node(OPCODE,SYM_INFO
*
);
32 extern new_2_node(OPCODE,SYM_INFO
*
, SYM_INFO
*
);
33 extern node_add_sym(SYM_INFO
*
,NODE
*
);
34 /
*
35
*
node finding function: returns node with given opcode and node arguments
36
*
/
37 extern NODE
*
match(OPCODE,NODE
*
,NODE
*
)
38
39 NODE
*
40 new_leaf(SYM_INFO
*
s) {
41 NODE
*
n;
42 n = new_0_node(s);
43 set_leaf(s,n);
44 set_node(s,n);
45 node_add_sym(s,n);
46 return n;
47 }
48
49 void
VUB-DINF/2009/2 97
50 basic_block_2_dag(INSTRUCTION bb[],int len) {
51 for (i=0;(i<len);++i) { /
*
for each instruction in the basic block
*
/
52 switch (type_of(bb[i]) {
53 case BINOP: /
*
A = B op C
*
/
54 nb = node(B);
55 if (!nb) /
*
B not yet in dag
*
/
56 nb = new_leaf(B);
57 nc = node(C);
58 if (!nc) /
*
C not yet in dag
*
/
59 nc = new_leaf(C);
60 /
*
find op-node wich children nb,nc
*
/
61 n = match(op,nb,nc);
62 if (!n)
63 n = new_2_node(op,nb,nc);
64 node_add_sym(A,n); set_node(A,n);
65 break;
66 case UNOP: /
*
A = op B
*
/
67 nb = node(B);
68 if (!nb) /
*
B not yet in dag
*
/
69 nb = new_leaf(B);
70 /
*
find op-node wich children nb,nc
*
/
71 n = match(op,nb,nc);
72 if (!n)
73 n = new_1_node(op,nb);
74 node_add_sym(A,n); set_node(A,n);
75 break;
76 case ZOP: /
*
A = B
*
/
77 n = node(B);
78 if (!n) /
*
B not yet in dag
*
/
79 n = new_leaf(B);
80 node_add_sym(A,n); set_node(A,n);
81 break;
82 case CJUMP: /
*
if B relop C GOTO L
*
/
83 /
*
this must be the last instruction in the block!
*
/
84 nb = node(B);
85 if (!nb) /
*
B not yet in dag
*
/
86 nb = new_leaf(B);
87 nc = node(C);
88 if (!nc) /
*
C not yet in dag
*
/
89 nc = new_leaf(C);
90 n = new_2_node(relop,nb,nc);
91 /
*
set_node(L,n);
*
/
92 break;
93 default:
94 break;
95 }
96 }
97 }
VUB-DINF/2009/2 98
2
The result of executing the algorithm on the basic block B
2
from Figure 6.1 is
shown in Figure 6.2.
4
*
t2
addr
t3
t4
t1,t7
t9
+ t10,product
addr
t5
- t6
t8
-
=[]
+
20
< L3
=[]
*
t11,i
product i a 1 b
Figure 6.2: Dag of example code
The DAG representation of a basic block provides useful information:
The range of the leaf map is the set of all symbols that are used in the basic
block.
The range of the node map contains the set of symbols that are available for
use outside the basic block. In the absence of more precise global informa-
tion, we must assume that they will be used and therefore we assume that
these variables are live
1
at the end of the basic block. However, temporary
variables created during intermediate code generation may be assumed not
to be live.
Note that a variable available at the end of a basic block is not necessarily live.
Section 6.3.5 (page 110), contains a global analysis that obtains a more precise
smaller estimate of which variables are really live.
1
See Denition 24, page 110, for a precise denition of live variable.
VUB-DINF/2009/2 99
6.2.2 Code simplication
Algorithm 9 [Code simplication in a basic block]
The DAG can also be used to perform code simplication on the basic block.
Basically, intermediate code is generated for each node in the block, subject to
the following rules and constraints (we assume that the code does not contain
array assignments or pointer manipulations):
Code for children should precede the code for the parent. The code for the
unique jump instruction should be generated last.
When performing the operation corresponding to a node n we prefer a live
target variable from the set node(n).
If node(n) contains several live variables, add additional simple assign-
ments of the form A = B to ensure that all live variables are assigned to.
If node(n) is empty, use a new temporary variable as the target.
Do not assign to a variable v if its current value is still needed (e.g. because
its leaf node still has an unevaluated parent).
2
The result of applying the algorithm sketched above is shown below. Note that,
compared with the original version, both the number of instructions and the num-
ber of temporary variables has decreased.
1
2
3 t1 = 4
*
i
4 t2 = addr a
5 t3 = t1 -4
6 t4 = t3[t1]
7 t5 = addr b
8 t6 = t5 - 4
9 t8 = t6[t1]
10 t9 = t4
*
t8
11 product = product + t9
12 i = i+1
13 if (i<20) goto 3
It should be noted that there are heuristics (see e.g Section 9.8 in [ASU86]) to
schedule node evaluation of a dag which tend to have a benecial effect on the
number of registers that will be needed when machine code is generated
2
.
2
For trees there is even an optimal, w.r.t. register usage, algorithm.
VUB-DINF/2009/2 100
6.2.3 Array and pointer assignments
The presence of array references and pointer manipulations complicates the code
simplication algorithm.
Consider the intermediate code
1 x = a[i]
2 a[j] = y
3 z = a[i]
The corresponding dag is shown in Figure 6.3.
=[] x,z
[]=
a i j y
Figure 6.3: Dag of code with array manipulation
A possible simplied instruction sequence would be
1 x = a[i]
2 z = x
3 a[j] = y
which is clearly not equivalent to the original (consider e.g. what would happen
if i == j but y ,= a[i].
The effect of an array assignment (a[x]=y) node should be that all nodes refer-
ring to an element of a should be killed in the sense that no further identiers can
be added to such nodes. Moreover, when generating simplied code, care should
be taken that all array reference nodes that existed before the creation of the ar-
ray assignment node should be evaluated before the assignment node. Similarly,
all array reference nodes created after the array assignment node should be eval-
uated after that node. One way to represent such extra constraints is by adding
extra edges to the dag from the old array reference nodes to the array assign-
ment node and from that node to the new array reference nodes, as shown in
Figure 6.4 (the extra constraining edges are shown as dashed arrows).
The effect of a pointer assignment
*
a = b is even more drastic. Since a can
point anywhere, the node corresponding to such an instruction kills every node in
the dag so far. Moreover, when generating simplied code, all old nodes will
VUB-DINF/2009/2 101
=[]
a i j y
[]=
z
x
=[]
Figure 6.4: Corrected dag of code with array manipulation
have to be evaluated before the pointer assignment and all new nodes after it.
Similarly, if we assume that a procedure can have arbitrary side effects, the effect
of a CALL instruction is similar to the one of a pointer assignment.
6.2.4 Algebraic identities
The dag creation algorithm can be made more sophisticated by taking into account
certain algebraic identities such as commutativity (a b = b a) and associativity.
Another possibility when processing a conditional jump instruction on a > b is to
check whether there is an identier x holding a b and to replace the condition
by x > 0 which is probably cheaper.
As an example, consider the intermediate code below.
1 t1 = b + c
2 a = t1
3 t2 = c + d
4 t3 = t2 + b
5 e = t3
The corresponding dags (with and without algebraic optimization) are shown in
Figure 6.5. In the example, the rst version of the dag is optimized by applying
associativity to recognize b +c as a common subexpression, eliminating the need
for the temporary variable t2.
6.3 Global ow graph information
In this section we discuss several kinds of information that can be derived from
the ow graph (and the instructions in it). In the next section, we will design code
improving transformations that rely on this information.
VUB-DINF/2009/2 102
+ t3,e
+
t1,a
+
t2
+
t1,a
+ t3,e
b
c d b c d
dag optimized dag
Figure 6.5: Dag using algebraic identities
Note that we assume that a ow graph corresponds to a single procedure. Also,
we will not consider problems caused by aliasing, where two variables refer to
the same location (aliasing may be caused by pointer manipulation and/or refer-
ence parameters in procedure calls). The impact of aliasing is briey discussed in
Section 6.5.
In the discussion belowwe use the following concepts related to owgraphs of ba-
sic blocks, which themselves consist of intermediate code instruction sequences.
A basic block B
, if
there is an edge from B to B
in the ow graph.
We assume the existence of an initial basic block which represents the entry
to the procedure.
There are n + 1 points in a basic block with n instructions: one between
each pair of consecutive instructions, one before the rst instruction and one
just after the last instruction.
Consider all points in all basic blocks of a ow graph. A path from a point
p
0
to a point p
n
is a sequence of points p
0
, p
1
, . . . , p
n1
, p
n
such that for
each 0 i < n, either
p
i
is the point immediately before an instruction s and p
i+1
is the point
immediately after s; or
p
i
is (after) the end of some block and p
i+1
is the point before the rst
instruction in a successor block.
Often, we will confuse an instruction s with the point immediately before it.
VUB-DINF/2009/2 103
6.3.1 Reaching denitions
Denition 18 A denition of a variable is an instruction that assigns, or may
assign, a value to it. In the rst case, we say that a denition is unambiguous,
otherwise its is called ambiguous. The set of all denitions of a variable a is
denoted by Def(a), while UDef(a) represents the set of unambiguous denitions
of a
A denition d reaches a point p if there is a path from the point immediately
following d to p such that d is not killed by an unambiguous denition of the same
variable along that path.
The phrase may assign is necessary since, e.g. a call to a procedure with x as
a formal (reference) parameter may or may not result in an assignment to x. A
similar reasoning holds for an assignment through a pointer or to an array element.
Note that Def(a) is easy to compute.
Thus, if a denition d for x reaches a point p, the value of x at p may be given by
the value assigned at d.
The following basic block properties will be useful later on:
Denition 19 Let B be a basic block.
GEN(B) = d B [ d is a denition that reaches the end of B
KILL(B) = d , B [ d Def(x) and B UDef(x) ,=
IN(B) = d [ d is a denition and d reaches the start of B
OUT(B) = d [ d is a denition and d reaches past the end of B
Observe that GEN(B), which depends only on B, is straightforward to construct
using a backward scan of B (after seeing an unambiguous denition for a variable
x, no more denitions for x should be added to GEN(B)). Similarly, given the
set DEF of all denitions in the graph, the set KILL(B) can easily be computed
by keeping only those denitions from DEF that dene a variable for which an
unambiguous denition in B exists.
It is easy to see that IN(B) and OUT(B) must satisfy the following data-ow
equations:
OUT(B) = (IN(B) KILL(B)) GEN(B)
IN(B) =
C<B
OUT(C)
VUB-DINF/2009/2 104
If there are n blocks in the graph, we have 2n equations with 2n unknowns
(GEN(B) and KILL(B) can be determined independently). Due to the possi-
ble presence of cycles in the ow graph, these equations usually do not have a
unique solution.
E.g. consider the graph in Figure 6.6.
B
Figure 6.6: Flow graph with cycle
Clearly, if IN(B) = I
0
and OUT(B) = O
0
form a solution and d is a denition
such that d , I
0
O
0
KILL(B), then IN(B) = I
0
d is also a solution. It
turns out that the smallest solution is the desired one
3
. It can be easily computed
using the following (smallest) xpoint computation.
Algorithm 10 [Computing denitions available at a basic block]
1 typedef set<Instruction
*
> ISET;
2 typedef .. BLOCK;
3
4 ISET in[BLOCK], out[BLOCK], kill[BLOCK], gen[BLOCK];
5
6 void
7 compute_in_out() {
8 for all b in BLOCK {
9 in[b] = emptyset;
10 out[b] = gen[b];
11 }
12
13 bool change = false;
14
15 do {
16 change = false;
17 foreach b in BLOCK {
3
The solutions are closed under intersection
VUB-DINF/2009/2 105
18 newin = set_union { out[c] | c < b };
19 change = change || (newin != in[b]);
20 in[b] = newin;
21 out[b] = (in[b] - kill[b]) set_union gen[b]
22 }
23 while change;
24 }
25 }
2
Denition 20 Let a be a variable and let u be a point in the ow graph. The set
of denitions of a that reach u is dened by
UD(a, u) = d Def(a) [ d reaches u
Such a set is often called a use-denition chain.
UD(a, u) can be easily computed using IN(B) where u is the basic block con-
taining u.
Algorithm 11 [Computing use-denition chains]
1 iset
2 defu(SYM_INFO
*
a,POINT u) {
3 BLOCK b = block_of(u);
4
5 if (Def(a) intersection B != emptyset)
6 /
*
return the singleton containing the last
7
*
definition in b of a before u
8
*
/
9 for (p=u; (p>0); --p)
10 if (p in UDEF(a))
11 return {p}
12 return Def(a) intersect in[b]
13 }
2
6.3.2 Reaching denitions using datalog
An alternative denition uses logic programming (in particular, datalog) to dene
reaching denitions, see [ALSU07], page 925.
VUB-DINF/2009/2 106
We will use a term B.N, N 0 to denote the point in block B us before the
N + 1th statement (in block B). The latter statement is said to be at point B.N.
We start with the following base predicates.
1 assign(B.N, X, E) % the statement following B.N has the
2 % form X = E, E an expression (with at most 2 operands)
3 use(B.N, X) % the statement at B.N uses the variable X
4 type(X,T) % type type of X is T
5 size(B, N) % block B has N statements
6 succ(B, C) % true iff block B has a successor block C
7 initial(B) :- not( succ(C,B) ). % initial block has no predecessors
We can then dene what it means for a statement to (ambiguously or unam-
bigously) dene a variable X (here we assume a strongly typed language without
casting).
1 % unambig_def(B.N, X) -- statement N in block B unambiguously defines variable X
2 unambig_def(B.N, X) :- assign(B.N, X, _).
3
4 % def(B.N, X) -- statement N in block B may define variable X
5 define(B.N, X) :- unambig_def(B.N, X).
6 define(B.N, X) :- assign(B.N,
*
P, _), type(X, T), type(P, T
*
).
Then the following program computes the reaching denitions.
1 % rd(B.N, C.M, X) -- definition of X at C.M reaches B.N
2 rd(B.N, B.N, X) :- define(B.N, X).
3 rd(B.N, C.M, X) :- rd(B.N-1, C.M, X), not(unambig_def(B.N-1, X)).
4 rd(B.0, C.M, X) :- rd(D.N, C.M, X), succ(D, B), size(D,N).
The rst rule simply states that a denition reaches itself while the second rule
indicates that if a denition reaches a point N 1 in a block B and the next
statement does not unambiguously (re)dene X, then the denition also reaches
point N (after the next statement) in B. The third rule says that a denition reaches
the beginning of a block B if it reaches the end of a predecessor D of B.
A use-denition predicate be dened as follows, where the use u is represented
by a point N in a block B, and d by its block C and point M):
ud(A, u(B.N), d(C.M)) :- rd( B.N, C.M, A), use(B.N, A).
6.3.3 Available expressions
Denition 21 An expression e = x+y
4
is available at a point p, denoted AVAIL(e, p)
if every path from the initial basic block to p evaluates z = x + y, and after the
4
We use + as an example operator. The same denition applies to other binary or unary
operators.
VUB-DINF/2009/2 107
last such evaluation prior to p, there are no subsequent denitions for x, y or z.
A block kills an expression x + y if it denes x or y and does not subsequently
(re)compute x + y. A block generates an expression x + y is if it unambiguously
evaluates x + y and does not subsequently redene x or y.
In order to be able to determine the availability of an expression at some point, we
dene
Denition 22 Let B be a basic block.
GEN
e
(B) = e [ B generates e
KILL
e
(B) = e [ B kills e
IN
e
(B) = e [ e is available at the start of B
OUT
e
(B) = e [ e is available past the end of B
Observe that GEN
e
(B), which depends only on B, is straightforward to construct
using a forward scan of B, keeping track of available expressions by appropriately
processing denitions. E.g. an instruction z = x + y makes x + y available but
kills any available expressions involving z (note that z = x is possible). The set
KILL
e
(B) consists of all expressions y +z such that either y or z is dened in the
block and y + z is not generated by the block.
It is easy to see that IN
e
(B) and OUT
e
(B) must satisfy the following data-ow
equations:
OUT
e
(B) = (IN(B) KILL
e
(B)) GEN
e
(B)
IN
e
(B) =
C<B
OUT
e
(C) if B is not initial
if B is initial
Note the similarity of these equations with the ones for reaching denitions. The
main difference is the use of instead of in the equation for IN
e
(B) ( is
necessary since e must be available on all paths into B in order to be sure that e is
available at the start of B).
Consider the ow graph in Figure 6.7 and assume that neither x nor y are dened
in block B after d which is itself the nal denition of z in block B. Assume
furthermore that block C does not contain any denitions for x, y or z. Hence
x + y should be available at the beginning of block C, i.e. x + y IN
e
(C),
which would not be found if the minimal solution for the above equations were
computed.
VUB-DINF/2009/2 108
d: z=x + y
B
C
Figure 6.7: Example ow graph for available expressions
Since, clearly, the solutions for IN
e
(C) should be closed under union, it turns out
that the intended solution is the maximal one. Hence, in the following xpoint
procedure, we compute the greatest xpoint.
Algorithm 12 [Computing expressions available at a basic block]
1 typedef .. EXPRESSION;
2 typedef set<EXPRESSION> ESET;
3 typedef .. BLOCK;
4
5 extern BLOCK
*
initial_block();
6 extern ESET all_expressions();
7
8 ESET ine[BLOCK], oute[BLOCK], kille[BLOCK], gene[BLOCK];
9
10 void
11 compute_avail_exp() {
12 BLOCK b1 = initial_block();
13
14 /
*
initialization
*
/
15
16 ine[b1] = empyset; /
*
never changes
*
/
17 out[b1] = gene[b1]; /
*
never changes
*
/
18
19 for all b in (BLOCK - {b1}) {
20 oute[b] = all_expressions() - kille[b]
21 }
22
23 bool change = false;
VUB-DINF/2009/2 109
24
25 do {
26 change = false;
27 for all b in (BLOCK - {b1}) {
28 ine[b] = intersection { oute[c] | c < b }
29 oldout = oute[b];
30 oute[b] = gene[b] set_union (ine[b] - kille[b]);
31 change = change || (oute[b]!=oldout);
32 }
33 }
34 while (change);
35
36 }
2
Note that, based on IN
e
(B), it is easy to determine AVAIL(e = x + y, p), for
p B.
Algorithm 13 [Computing expression availability at a point]
Let p B be a point where we want to compute AVAIL(e, p).
Check whether e IN
e
(B).
If so, check whether x or y are redened before p in B. If not, e is available;
else not.
If e , IN
e
(B), check whether e is generated before p in B. If so, e is
available; else not.
2
6.3.4 Available expressions using datalog
In datalog, the formulation is slightly more involved than might be expected be-
cause the usual datalog semantics computes minimal solutions. The approach is
to dene a predicate unavail_exp/2 asserting that an expression is not available
at some point. The availability of an expression at a point then hold is unavail
does not hold at that point.
1 define_exp(B.N, X+Y) :- assign(B.N, _, X+Y).
2 % a possibled definition of any operand kills the expression
3 kill_exp(B.N, X+Y) :- define(B.N, X, _).
4 kill_exp(B.N, X+Y) :- define(B.N, Y, _).
VUB-DINF/2009/2 110
5
6 unavail_exp(B.N, X+Y) :- kill_exp(B.N
7 unavail_exp(B.N+1, X+Y) :-
8 unavail_exp(B.N, X+Y), not(define_exp(B.N+1, X+Y)).
9 % if X+Y is unavaible at the end of a predecessor of B,
10 % it is also unavailable at the beginning of B
11 unavail_exp(B.0, X+Y) :- succ(C, B), size(C,N), unavail_exp(C.N, X+Y).
12 % at the start of the initial block, all expressions are unavailable
13 unavail_exp(B.0, X+Y) :- initial(B).
14 % an expression is available if it is not unavailable
15 avail(B.N, X+Y) :- not(unavail_exp(B.N, X+Y)).
6.3.5 Live variable analysis
Denition 23 A variable x is used at instruction s if its value may be required by
s.
Denition 24 Let x be a variable and let p be a point in the ow graph. We say
that x is live at p is there is some path from p on which the value of x at p can be
used. If x is not live at p, we say that it is dead at p.
In datalog this becomes:
1 live(B.N, X) :- assign(B.N, _, X + Y).
2 live(B.N, X) :- assign(B.N, _, Y + X).
3 live(B.N-1, X) :- live(B.N, X),
4 not(unambig_def(B.N-1, X).
5 live(B.N, X) :- size(B, N), succ(B, C), live(C.0, X).
In order to be able to determine whether a variable is live at some point, we dene
Denition 25 Let B be a basic block.
KILL
live
(B) = x [ x is unambiguously dened before used in B
GEN
live
(B) = x [ x may be used in B before it is unambiguously dened in B
IN
live
(B) = x [ x is live at the start of B
OUT
live
(B) = x [ x is live past the end of B
VUB-DINF/2009/2 111
Note that, in the denition of KILL
live
(B), it is not necessary that x is actually
used in B, as long as there is an unambiguous denition of x before any use. Sim-
ilarly, in the denition of IN
live
(B), it may well be that x IN
live
(B) although x
is not (unambiguously) dened in B.
Observe that both KILL
live
(B) and GEN
live
(B), which depend only on B, are
straightforward to construct.
To compute IN
live
(B) and OUT
live
(B), we note that they satisfy the following
data-ow equations:
IN
live
(B) = GEN
live
(B) (OUT
live
(B) KILL
live
(B))
OUT
live
(B) =
B<C
IN
live
(C)
Again we will use a xpoint computation to determine the smallest solution of the
above equations. Note that intuitively, the algorithm works backwards in the
graph, as can be seen from the use of successors instead of predecessors in the
equation for OUT
live
(B).
Algorithm 14 [Computing live variables for a basic block]
1 typedef set<SYM_INFO
*
> VSET;
2 typedef .. BLOCK;
3
4 VSET in_live[BLOCK], out_live[BLOCK], use[BLOCK], gen_live[BLOCK];
5
6 void
7 compute_live()
8 {
9 for all b in BLOCK
10 in_live[b] = emptyset;
11
12 bool change = true;
13
14 while (change) {
15 change = false;
16 foreach b in BLOCK {
17 oldin = in_live[b];
18 out_live[b] = union { in[s] | b < s };
19 in_live[b] = use[b] union (out_live[b] - gen_live[b]);
20 change = change || (oldin!=in_live[b]);
21 }
22 }
23 }
VUB-DINF/2009/2 112
2
Note that the information from OUT
live
(B) can be protably used to rene Al-
gorithm 9.
6.3.6 Denition-use chaining
Denition 26 The denition-use chain of a denition d is dened as
DU(d, x) = s [ s uses x and there is a path from d to s that does not redene x
The following sets, dened for basic blocks, facilitate the computation of DU(d, x).
Denition 27 Let B be a basic block.
KILL
use
(B) = (s, x) [ s , B uses x and B denes x
GEN
use
(B) = (s, x) [ s B uses x and x is not dened prior to s in B
IN
use
(B) = (s, x) [ s uses x and s reachable from the beginning of B
OUT
use
(B) = (s, x) [ s , B uses x and s reachable from the end of B
Here, reachable means without any intervening denitions of x.
Observe that both KILL
use
(B) and GEN
use
(B) are straightforward to construct.
To compute IN
use
(B) and OUT
use
(B), we note that they satisfy the following
data-ow equations:
IN
use
(B) = GEN
use
(B) (OUT
use
(B) KILL
use
(B))
OUT
use
(B) =
B<C
IN
use
(C)
Since the above equations are isomorphic to the ones in Section 6.3.5, an algo-
rithm similar to Algorithm 14 can be used to compute OUT
use
(B). DU(d, x) can
then be computed by.
DU(d, x) =
OUT
use
(B) (s, x) [ s B uses x
if B does not contain a denition of x after d
(s, x) [ s B uses x and s comes before d
if d
.
No paths from s to s
several times,
contains denitions of y.
In datalog:
1 % copy(B.N, X, Y) -- X is copy of Y at B.N
2 copy(B.N, X, Y) :- assign(B.N, X, Y).
3 copy(B.N, X, Y) :- copy(B.N-1, X, Y),
4 not( def(B.N-1, X) ), not( def(B.N-1, Y) ).
5 copy(B.0, X, Y) :- not( initial(B) ),
6 not (
7 succ(C, B), size(C, M), not( copy(C.M, X, Y) )
8 ).
To solve the latter question, we solve yet another data-owproblemwhere IN
copy
(B)
(OUT
copy
(B)) is the set of copies s : x = y such that every path from the initial
node to the beginning (end) of B contains s, and subsequent to the last occurrence
of s, there are no denitions of y. We also consider GEN
copy
(B), the set of copies
s:x = y generated by B where s B and there are no subsequent denitions of
x or y within B. KILL
copy
(B) contains the set of copies s:x = y where either x
or y is dened in B and s , B. Note that IN
copy
(B) can contain only one copy
instruction with x on the left since, otherwise, both instructions would kill each
other.
VUB-DINF/2009/2 115
Then the following equations hold:
OUT
copy
(B) = GEN
copy
(B) (IN
copy
(B) KILL
copy
(B))
IN
copy
(B) =
C<B
OUT
copy
(C) if B is not initial
if B is initial
Note that these equations are isomorphic to the ones used for Algorithm 12 and
therefore we can use a similar algorithm to solve them.
Given IN
copy
(B), i.e. the set of copies x = y that reach B along every path, with
no denition of x or y following the last occurrence of x = y on the path, we can
propagate copies as follows.
Algorithm 16 [Copy propagation]
For each copy instruction s:x = y do the following:
1. Determine DU(s, x), i.e. the set of uses s
DU(s, x), s IN
copy
(B
s
)
6
, i.e. s reaches
B
s
on every path and, moreover, no denitions of x or y occur prior to s
in B.
3. If the previous step succeeds, remove s and replace x by y in each s
DU(s, x).
2
Applying Algorithm 16 on the code of Figure 6.9 yields
1 u = x + y
2 b = u
*
z
3 ...
4 d = u
*
z
Now, running Algorithm 15 again will result in
1 u = x + y
2 v = u
*
z
3 b = v
4 ...
5 d = v
6
B
s
is the basic block containing s
.
VUB-DINF/2009/2 116
6.4.3 Constant folding and elimination of useless variables
Another transformation concerns the propagation of constants. As an example,
consider the code
1 t0 = 33
2 t1 = 3
3 t2 = t0 + t1
4 t3 = t2 - 35
5 x = t3;
Algorithm 17 [Constant folding]
Call a denition s:x = a + b
7
constant if all its operands a and b are constant.
We can then replace s by x = c where c = a + b is a new constant.
For each constant denition s and for each s
) = s,
i.e. s is the only x-denition reaching s
, we can replace x in s
by c, possi-
bly making s
passes
through b.
A back edge is an edge n b such that b dominates n.
The (natural) loop L of a back edge n h consists of h and all basic
blocks x such that there is a path from x to n not going through h; h is
called the header of the loop.
An inner loop is a loop that contains no other loops.
An exit node of a loop L is a block e L that has a successor outside L.
B
1
B
2
B
3
B
6
B
5
B
4
Figure 6.10: Flow graph with loops
In Figure 6.10, there are three loops: L
0
= B
2
, L
1
= B
3
, L
2
= B
2
, B
3
, B
4
, B
5
.
B
2
is the header of L
2
, while B
4
is L
2
s single exit node.
One crude way to compute the dominators of a block b is to take the intersection
of all acyclic paths from the initial node to b.
A smarter algorithm relies on the fact that
dom(b) = b
c<b
dom(c)
i.e. a node that dominates all predecessors of b also dominates b.
Algorithm 18 [Computing dominators]
VUB-DINF/2009/2 118
1 typedef set<BLOCK
*
> BLOCKS;
2
3 BLOCK
*
b0; /
*
initial basic block
*
/
4 set<BLOCK
*
> B; /
*
set of all blocks
*
/
5
6 void
7 compute_dominates()
8 {
9 /
*
initialize
*
/
10
11 dom(b0) = { b0 };
12 for b in (B - {b0})
13 dom(b) = B;
14
15 /
*
compute fixpoint
*
/
16
17 do
18 {
19 change = false;
20 for b in (B-{b0})
21 {
22 old_dom = dom(b);
23 dom(b) = {b} union intersection { dom(c) | c < b }
24 change = change || (dom(b)!=old_dom);
25 }
26 }
27 while (change)
28 }
2
The table below shows the application of Algorithm 19 to the ow graph of Fig-
ure 6.10.
block 1 2 3 4 5 6
predecessors 1,2,5 2,3 3 4 4
dominators initialize 1 1,2,3,4,5,6 1,2,3,4,5,6 1,2,3,4,5,6 1,2,3,4,5,6 1,2,3,4,5,6
dominators iteration 1 1 1,2 1,2,3 1,2,3,4 1,2,3,4,5 1,2,3,4,6
dominators iteration 2 1 1,2 1,2,3 1,2,3,4 1,2,3,4,5 1,2,3,4,6
The following algorithm can be used to compute the natural loop corresponding
to a back edge. It corresponds to a backward scan from the header.
Algorithm 19 [Computing the natural loop of a back edge]
VUB-DINF/2009/2 119
1 type set<BLOCK
*
> BLOCKS;
2
3 typedef struct {
4 BLOCKS nodes;
5 BLOCK
*
header;
6 } LOOP;
7
8 BLOCKS
9 predecessors(BLOCK
*
b); /
*
return predecessors of b in flow graph
*
/
10
11 LOOP
12 natural_loop(BLOCK
*
src,BLOCK
*
trg)
13 {
14 /
*
compute natural loop corresponding to back edge src-->trg
*
/
15
16 BLOCK
*
stack[];
17 BLOCK
*
tos = stack;
18 LOOP loop = { { trg }, trg }; /
*
trg is header
*
/
19
20 if (trg!=src) {
21 loop.nodes = loop.nodes union { src }
22 stack[tos++] = src; /
*
push src
*
/
23 }
24
25 while (tos!=stack) { /
*
stack not empty
*
/
26 BLOCK
*
b = stack[--tos]; /
*
pop b
*
/
27 BLOCKS P = predecessors(b);
28
29 for each p in P
30 if (!(p in loop)) {
31 loop.nodes = loop.nodes union {p};
32 stack[tos++] = p;
33 }
34
35 }
36 }
2
Applying Algorithm19 to the back edge B
5
B
2
in the owgraph of Figure 6.10
is illustrated in the table below.
faze loop.nodes stack
initialize 2
adding src 2, 5 5
iteration 1 2, 5, 4 4
iteration 2 2, 5, 4, 3 3
iteration 3 2, 5, 4, 3
VUB-DINF/2009/2 120
6.4.5 Moving loop invariants
A loop invariant is a instruction of the form s:x = y + z
8
such that x does not
change as long as control stays in the loop. Such instructions can, under certain
conditions, be moved outside of the loop so that the value x is computed only
once.
Example 20 Consider a C function to compute the scalar product of two vectors
(represented as arrays).
1 int
2 innerproduct(int a[]; int b[], int s) {
3 // s is size of arrays
4 int r = 0;
5 int i = 0;
6 while (i<s) {
7 r = r + a[i]
*
b[i];
8 i = i+1;
9 }
10 return r;
11 }
The ow graph, after code simplication, is shown in Figure 6.11 on page 121.
Note that the size of an int is assumed to be 4 bytes, as witnessed by the offset
computation t1 = 4
*
i.
Applying Algorithm 19 on the graph of Figure 6.11 yields that, among others, C
dominates A, B and D and that, consequently, D C is a back edge. It then
follows from Algorithm 19 that L = D, C is an inner loop with exit node C.
Algorithm 20 [Computing loop invariants]
Given a loop L, we nd the list of loop invariants from L as follows (note that
the order of the list is important, as it reects the order of the instructions after
having been moved):
1. Mark invariant (and append to the list of invariants) all instructions
s:x = y + z such that each operand y (or z) is either constant or is such
that UD(y, s) L = , i.e. all its reaching denitions are outside L.
2. Repeat the following step until no more instructions are marked.
3. Mark (and append to the list) invariant all instructions s:x = y + z such
that each operand y (or z) is
8
Here + stands for any operator.
VUB-DINF/2009/2 121
r = 0
i = 0
E
C
B
A
D t1 = 4 * i
if i<s goto B
goto A
return r
t2 = &a
t3 = t2[t1]
t4 = &b
t5 = t4[t1]
t6 = t3 * t5
r = r + t6
i = i+1
goto C
Figure 6.11: Flow graph of Example 20
constant; or
all its reaching denitions are outside L; or
UD(y, s) = q where q L is marked invariant; i.e. the only reach-
ing denition of y is an invariant in the loop.
2
Applying Algorithm 20 for Example 20 yields two loop invariants: t2 = &a and
t4 = &b, both in block D.
Having found the invariants in a loop, we can move some of them outside the
loop. They will be put in a new node, called a preheader which has as its single
successor the header of the loop. The ow graph is further updated such that all
edges that pointed from outside the loop to the header, will be made to point to
the new preheader node.
Algorithm 21 [Moving invariants]
Let I be the list of invariants of the loop L.
1. For each s:x = y + z from I, check that:
VUB-DINF/2009/2 122
(a) s is in a block b that dominates all exits of L or x , IN
live
(e) for any
successor e , L of an exit block of L; and
(b) x is not dened anywhere else in L; and
(c) For each use s
of x in L, UD(x, s
G
, 36
=
G
, 36
[w[, 24
pref
l
(w), 24
M
, 26, 29
M
, 26, 29
, 26
-closure, 30
-move, 28
$1, 76
$$, 76
GEN
e
(B), 107
KILL
use
(B), 112
KILL
live
(B), 110
GEN(B), 103
IN
e
(B), 107
IN
use
(B), 112
IN
live
(B), 110
IN(B), 103
KILL
e
(B), 107
KILL(B), 103
OUT
e
(B), 107
OUT
use
(B), 112
OUT
live
(B), 110
OUT(B), 103
UD(a, u), 105
GEN
use
(B), 112
GEN
live
(B), 110
absolute code, 130
abstract syntax trees, 76
action table, 53
activation record, 132
algorithm optimization, 93
aliases, 126, 129
aliasing, 102
alphabet, 24
assembler, 7
assembler source, 130
attribute, 65
attribute grammar, 65
attributes
inherited, 66
synthesized, 66
available expression, 106
awk, 8
back edge, 117
back end, 74
backpatching, 83
basic block, 94
basic induction variable, 123
bison
attributes, 68
cfg
see context-free grammar, 35
cfront, 7
code simplication, 99
code-generator generator, 78
compiler, 6
computation by a DFA, 26
conguration, 26
conict
reduce-reduce, 57
shift-reduce, 57
context-free grammar, 14, 35
182
VUB-DINF/2009/2 183
derives, 36
generated language, 36
left recursive, 41
left sentential form, 38
leftmost derivation, 38
LL(1), 43
LL(k), 44
LR(1), 59
right sentential form, 50
rightmost derivation, 50
context-free language, 36
cross-compiler, 7
dag, 95
dead variable, 110
denition-use chain, 112
dependency graph, 67
derivation
leftmost, 38
rightmost, 50
deterministic nite automaton, 26
DFA, 26
computation, 26
computation step, 26
conguration, 26
directed acyclic graph, 95
dominate, 117
empty string, 24
exit node, 117
expression
available, 106
family of an induction variable, 123
nite automaton
deterministic, 26
nondeterministic, 28
rst, 45
ow graph, 94
follow, 45
formal language, 7, 24
front end, 74
goto table, 53
gprof, 116
grammar, 10
attribute, 65
handle, 50
header of a loop, 117
immediate left recursion, 41
induction variable, 123
basic, 123
family, 123
inherently ambiguous languages, 38
inherited attributes, 66
initial basic block, 102
inner loop, 117
interpreter, 8
item
LR(1), 54
Java virtual machine, 8
JVM, 8
language
formal, 7, 24
little, 8
regular, 24
source, 6
target, 6
languages
inherently ambiguous, 38
leader, 94
left recursion, 41
immediate, 41
left sentential form, 38
leftmost derivation, 38
length of string, 24
lexical analyze, 12
lisp, 8
little language, 8
live variable, 110
LL(1) grammar, 43
VUB-DINF/2009/2 184
LL(1) parse table construction, 45
LL(k) grammars, 44
loop
header, 117
inner, 117
invariant, 120
LR(1) grammar, 59
LR(1) item, 54
lvalue, 89
Micro, 9
Minic, 72
natural loop, 117
NFA, 28
nondeterministic nite automaton, 28
nonterminal symbol, 35
parse tree, 13, 37
parser, 13
predictive, 44
parsing
predictive, 38
top-down, 38
path between points, 102
phrase, 50
simple, 50
point, 102
predictive parser, 44
operation, 45
predictive parsing, 38
prex, 24
preheader, 121
product, 24
production, 35
prolog, 8
reduce operation, 51
reduce-reduce conict, 57
register allocation, 17
regular expressions, 24
regular language, 24
relocatable code, 130
retargeting, 74
right sentential form, 50
rightmost derivation, 50
scanner, 12
scope, 69
semantic action, 15, 65
semantics
static, 71
shift operation, 51
shift-reduce conict, 57
simple phrase, 50
simple postx, 76
Smalltalk, 8
source language, 6
SQL, 8
start symbol, 35
state, 26, 28
nal, 26
initial, 26
static semantics, 71
strength reduction, 124
successor, 102
symbol table, 12
syntax, 7
syntax-directed translation, 15, 67
synthesized attributes, 66
target language, 6
terminal symbol, 35
three-address code, 15
instructions, 78
token, 12
top-down parsing, 38
topological sort, 67
transition function, 26
type rules, 72
type system, 71
use-denition chain, 105
used variable, 110
VUB-DINF/2009/2 185
viable prex, 50
x86, 10
yacc
attributes, 68
yield, 14
List of Figures
1.1 A source text in the C language . . . . . . . . . . . . . . . . . . . 6
1.2 The syntax of the Micro language . . . . . . . . . . . . . . . . . 9
1.3 A Micro program . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4 X86 assembly code generated for the program in Figure 1.3 . . . . 11
1.5 Result of lexical analysis of program in Figure 1.3 . . . . . . . . . 13
1.6 Parse tree of program in Figure 1.3 . . . . . . . . . . . . . . . . . 14
1.7 three-address code corresponding to the program of Figure 1.3 . . 16
1.8 Optimized three-address code corresponding to the program of
Figure 1.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1 A declaration for TOKEN and lex() . . . . . . . . . . . . . . . . 18
2.2 The transition diagram for lex() . . . . . . . . . . . . . . . . . . . 23
2.3 A DFA for NAME . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4 DFA implementation . . . . . . . . . . . . . . . . . . . . . . . . 28
2.5 M
1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.6 M
, M
and M
a
. . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.7 M
r
1
+r
2
, M
r
1
r
2
and M
r
1
. . . . . . . . . . . . . . . . . . . . . . . 32
2.8 A generated scanner . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.1 Parse trees in the ambiguous context-free grammar from Example 5 37
3.2 A simple top-down parse . . . . . . . . . . . . . . . . . . . . . . 39
3.3 Eliminating immediate left recursion . . . . . . . . . . . . . . . . 42
3.4 The prediction problem . . . . . . . . . . . . . . . . . . . . . . . 43
3.5 A predictive parser . . . . . . . . . . . . . . . . . . . . . . . . . 44
186
VUB-DINF/2009/2 187
3.6 Predictive parser operation . . . . . . . . . . . . . . . . . . . . . 46
3.7 Naive shift-reduce parsing procedure . . . . . . . . . . . . . . . . 53
3.8 A shift-reduce parser . . . . . . . . . . . . . . . . . . . . . . . . 54
3.9 Shift-reduce parser algorithm . . . . . . . . . . . . . . . . . . . . 55
3.10 A (partial) rightmost derivation . . . . . . . . . . . . . . . . . . . 56
3.11 States of the viable prex DFA of G
3
. . . . . . . . . . . . . . . . 58
3.12 Transition function of the viable prex DFA of G
3
. . . . . . . . . 59
3.13 LR(1) tables for G
3
. . . . . . . . . . . . . . . . . . . . . . . . . 60
4.1 Computing synthesized attribute values . . . . . . . . . . . . . . 66
4.2 Computing inherited attribute values . . . . . . . . . . . . . . . . 68
4.3 A simple symbol table using a string pool and hashing . . . . . . 70
4.4 A stacked symbol table . . . . . . . . . . . . . . . . . . . . . . . 71
5.1 Abstract syntax tree corresponding to the program in Figure 1.3 . 77
5.2 Tree-rewriting rules . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.3 Three-address code instructions . . . . . . . . . . . . . . . . . . 79
5.4 Conditional control ow statements . . . . . . . . . . . . . . . . 83
5.5 Exit sets after and . . . . . . . . . . . . . . . . . . . . . . . . . 84
6.1 Flow graph of example code . . . . . . . . . . . . . . . . . . . . 95
6.2 Dag of example code . . . . . . . . . . . . . . . . . . . . . . . . 98
6.3 Dag of code with array manipulation . . . . . . . . . . . . . . . . 100
6.4 Corrected dag of code with array manipulation . . . . . . . . . . 101
6.5 Dag using algebraic identities . . . . . . . . . . . . . . . . . . . . 102
6.6 Flow graph with cycle . . . . . . . . . . . . . . . . . . . . . . . . 104
6.7 Example ow graph for available expressions . . . . . . . . . . . 108
6.8 Code before rst subexpression elimination . . . . . . . . . . . . 113
6.9 Code after rst subexpression elimination . . . . . . . . . . . . . 114
6.10 Flow graph with loops . . . . . . . . . . . . . . . . . . . . . . . 117
6.11 Flow graph of Example 20 . . . . . . . . . . . . . . . . . . . . . 121
6.12 Flow graph of Example 20 after moving loop invariants . . . . . . 122
VUB-DINF/2009/2 188
6.13 Flow graph of Example 20 after strength reduction . . . . . . . . 125
6.14 Flow graph of Figure 6.13 after copy propagation and constant
folding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
6.15 optimized ow graph for Example 20 . . . . . . . . . . . . . . . 127
7.1 Memory layout in C . . . . . . . . . . . . . . . . . . . . . . . . . 131
7.2 Activation records . . . . . . . . . . . . . . . . . . . . . . . . . . 132
7.3 Multiplication on IBM/370 . . . . . . . . . . . . . . . . . . . . . 135
List of Tables
2.1 Regular expressions describing Micro tokens . . . . . . . . . . . 25
A.1 Some x86 Flags . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
A.2 Data Transfer Instructions . . . . . . . . . . . . . . . . . . . . . 141
A.3 Integer Arithmetic Instructions . . . . . . . . . . . . . . . . . . . 142
A.4 Linux System Calls . . . . . . . . . . . . . . . . . . . . . . . . . 145
A.5 Logical Operations . . . . . . . . . . . . . . . . . . . . . . . . . 145
A.6 Control Flow Instructions . . . . . . . . . . . . . . . . . . . . . . 146
189
Bibliography
[ALSU07] Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman.
Compilers: Principles, techniques and tools, second edition. Pear-
son,Addison Wesley, 2007.
[ASU86] Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers: Prin-
ciples, techniques and tools. Addison Wesley, 1986.
[Bar03] Jonathan Bartlett. Programming from the Ground Up. http://
www.bartlettpublishing.com/ and http://savannah.
nongnu.org/projects/pgubook/, 2003.
[FL91] Charles N. Fischer and Richard J. LeBlanc. Crafting a compiler with
C. Addison-Wesley, 1991.
[HU69] John E. Hopcroft and Jeffrey D. Ullman. Formal languages and their
relation to automata. Addison Wesley, 1969.
[Sal73] A. Salomaa. Formal languages. ACM Monograph Series. Academic
Press, 1973.
190