Escolar Documentos
Profissional Documentos
Cultura Documentos
Introduction
o Lecture 1 (printable)
Lexical Analysis
o Lecture 2 (printable)
o Lecture 3 (printable)
o Lecture 4 (printable)
o Lecture 5 (printable)
o Lecture 6 (printable)
o Lecture 7 (printable)
Syntax Analysis
o Lecture 8 (printable)
o Lecture 9 (printable)
o Lecture 10 (printable)
o Lecture 11 (printable)
o Lecture 12 (printable)
o Lecture 13 (printable)
Semantic Analysis
o Lecture 14 (printable)
o Lecture 15 (printable)
o Lecture 16 (printable)
o Lecture 17 (printable)
Intermediate Code
Generation
o Lecture 18 (printable)
o Lecture 19 (printable)
o Lecture 20 (printable)
o Lecture 21 (printable)
o Lecture 22 (printable)
Final Code Generation
o Lecture 23 (printable)
o Lecture 24 (printable)
o Lecture 25 (printable)
o Lecture 26 (printable)
Labs and lectures will discuss all of these, but if you do not
know them already, the sooner you go learn them, the better.
C and "make".
If you are not expert with these yet, you will be a lot closer
by the time you pass this class.
lex and yacc
These are compiler-writers tools, but they are useful for
other kinds of applications, almost anything with a complex
file format to read in can benefit from them.
gdb
If you do not know a source-level debugger well, start
learning. You will need one to survive this class.
e-mail
Regularly e-mailing your instructor is a crucial part of class
participation. If you aren't asking questions, you aren't
doing your job as a student.
web
This is where you get your lecture notes, homeworks, and
labs, and turnin all your work.
virtual environment
We have a 3D video game / chat tool available that can
help us handle questions when one of us is not on campus.
Compilers - What Are They and What Kinds of Compilers
are Out There?
The purpose of a compiler is: to translate a program in some
language (the source language) into a lower-level language
(the target language). The compiler itself is written in some
language, called the implementation language. To write a
compiler you have to be very good at programming in the
IDENTIFIER=[a-zA-Z][a-zA-Z0-9]*
Lexical Analyzers can be written by hand, or implemented
automatically using finite automata.
What is a "token" ?
preprocessor
Program that processes the source code before the compiler
sees it. Usually, it implements macro expansion, but it can
do much more.
editor
Editors may operate on plain text, or they may be wired
into the rest of the compiler, highlighting syntax errors as
you go, or allowing you to insert or delete entire syntax
constructs at a time.
debugger
Program to help you see what's going on when your
program runs. Can print the values of variables, show what
procedure called what procedure to get where you are, run
up to a particular line, run until a particular variable gets a
special value, etc.
profiler
Program to help you see where your program is spending
its time, so you can tell where you need to speed it up.
Auxiliary data structures
You were presented with the phases of the compiler, from
lexical and syntax analysis, through semantic analysis, and
intermediate and final code generation. Each phase has an input
and an output to the next phase. But there are a few data
structures we will build that survive across multiple phases: the
literal table, the symbol table, and the error handler.
lexeme table
a table that stores lexeme values, such as strings and
variable names, that may occur in many places. Only one
putc(c, ofp);
}
Warning: while using and adapting the above code is fair game
in this class, the yylex() function is very different than the
filecopy() function! It takes no parameters! It returns an integer
every time it finds a token! So if you "borrow" from this
example, delete filecopy() and write yylex() from scratch.
Multiple students have fallen into this trap before you.
A Brief Introduction to Make
It is not a good idea to write a large program like a compiler as a
single source file. For one thing, every time you make a small
change, you would need to recompile the whole program, which
will end up being many thousands of lines. For another thing,
parts of your compiler may be generated by "compiler
construction tools" which will write separate files. In any case,
this class will require you to use multiple source files, compiled
separately, and linked together to form your executable program.
This would be a pain, except we have "make" which takes care
of it for us. Make uses an input file named "makefile", which
stores in ASCII text form a collection of rules for how to build a
program from its pieces. Each rule shows how to build a file
from its source files, or dependencies. For example, to compile a
file under C:
foo.o : foo.c
gcc -c foo.c
The first line says to build foo.o you need foo.c, and the second
line, which must being with a tab, gave a command-line to
operators
BASIC
the characters
themselves
C
For operators that are regular
expression operators we need
mark them with double quotes or
backslashes to indicate you mean
the character, not the regular
expression operator. Note several
operators have a common prefix.
The lexical analyzer needs to look
ahead to tell whether an = is an
assignment, or is followed by
[a-zA-Z]
[0-9]
{letter}({letter}|{digit})*
.
The dot operator matches any one character except
newline: [^\n]
r*
match r 0 or more times.
r+
match r 1 or more times.
r?
match r 0 or 1 time.
r{m,n}
match r between m and n times.
r1r2
concatenation. match r1 followed by r2
r1|r2
alternation. match r1 or r2
(r)
parentheses specify precedence but do not match anything
r1/r2
lookahead. match r1 when r2 follows, without consuming r2
^r
match r only when it occurs at the beginning of a line
r$
match r only when it occurs at the end of a line
lecture #4 began here
Announcements
Next homework I promise: I will ask the TA to run your
program with a nonexistent file as a command-line argument!
Lexical Attributes and Token Objects
Besides the token's category, the rest of the compiler may need
several pieces of information about a token in order to perform
semantic analysis, code generation, and error handling. These
are stored in an object instance of class Token, or in C, a struct.
The fields are generally something like:
struct token {
int category;
char *text;
int linenumber;
int column;
char *filename;
union literal value;
}
The union literal will hold computed values of integers, real
numbers, and strings. In your homework assignment, I am
requiring you to compute column #'s; not all compilers require
them, but they are easy. Also: in our compiler project we are not
worrying about optimizing our use of memory, so am not
requiring you to use a union.
Flex Manpage Examplefest
To read a UNIX "man page", or manual page, you type
"man command" where command is the UNIX program or
library function you need information on. Read the man page for
man to learn more advanced uses ("man man").
It turns out the flex man page is intended to be pretty complete,
enough so that we can draw our examples from it. Perhaps what
you should figure out from these examples is that flex is
num_lines, num_chars );
}
Toy compiler example
atoi( yytext ) );
}
{DIGIT}+"."{DIGIT}*
{
printf( "A float: %s (%g)\n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function
{
printf( "A keyword: %s\n", yytext );
}
{ID}
yytext );
"{"[^}\n]*"}"
/* eat up one-line
comments */
[ \t\n]+
.
%s\n", yytext );
/* eat up whitespace */
printf( "Unrecognized character:
%%
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* skip over program
name */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
C Comments:
void f() {
int i = 0;
...
foo(current)
}
Here, current is passed in as a parameter to foo, but it is a
pointer that hasn't been pointed at anything. I cannot tell
you how many times I personally have written bugs myself
or fixed bugs in student code, caused by reading or writing
to pointers that weren't pointing at anything in particular.
Local variables that weren't initialized point at random
garbage. If you are lucky this is a coredump, but you might
not be lucky, you might not find out where the mistake
was, you might just get a wrong answer. This can all be
fixed by
struct tokenlist *current = NULL, *head = NULL;
Avoid this common C bug:
struct token *t = (struct token
*)malloc(sizeof(struct token *)));
This compiles, but causes coredumps during program
execution. Why?
Check your malloc() return value to be sure it is not NULL.
Sure, modern programs will "never run out of memory".
Wrong. malloc() can return NULL even on big machines.
Operating systems often place limits on memory so as to
protect themselves from runaway programs or hacker
attacks.
(a|c)*|(a|c)*b(a|c)*
(a|c)*(b|)(a|c)*
Regular expressions can be converted automatically to
NFA's
Each rule in the definition of regular expressions has a
corresponding NFA; NFA's are composed using transitions.
This is called "Thompson's construction" ). We will work
examples such as (a|b)*abb in class and during lab.
1. For , draw two states with a single transition.
2. For any letter in the alphabet, draw two states with a single
transition labeled with that letter.
and s.
...
F -> ident
Given the arithmetic expression grammar from last lecture:
How can a program figure that x + y * z is legal?
How can a program figure out that x + y (* z) is illegal?
A brief aside on casting your mallocs
If you don't put a prototype for malloc(), C thinks it returns an
int.
#include <stdlib.h>
includes prototypes for malloc(), free(), etc. malloc() returns a
void *.
void * means "pointer that points at nothing", or "pointer that
points at anything". You need to cast it to what you are really
pointing at, as in:
union lexval *l = (union lexval *)malloc(sizeof(union lexval));
Note the stupid duplication of type information; no language is
perfect! Anyhow, always cast your mallocs. The program may
work without the cast, but you need to fix every warning, so you
don't accidentally let a serious one through.
Recursive Descent Parsing
Perhaps the simplest parsing method, for a large subset of
context free grammars, is called recursive descent. It is simple
because the algorithm closely follows the production rules of
nonterminal symbols.
Write 1 procedure per nonterminal rule
T -> T * F | F
F -> ( E ) | ident
We can remove the left recursion by introducing new
nonterminals and new production rules.
E -> T E'
E' -> + T E' |
T -> F T'
T' -> * F T' |
F -> ( E ) | ident
Getting rid of such immediate left recursion is not enough, one
must get rid of indirect left recursion, where two or more
nonterminals are mutually left-recursive. One can
rewrite any CFG to remove left recursion (Algorithm 4.1).
for i := 1 to n do
for j := 1 to i-1 do begin
replace each Ai -> Aj gamma with productions
Ai -> delta1gamma | delta2gamma
end
eliminate immediate left recursion
Removing Left Recursion, part 2
Left recursion can be broken into three cases
case 1: trivial
A:A|
The recursion must always terminate by A finally deriving so
you can rewrite it to the equivalent
A : A'
A' : A' |
Example:
E : E op T | T
can be rewritten
E : T E'
E' : op T E' |
case 2: non-trivial, but immediate
In the more general case, there may be multiple recursive
productions and/or multiple non-recursive productions.
A : A 1 | A 2 | ... | 1 | 2
As in the trivial case, you get rid of left-recursing A and
introduce an A'
A : 1 A' | 2 A' | ...
A' : 1 A' | 2 A' | ... |
case 3: mutual recursion
1. Order the nonterminals in some order 1 to N.
2. Rewrite production rules to eliminate all nonterminals in
leftmost positions that refer to a "previous" nonterminal.
When finished, all productions' right hand symbols start
with a terminal or a nonterminal that is numbered equal or
higher than the nonterminal no the left hand side.
3. Eliminate the direct left recusion as per cases 1-2.
end
procedure A()
if yychar == a then { # use production 2
yychar := scan()
return A()
}
else
succeed # production rule 3, match
end
procedure B()
if yychar == b then {
yychar := scan()
succeed
}
else fail
end
procedure C()
if yychar == c then {
yychar := scan()
succeed
}
else fail
end
Backtracking?
Could your current token begin more than one of your possible
production rules? Try all of them, remember and reset state for
each try.
S -> cAd
A -> ab
A -> a
Left factoring can often solve such problems:
S -> cAd
A -> a A'
A'-> b
A'-> ()
One can also perform left factoring to reduce or eliminate the
lookahead or backtracking needed to tell which production rule
to use. If the end result has no lookahead or backtracking
needed, the resulting CFG can be solved by a "predictive parser"
and coded easily in a conventional language. If backtracking is
needed, a recursive descent parser takes more work to
implement, but is still feasible. As a more concrete example:
S -> if E then S
S -> if E then S1 else S2
can be factored to:
S -> if E then S S'
S'-> else S2 |
Some More Parsing Theory
Follow(A)
Follow(A) for nonterminal A is the set of terminals that can
appear immediately to the right of A in some sentential form S > aAxB... To compute Follow, apply these rules to all
nonterminals in the grammar:
1. Add $ to Follow(S)
2. if A -> aBb then add First(b) - to Follow(B)
3. if A -> aB or A -> aBb where is in First(b), then add
Follow(A) to Follow(B).
On resizing arrays in C
The sval attribute in homework #2 is a perfect example of a
problem which a BCS major might not be expected to manage,
but a CS major should be able to do by the time they graduate.
This is not to encourage any of you to consider BCS, but rather,
to encourage you to learn how to solve problems like these.
The problem can be summarized as: step through yytext,
copying each piece out to sval, removing doublequotes and
plusses between the pieces, and evaluating CHR$() constants.
Space allocated with malloc() can be increased in size by
realloc(). realloc() is awesome. But, it COPIES and MOVES the
old chunk of space you had to the new, resized chunk of space,
and frees the old space, so you had better not have any other
pointers pointing at that space if you realloc(), and you have to
update your pointer to point at the new location realloc() returns.
i = 0; j = 0;
while (yytext[i] != '\0') {
if (yytext[i] == '\"') {
/* copy string into sval */
i++;
while (yytext[i] != '\"') {
sval[j++] = yytext[i++];
}
}
else if ((yytext[i] == 'C') || (yytext[i] == 'c')) {
/* handle CHR$(...) */
i += 5;
k = atoi(yytext + i);
sval[j++] = k;
/* might check for 0-255 */
while (yytext[i] != ')') i++;
}
/* else we can just skip it */
i++;
}
sval[j] = '\0'; /* NUL-terminate our string */
There is one more problem: how do we allocate memory for
sval, and how big should it be?
Solution #1: sval = malloc(strlen(yytext)+1) is very safe,
but wastes space.
Solution #2: you could malloc a small amount and grow the
array as needed.
sval = strdup("");
...
sval = malloc(strlen(yytext)+1);
/* ... do the code copying into sval; be sure to
NUL-terminate */
YACC
Bottom Up Parsing
S->aABe
A->Abc
A->b
B->d
Handles
2.
3.
Input
w$
Shift one symbol from the input onto the parse stack
2.
3.
each time yylex() returns to the parser will get copied over to the
top of the value stack when the token is shifted onto the parse
stack.
You can either declare that struct token may appear in the
%union,
and put a mixture of struct node and struct token on the value
stack,
or you can allocate a "leaf" tree node, and point it at your struct
token. Or you can use a tree type that allows tokens to include
their lexical information directly in the tree nodes. If you have
more than one %union type possible, be prepared to see type
conflicts
and to declare the types of all your nonterminals.
Getting all this straight takes some time; you can plan on
it. Your best
bet is to draw pictures of how you want the trees to look, and
then make the
code match the pictures. No pictures == "Dr. J will ask to see
your
pictures and not be able to help if you can't describe your trees."
Declaring value stack types for terminal and nonterminal
symbols
Unless you are going to use the default (integer) value stack, you
will
have to declare the types of the elements on the value
stack. Actually,
you do this by declaring which
union member is to be used for each terminal and nonterminal in
the
grammar.
Example: in the cocogram.y that I gave you we could add a
%union declaration
with a union member named treenode:
%union {
nodeptr treenode;
}
might write:
%token < tokenptr > SEMICOL
Announcements
Announcements
shift-reduce
a shift reduce conflict occurs when the grammar indicates
that
different successful parses might occur with either a
shift or a reduce
at a given point during parsing. The vast majority of
situations where
this conflict occurs can be correctly resolved by shifting.
reduce-reduce
a reduce reduce conflict occurs when the parser has two or
more
handles at the same time on the top of the
stack. Whatever choice
the parser makes is just as likely to be wrong as not. In
this case
it is usually best to rewrite the grammar to eliminate the
conflict,
possibly by factoring.
S -> id LP plist RP
S -> E GETS E
plist -> plist, p
plist -> p
p -> id
E -> id LP elist RP
E -> id
elist -> elist, E
elist -> E
the parser will not know which rule to use to reduce the id: (5)
or (7).
Further Discussion of Reduce Reduce and Shift Reduce
Conflicts
T : F | F T2 ;
T2 : p F T2 | ;
F:lTr|v;
T : F g;
T : F T2 g;
T2 : t F T2 ;
T2 : ;
F:lTr;
F:v;
This grammar is not much different than before, and has the
same problem,
but the surrounding context (the "calling environments") of F
cause the
grammar to have a shift-reduce instead of reduce-reduce. Once
again,
the trouble is after you have seen an F and dwells on the
question of
whether to reduce the epsilon production, or instead to shift,
upon
seeing a token g.
%right ASSIGN
%left PLUS MINUS
%left TIMES DIVIDE
%right POWER
%%
expr: expr ASSIGN expr
| expr PLUS expr
| expr MINUS expr
| expr TIMES expr
| expr DIVIDE expr
| expr POWER expr
;
Merr also uses the yychar (current input token) to refine the
diagnostics
in the event that two of your example errors occur on the same
parse state.
See the Merr web page.
Announcements
The TA's HW2 grades are available from the TA. The
distribution (out of 80) was
76, 74, 74, 74, 73, 72, 66, 65, 55, 52, 46, 35, 30, 30, 30, 15, 14
1/3rd of the class got an "A". The rest of you need to visit the
TA, see how
the grades were measured, see the professor, and most
important, get a lexical
analyzer working well enough to complete the later assignments
in this course.
If your grade was below 70, you probably want to get it working
and resubmit
The first char ("L") means input tokens are read from the left
(left to right). The second char ("R" or "L") means parsing
finds the rightmost, or leftmost, derivation. Relevant
if there is ambiguity in the grammar. (0) or (1) or (k) after
the main lettering indicates how many lookahead characters are
used. (0) means you only look at the parse stack, (1) means you
use the current token in deciding what to do, shift or reduce.
(k) means you look at the next k tokens before deciding what
to do at the current position.
LR Parsers
Note: in Spring 2006 this material is FYI but you will not be
examined on it.
A -> a . A b
A -> a A . b
A -> a A b .
Note: A production A-> generates
only one item:
A -> .
Intuition: an item A-> . denotes:
1.
2.
3.
4.
5.
6.
6.
7.
to closure(I).
These two rules are applied repeatedly until no new items can
be added.
Intuition: If A -> . B is in
closure(I) then we hope to see a string derivable from B in the
input. So if B-> is a production,
we should hope to see a string derivable from .
Hence, B->. is in closure(I).
[A->.X]
is in I => we've seen a string derivable
from ; we hope to see a string derivable
from X.
E -> E+T | T
T -> T*F | F
F -> (E) | id
1.
begin
C := { closure({[S' -> .S]}) };
repeat
for each set of items I in C:
if 2 != ,
we should shift
4.
5.
6.
7.
if 2 = ,
A -> 1 is the handle,
and we should reduce by this production
Note: two valid items may tell us to do different things for the
same viable prefix. Some of these conflicts can be resolved
using
lookahead on the input string.
Constructing an SLR Parsing Table
1.
o
o
o
o
o
o
o
[A -> .] is in
Ii : set action[i,a] to "reduce A -> x"
for all a e FOLLOW(A), where A != S'
[S' -> S] is in Ii :
set action[i,$] to "accept"
11.
12.
13.
14.
goto transitions constructed as follows: for all nonterminals:
15.
if goto(Ii, A) = Ij, then goto[i,A] = j
16.
17.
All entries not defined by (3) & (4) are made "error".
18.
If there are any multiply defined entries, grammar is
not SLR.
19.
20.
Initial state S0 of parser: that constructed from
21.
I0 or [S' -> S]
22.
Example:
S -> aABe
A -> Abc
{b,d}
FIRST(S) = {a}
FIRST{A} = {b}
FOLLOW(S) = {$}
FOLLOW(A) =
A -> b
FIRST{B} = {d}
FOLLOW{B} =
B -> d
FIRST{S'}= {a}
FOLLOW{S'}=
{e}
{$}
I0 = closure([S'->.S]
= closure([S'->.S],[S->.aABe])
goto(I0,S) = closure([S'->S.]) = I1
goto(I0,a) = closure([S->a.Abe])
= closure([S->a.Abe],[A->.Abc],[A->.b]) = I2
goto(I2,A) = closure([S->aA.Be],[A->A.bc])
= closure([S->aA.Be],[A->A.bc],[B->.d]) = I3
goto(I2,B) = closure([A->b.]) = I4
goto(I3,B) = closure([S->aAB.e]) = I5
goto(I3,b) = closure([A->Ab.c]) = I6
goto(I3,d) = closure([B->d.]) = I7
goto(I5,e) = closure([S->aABe.]) = I8
goto(I6,c) = closure([A->Abc.]) = I9
On Tree Traversals
Trees are classic data structures. Trees have nodes and edges, so
they are
a special case of graphs. Tree edges are directional, with roles
"parent"
and "child" attributed to the source and destination of the edge.
A tree has the property that every node has zero or one
parent. A node
with no parents is called a root. A node with no children is
called a leaf.
A node that is neither a root nor a leaf is an "internal
node". Trees have
a size (total # of nodes), a height (maximum count of nodes
from root to a leaf),
and an "arity" (maximum number of children in any one node).
#include <stdarg.h>
struct tree {
short label;
short nkids;
};
struct tree *alctree(int label, int nkids, ...)
{
int i;
va_list ap;
struct tree *ptr = malloc(sizeof(struct tree) +
(nkids-1)*sizeof(struct tree *));
if (ptr == NULL) {fprintf(stderr, "alctree out of memory\n");
exit(1); }
ptr->label = label;
ptr->nkids = nkids;
va_start(ap, nkids);
for(i=0; i < nkids; i++)
ptr->child[i] = va_arg(ap, struct tree *);
va_end(ap);
return ptr;
}
if (t == NULL) return;
printf("%p: %d, %d children\n", t, t->label, t->nkids);
}
Semantic Analysis
syntax-directed definitions
high-level (declarative) specifications of semantic rules
translation schemes
semantic rules and the order in which they get evaluated
synthesized
attributes computed from information contained within
one's children.
These are generally easy to compute, even on-the-fly
during parsing.
inherited
Attribute Examples
Not all expressions have constant values; the ones that do may
allow
various optimizations.
CFG
Semantic Rule
E1.isconst = E2.isconst &&
E1 : E2 + T T.isconst
if (E1.isconst)
E:T
T:T*F
T:F
F:(E)
F : ident
F : intlit
mktable(parent)
creates a new symbol table, whose scope is local to (or
inside) parent
enter(table, symbolname, type, offset)
insert a symbol into a table
lookup(table, symbolname)
lookup a symbol in a table; returns structure pointer
including type and offset. lookup operations are often
chained together progressively from most local scope on
out to global scope.
addwidth(table)
struct node {
int code;
/* terminal or nonterminal symbol */
int nkids;
union {
struct token { ... } leaf;
struct node *kids[9];
}u;
};
Type Checking
o
o
o
o
o
o
o
o
are types.
1.
17.
that include identifiers and function calls with
parameters.
18.
Parameters may themselves be function calls, as in
f(g(x)),
19.
or h(a,b,i(j(k,l)))
20.
21.
What are the FIRST(E) and FOLLOW(T) in the
grammar:
22.
23.
E:E+T|T
24.
T:T*F|F
F : ( E ) | ident
25.
What is the -closure(move({2,4},b)) in the following
NFA?
26.
That is, suppose you might be in either state 2 or 4
at the time
27.
you see a symbol b: what NFA states might you
find yourself in
28.
after consuming b?
(automata to be written on the board)
29.
struct c_type {
int base_type; /* 1 = int, 2=float, ... */
union {
struct array {
int size;
struct c_type *elemtype;
} a;
struct ctype *p;
struct struc {
char *label;
struct field **f;
} s;
} u;
}
struct field {
char *name;
struct ctype *elemtype;
}
int [10][20]
struct foo { int x; char *s; }
grammar rule
semantic rule
E1 : E2 PLUS E3 E1.type = check_types(PLUS, E2.type, E3.type)
1.
13.
You have to use the struct information to check the
validity of each
14.
dot operator like in rec.foo. To do this you'll have
to lookup rec
15.
in the symbol table, where you store rec's
type. rec's type must be
16.
a struct type for the dot to be legal, and that struct
type should
17.
include a hash table or link list that gives the names
and types of
18.
the fields -- where you can lookup the name foo to
find its type.
19.
Run-time Environments
How does a compiler (or a linker) compute the addresses for the
various
instructions and references to data that appear in the program
source code?
To generate code for it, the compiler has to "lay out" the data as
it will
be used at runtime, deciding how big things are, and where they
will go.
Announcements
1.
2.
3.
4.
5.
8.
Activation Records
...
parameter
previous frame pointer (FP)
saved registers
...
FP--> saved PC
local
...
local
temporaries
SP--> ...
At any given instant, the live activation records form a chain and
follow a stack discipline. Over the lifetime of the program, this
information (if saved) would form a gigantic tree. If you
remember
prior execution up to a current point, you have a big tree in
which
its rightmost edge are live activation records, and the nonrightmost
tree nodes are an execution history of prior calls.
Approaches:
o
o
o
o
reference counting
traversal of known pointers (marking)
o
conservative collection
There is one more tool you should know about, which is useful
for certain
kinds of bugs, primarily subtle memory violations. It is called
electric
fence. To use electric fence you add
/home/uni1/jeffery/ef/ElectricFence-2.1/libefence.a
to the line in your makefile that links your object files together
to
form an executable.
newlabel()
Production
Semantic Rules
S -> id ASN
S.code = E.code || gen(ASN, id.place, E.place)
E
E.place = newtemp();
E -> E1
E.code = E1.code || E2.code ||
PLUS E2
gen(PLUS,E.place,E1.place,E2.place);
E.place = newtemp();
E -> E1 MUL
E.code = E1.code || E2.code ||
E2
gen(MUL,E.place,E1.place,E2.place);
E -> MINUS E.place = newtemp();
E1
E.code = E1.code || gen(NEG,E.place,E1.place);
E -> LP E1 E.place = E1.place;
RP
E.code = E1.code;
E.place = id.place;
E -> IDENT
E.code = emptylist();
Three-Address Code
Instruction set:
mnemonic
ADD,
SUB,MUL,DIV
C equivalent
NEG
x := op y
ASN
ADDR
x := y
x := &y
LCONT
x := *y
x := y op z
description
store result of binary
operation on y and z to x
store result of unary
operation on y to x
store y to x
store address of y to x
store contents pointed to by
y to x
SCONT
*x := y
GOTO
PARM
goto L
if x rop y then
goto L
if x then goto
L
if !x then goto
L
param x
CALL
call p,n,x
RET
return x
BLESS,...
BIF
BNIF
local x,n
o
o
o
o
o
o
o
o
o
o
o
o
o
struct descrip {
short type;
short size;
union {
char *string;
int ival;
float rval;
struct descrip *array;
/* ... for other types */
} value;
};
Basic Blocks
read x
t1 = x > 0
if t1 == 0 goto L1
fact = 1
label L2
t2 = fact * x
fact = t2
t3 = x - 1
x = t3
t4 = x == 0
if t4 == 0 goto L2
t5 = addr const:0
param t5
; "%d\n"
param fact
call p,2
label L1
halt
Attribute Manipulations
E.true = newlabel();
E.false = S.follow;
S->if E then S1
S1.follow = S.follow;
S.code = E.code || gen(LABEL, E.true)||
S1.code
E.true = newlabel();
E.false = newlabel();
S1.follow = S.follow;
S->if E then S1 else S2 S2.follow = S.follow;
S.code = E.code || gen(LABEL, E.true)||
S1.code || gen(GOTO, S.follow) ||
gen(LABEL, E.false) || S2.code
Announcement
To request a referral go to www.nmsu.edu/pment, click on "Coop Job Listings", Job #86 or call the co-op office at 6464115. LANL is requiring a cover letter to also be sent,
please send that via email at coop@nmsu.edu in the subject
line put attn: LANL cover letter.
Co-op Office
646-4115
More on Generating Code for Boolean Expressions
1.
Short-Circuit Example
L2:
L3:
L1:
L4:
if a<b goto L1
if c<d goto L2
goto L3
if e<f goto L1
t=0
goto L4
t=1
...
While Loops
Attribute Manipulations
E.true = newlabel();
E.false = S.follow;
S1.follow = E.first;
S->while E do S1 S.code = gen(LABEL, E.first) ||
E.code || gen(LABEL, E.true)||
S1.code ||
gen(GOTO, E.first)
void main()
{
int i;
i = 0;
while (i < 20)
i = i * i + 1;
print(i);
}
void print(int i) { }
opcode
dest
local
src1 src2
local const
LT
t0.offset i.offset 20
opcode
dest
local
src1 src2
local local
MUL
t1.offset i.offset i.offset
void codegen(nodeptr t)
{
int i, j;
if (t==NULL) return;
/*
* this is a post-order traversal, so visit children first
*/
for(i=0;i<t->nkids;i++)
codegen(t->child[i]);
/*
* back from children, consider what we have to do with
* this node. The main thing we have to do, one way or
* another, is assign t->code
*/
switch (t->label) {
case PLUS: {
t->code = concat(t->child[0].code, t->child[1].code);
g = gen(PLUS, t->address,
t->child[0].address, t->child[1].address);
t->code = concat(t->code, g);
break;
}
/* ... really, we need a bazillion cases, perhaps one for each
* production rule (in the worst case)
*/
default:
/* default is: concatenate our children's code */
t->code = NULL;
for(i=0;i<t->nkids;i++)
t->code = concat(t->code, t->child[i].code);
}
}
Zero operators.
if (x) S
translates into
if x != 0 goto L1
goto L2
label L1
...code for S
label L2
if x == b goto L1
...code for S
label L1
I may do this without comment in later examples, to keep them
short.
if (a < b) S
translates into
if i >= b goto L1
...code for S
label L1
if (a < b)
if (c > d)
...code for S
which if we expand it
if i >= b goto L1
if c <= d goto L2
...code for S
label L2
label L1
if (a < b || c > d) S
translates into
if a < b goto L1
if c > d goto L2
goto L3
label L2
label L1
...code for S
label L3
Array subscripting!
t0 := addr a
t1 := i * 4
t2 := plus t0 t1
t3 := deref t2
x := t3
Debugging Miscellany
makefile .h dependencies!
if you do not list makefile dependencies for important .h
files,
you may get coredumps!
traversing multiple times by accident?
at least in my version, I found it easy to accidentally retraverse
portions of the tree. this usually had a bad effect.
bad grammar?
our sample grammar was adapted from good sources, but
don't assume its
impossible that it could have a flaw or that you might
have messed it up.
Final Code
Alternatives:
interpret the source code
we could have build an interpreter instead of a compiler, in
which the
source code was kept in string or token form, and reparsed every
execution. Early BASIC's did this, but it is Really Slow.
interpret the parse tree
we could have written an interpreter that executes the
program
by walking around on the tree doing traversals of
various subtrees.
In mainstream compilers,
final code generation takes a linear sequence of 3-address
intermediate
code instructions, and translates each 3-address instruction into
one or
more native instructions.
The big issues in code generation are (a) instruction selection,
and (b)
register allocation and assignment.
Instruction Selection
lvalue() and rvalue() are mini-tree traversals for the lefthand side
and righthand side of an assignment statement. Their missions
are to
propagate information from the parent, namely, inherited
attributes
In real life, you should build a flow graph, and propagate these
variable definition and use attributes using the flow graph
instead
of the syntax tree. For example, if the program starts by calling
a subroutine at the bottom of code which initializes all the
variables, the flow graph will not be fooled into generating
warnings
like you would if you just started at the top of the code and
checked
whether for each variable, assignments appear earlier in the
source
code than the uses of that variable.
Runtime Systems
INPUT/PRINT
READ/DATA
CLEAR
CLOAD/CSAVE/SKIPF
CLS
SET/RESET
SOUND
CHR$, LEFT$, MID$, RIGHT$, INKEY$
ASC, INT, JOYSTK, LEN, PEEK, RND, VAL
DIM
string +, string compares
libb.c
STRCAT
Our 3-address instruction set has call and return instructions, but
basic
is less structured than regular procedural languages; you can
GOSUB to any
line number you want. You can't use a variable to GOSUB to
line number X,
but in principle every line number could be the target of a
procedure call.
If you use the "call" (3-address) instruction to do GOSUB, your
native code
will have to make a clear distinction between BASIC call's and
calls to
Reusing a Register
b, %eax
a, %eax
c, %eax
d, %eax
e, %eax
f, %eax
g, %eax
a, %eax
c, %eax
e, %eax
%eax, a
Now consider
a = (a+b)*(c+d)*(e+f)*(g+a)*(c+e);
How many registers are needed here?
movl
movl
b, %eax
a, %edx
addl
movl
addl
imull
movl
addl
imull
movl
addl
imull
movl
addl
imull
movl
%eax, %edx
d, %eax
c, %eax
%eax, %edx
f, %eax
e, %eax
%eax, %edx
a, %eax
g, %eax
%eax, %edx
e, %eax
c, %eax
%edx, %eax
%eax, a
b, %eax
a, %edx
%eax, %edx
d, %eax
c, %eax
%edx, %ecx
%eax, %ecx
f, %eax
e, %edx
addl
movl
addl
imull
leal
movl
imull
leal
movl
%eax, %edx
a, %eax
g, %eax
%edx, %eax
(%eax,%ecx), %edx
c, %eax
e, %eax
(%eax,%edx), %eax
%eax, a
movl
imull
movl
movl
addl
movl
addl
imull
movl
imull
movl
movl
cltd
idivl
movl
movl
leal
movl
%edx, %ecx
%eax, %ecx
f, %eax
e, %edx
%eax, %edx
a, %eax
g, %eax
%eax, %edx
c, %eax
e, %eax
%eax, -4(%ebp)
%edx, %eax
-4(%ebp)
%eax, -4(%ebp)
-4(%ebp), %edx
(%edx,%ecx), %eax
%eax, a
Just for fun, let's compare the generated code for java with that
X86
native code we were just looking at:
iload_1
iload_2
iadd
iload_3
iload 4
iadd
imul
iload 5
iload 6
iadd
iload 7
iload_1
iadd
imul
iload_3
iload 5
imul
idiv
iadd
istore_1
name
redundant load or store
sample
MOVE R0,a
MOVE a,R0
optimized as
MOVE R0,a
dead code
control flow
simplification
algebraic simplification
strength reduction
#define debug 0
...
if (debug)
printf("ugh");
if a < b goto L1
...
L1: goto L2
if a < b goto
L2
...
L1: goto L2
x = x * 1;
x = y * 16;
x = y << 4;
Constant Folding
x = 7;
...
y = x+5;
Explicit:
(a+b)*i + (a+b)/j;
Implicit:
x = a[i]; a[i] = a[j]; a[j] = x;
Loop Unrolling
Gotos are expensive (do you know why?). If you know a loop
will
execute at least (or exactly) 3 times, it may be faster to copy
the
loop body those three times than to do a goto. Removing
gotos
simplifies code, allowing other optimizations.
x += 0 *
0;
y += x *
y += x * x;
x;
x += 1;
for(i=0; i<3; i++) { x += i * i; y += x
x += 1 * y += x *
* x; } >
1;
x;
y += x * x += 4;
x;
y += x *
x += 2 * x;
2;
y += x *
x;
t_0 = strlen(s);
for (i=0; i<strlen(s); i++)
for (i=0; i<t_0; i++)
s[i] = tolower(s[i]);
s[i] = tolower(s[i]);
Interprocedural Optimization
f(x,r,s,1);
f_1(x,r,s);
o
o
o
o
o
o
o
o
o
o
o
o
o
o