Compiler Design-Notes
Compiler Design-Notes
1
CS3501 COMPILER DESIGN SYLLABUS
COURSE OBJECTIVES:
To learn the various phases of compiler.
To learn the various parsing techniques.
To understand intermediate code generation and run-time environment.
To learn to implement the front-end of the compiler.
To learn to implement code generator.
To learn to implement code optimization.
45 PERIODS
COURSE OUTCOMES:
On Completion of the course, the students should be able to:
CO1:Understand the techniques in different phases of a compiler.
CO2:Design a lexical analyser for a sample language and learn to use the LEX tool.
CO3:Apply different parsing algorithms to develop a parser and learn to use YACC tool
CO4:Understand semantics rules (SDT), intermediate code generation and run-time environment.
2
UNIT 1 INTRODUCTION TO COMPILERS & LEXICAL ANALYSIS
• Preprocessor
A preprocessor produce input to compilers. They may perform the following functions.
1. Macro processing: A preprocessor may allow a user to define macros that are short hands for longer
constructs.
2. File inclusion: A preprocessor may include header files into the program text.
3. Rational preprocessor: these preprocessors augment older languages with more modern flow-of-control
and data structuring facilities.
4. Language Extensions: These preprocessor attempts to add capabilities to the language by certain amounts
to build-in macro
• Compiler
Compiler is a software which converts a program written in high level language (Source Language) to low level
language (Object/Target/Machine Language).
• Cross Compiler that runs on a machine ‘A’ and produces a code for another machine ‘B’. It is capable of
creating code for a platform other than the one on which the compiler is running.
• Source-to-source Compiler or transcompiler or transpiler is a compiler that translates source code written
in one programming language into source code of another programming language.
3
• High Level Language – If a program contains #define or #include directives such as #include or #define it is
called HLL. They are closer to humans but far from machines. These (#) tags are called pre-processor
directives. They direct the pre-processor about what to do.
• Pre-Processor – The pre-processor removes all the #include directives by including the files called file
inclusion and all the #define directives using macro expansion. It performs file inclusion, augmentation,
macro-processing etc.
• Assembly Language – Its neither in binary form nor high level. It is an intermediate state that is a
combination of machine instructions and some other useful data needed for execution.
• Assembler – For every platform (Hardware + OS) we will have a assembler. They are not universal since for
each platform we have one. The output of assembler is called object file. Its translates assembly language
to machine code.
• Interpreter – An interpreter converts high level language into low level machine language, just like a
compiler. But they are different in the way they read the input. The Compiler in one go reads the inputs,
does the processing and executes the source code whereas the interpreter does the same line by line.
Compiler scans the entire program and translates it as a whole into machine code whereas an interpreter
translates the program one statement at a time. Interpreted programs are usually slower with respect to
compiled ones.
• Relocatable Machine Code – It can be loaded at any point and can be run. The address within the program
will be in such a way that it will cooperate for the program movement.
• Loader/Linker – It converts the relocatable code into absolute code and tries to run the program resulting
in a running program or an error message (or sometimes both can happen). Linker loads a variety of object
files into a single file to make it executable. Then loader loads it in memory and executes it.
TYPE OF TRANSLATORS:-
• INTERPRETOR
• COMPILER
• PREPROCESSOR
Steps of Programming:
• Program Creation.
• Analysis of language by the
compiler and throws errors in
Steps of Programming:
case of any incorrect
• Program Creation.
statement.
• Linking of files or generation of Machine Code is not
• In case of no error, the
required by Interpreter.
Compiler converts the source
• Execution of source statements one by one.
code to Machine Code.
• Linking of various code files
into a runnable program.
• Finally runs a Program.
The compiler saves the Machine The Interpreter does not save the Machine Language.
4
Compiler Interpreter
Any change in the source program Any change in the source program during the
after the compilation requires translation does not require retranslation of the entire
recompiling the entire code. code.
CPU utilization is more in the case CPU utilization is less in the case of a Interpreter.
5
Compiler Interpreter
of a Compiler.
COUSINS OF COMPILER
1. Preprocessor 2. Assembler 3. Loader and Link-editor
• Parser Generator
• Scanner Generator
• Syntax Directed Translation Engines
• Automatic Code Generators
• Data-Flow Analysis Engines
• Compiler Construction Toolkits
Parser Generator
Parser Generator produces syntax analyzers (parsers) based on context-free grammar that takes input in the form of
the syntax of a programming language. It's helpful because the syntax analysis phase is quite complex and takes more
compilation and manual time.
Scanner Generator
6
Scanner Generator generates lexical analyzers from the input that consists of regular expression descriptions based on
tokens of a language. It generates a finite automaton to identify the regular expression.
Syntax Directed Translation Engines take a parse tree as input and generate intermediate code with three address
formats. These engines contain routines to traverse the parse tree and generate intermediate code. Each parse tree node
has one or more translations associated with it.
Automatic Code Generators take intermediate code as input and convert it into machine language. Each intermediate
language operation is translated using a set of rules and then sent into the code generator as an input. A template
matching process is used, and by using the templates, an intermediate language statement is replaced by its machine
language equivalent.
Data-Flow Analysis Engines is used for code optimization and can generate an optimized code. Data flow analysis is an
essential part of code optimization that collects the information, the values that flow from one part of a program to
another.
Compiler Construction Toolkits provide an integrated set of routines that helps in creating compiler components or in the
construction of various phases of a compiler.
7
Symbol Table – It is a data structure being used and maintained by the compiler, consists all the
identifier’s name along with their types. It helps the compiler to function smoothly by finding the
identifiers quickly. It consists of the following
• An extensible array of records.
• The identifier and the associated records contains collected information about the identifier.
• FUNCTION identify (Identifier name)
• RETURNING a pointer to identifier information contains
• The actual string
• A macro definition
• A keyword definition
• A list of type, variable & function definition
• A list of structure and union name definition
• A list of structure and union field selected definitions.
8
1. Lexical Analyzer – It reads the program and converts it into tokens. It converts a stream of lexemes into a
stream of tokens. Tokens are defined by regular expressions which are understood by the lexical analyzer.
It also removes white-spaces and comments.
2. Syntax Analyzer – It is sometimes called as parser. It constructs the parse tree. It takes all the tokens one
by one and uses Context Free Grammar to construct the parse tree.
The rules of programming can be entirely represented in some few productions. Using these
productions we can represent what the program actually is. The input has to be checked whether it is in
the desired format or not.
Syntax error can be detected at this level if the input is not in accordance with the grammar.
3. Semantic Analyzer – It verifies the parse tree, whether it’s meaningful or not. It furthermore produces a
verified parse tree.It also does type checking, Label checking and Flow control checking.
4. Intermediate Code Generator – It generates intermediate code, that is a form which can be readily
executed by machine We have many popular intermediate codes. Example – Three address code etc.
Intermediate code is converted to machine language using the last two phases which are platform
dependent.
Till intermediate code, it is same for every compiler out there, but after that, it depends on the platform.
To build a new compiler we don’t need to build it from scratch. We can take the intermediate code from
the already existing compiler and build the last two parts.
5. Code Optimizer – It transforms the code so that it consumes fewer resources and produces more
speed. The meaning of the code being transformed is not altered. Optimisation can be categorized into
two types: machine dependent and machine independent.
6. Target Code Generator – The main purpose of Target Code generator is to write a code that the
machine can understand and also register allocation, instruction selection etc. The output is dependent
on the type of assembler. This is the final stage of compilation.
Example:Consider the given expression and explain the different phases of compiler in evaluating the
expression
c=a+b*5;
Lexical Analysis
Lexemes Tokens
c identifier
= assignment symbol
a identifier
+ + (addition symbol)
b identifier
* * (multiplication symbol)
5 5 (number)
Hence, <id, 1><=>< id, 2>< +><id, 3 >< * >< 5>
9
Syntax Analysis
• Syntax analysis is the second phase of compiler which is also called as parsing.
Input: Tokens
Output: Syntax tree
Semantic Analysis
• Semantic analysis is the third phase of compiler.
• It checks for the semantic consistency.
• Type information is gathered and stored in symbol table or in syntax tree.
• Performs type checking.
10
Code Optimization
• Code optimization phase gets the intermediate code as input and produces optimized intermediate code as
output.
• It results in faster running machine code.
• It can be done by reducing the number of lines of code for a program.
(I).Local Optimization:-
There are local transformations that can be applied to a program to make an improvement. For example,
If A > B goto L2
Goto L3
L2 :
This can be replaced by a single statement
If A < B goto L3
(II)Loop Optimization:-
Another important source of optimization concerns about increasing the speed of loops. A typical loop
improvement is to move a computation that produces the same result each time around the loop to a point, in
the program just before the loop is entered.
To improve the code generation, the optimization involves
o Deduction and removal of dead code (unreachable code).
o Calculation of constants in expressions and terms.
o Collapsing of repeated expression into temporary string.
o Loop unrolling.
o Moving code outside the loop.
o Removal of unwanted temporary variables.
t1 = id3* 5.0
id1 = id2 + t1
Code Generation
• Code generation is the final phase of a compiler.
• It gets input from code optimization phase and produces the target code or object code as result.
• Intermediate instructions are translated into a sequence of machine instructions that perform the same task.
• The code generation involves
o Allocation of register and memory.
o Generation of correct references.
o Generation of correct data types.
o Generation of missing code.
LDF R2, id3
MULF R2, # 5.0
LDF R1, id2
ADDF R1, R2
11
STF id1, R1
Table Management (or) Book-keeping:-
This is the portion to keep the names used by the program and records essential information about each.
The data structure used to record this information called a ‘Symbol Table’.
Error Handlers:-
It is invoked when a flaw error in the source program is detected.
12
Lexical Analysis
Lexical analysis is the process of converting a sequence of characters from source program into a sequence of
tokens.
13
A program which performs lexical analysis is termed as a lexical analyzer (lexer), tokenizer or scanner.
Lexical analysis consists of two stages of processing which are as follows:
• Scanning
• Tokenization
14
Role of Lexical Analyzer
15
• The longest match is preferred.
• Among rules which matched the same number of characters, the rule given first is preferred.
LEXICAL ERRORS
Lexical errors are the errors thrown by your lexer when unable to continue. Which means that there's no
way to recognise a lexeme as a valid token for you lexer. Syntax errors, on the other side, will be thrown
by your scanner when a given set of already recognised valid tokens don't match any of the right sides
of your grammar rules. simple panic-mode error handling system requires that we return to a high-level
parsing function when a parsing or lexical error is detected.
Lexical error handling approaches
Lexical errors can be handled by the following actions:
• Deleting one character from the remaining input.
• Inserting a missing character into the remaining input.
• Replacing a character by another character.
• Transposing two adjacent characters.
➢ Error Recovery Schemes
• Panic mode recovery
• Local correction
o Source text is changed around the error point in order to get a correct text.
o Analyzer will be restarted with the resultant new text as input.
• Global correction
o It is an enhanced panic mode recovery.
o Preferred when local correction fails.
➢ Panic mode recovery
In panic mode recovery, unmatched patterns are deleted from the remaining input, until the lexical analyzer
can find a well-formed token at the beginning of what input is left.
Input Buffering
The lexical analyzer scans the input from left to right one character at a time. It uses two pointers begin ptr(bp)
and forward to keep track of the pointer of the input scanned.
16
Initially both the pointers point to the first character of the input string as shown below
The forward ptr moves ahead to search for end of lexeme. As soon as the blank space is encountered, it
indicates end of lexeme. In above example as soon as ptr (fp) encounters a blank space the lexeme “int” is
identified.
The fp will be moved ahead at white space, when fp encounters white space, it ignore and moves ahead. then
both the begin ptr(bp) and forward ptr(fp) are set at next token.
The input character is thus read from secondary storage, but reading in this way from secondary storage is
costly. hence buffering technique is used.A block of data is first read into a buffer, and then second by lexical
analyzer. there are two methods used in this context: One Buffer Scheme, and Two Buffer Scheme. These are
explained as following below.
17
refilled, that makes overwriting the first of lexeme.
➢ Strings
Any finite sequence of alphabets is called a string. Length of the string is the total number of occurrence of
alphabets, e.g., the length of the string sample is 14 and is denoted by |sample| = 14. A string having no
alphabets, i.e. a string of zero length is known as an empty string and is denoted by ε (epsilon).
➢ Special Symbols
A typical high-level language contains the following symbols:-
18
Punctuation Comma(,), Semicolon(;), Dot(.), Arrow(->)
Assignment =
Preprocessor #
➢ Language
A language is considered as a finite set of strings over some finite set of alphabets. Computer languages are
considered as finite sets, and mathematically set operations can be performed on them. Finite languages can
be described by means of regular expressions.
Regular expressions
Regular expressions have the capability to express finite languages by defining a pattern for finite strings of
symbols. The grammar defined by regular expressions is known as regular grammar. The language defined by
regular grammar is known as regular language.
Regular expression is an important notation for specifying patterns. Each pattern matches a set of strings, so
regular expressions serve as names for a set of strings
➢ Operations
The various operations on languages are:
19
• Union of two languages L and M is written as
L U M = {s | s is in L or s is in M}
• Concatenation of two languages L and M is written as
LM = {st | s is in L and t is in M}
• The Kleene Closure of a language L is written as
L* = Zero or more occurrence of language L.
➢ Notations
If r and s are regular expressions denoting the languages L(r) and L(s), then
• Union : (r)|(s) is a regular expression denoting L(r) U L(s)
• Concatenation : (r)(s) is a regular expression denoting L(r)L(s)
• Kleene closure : (r)* is a regular expression denoting (L(r))*
• (r) is a regular expression denoting L(r)
For relop ,we use the comparison operations of languages like Pascal or SQL where = is “equals” and < > is
“not equals” because it presents an interesting structure of lexemes.
The terminal of grammar, which are if, then , else, relop ,id and numbers are the names of tokens as far as
the lexical analyzer is concerned, the patterns for the tokens are described using regular definitions.
digit -->[0,9]
digits -->digit+
number -->digit(.digit)?(e.[+-]?digits)?
letter -->[A-Z,a-z]
id -->letter(letter/digit)*
if --> if
then -->then
else -->else
relop --></>/<=/>=/==/< >
Lex
Lex is a tool in lexical analysis phase to recognize tokens using regular expression.
• Lex tool itself is a lex compiler.
21
➢ Use of Lex
• lex.l is an a input file written in a language which describes the generation of lexical analyzer. The lex compiler
transforms lex.l to a C program known as lex.yy.c.
• lex.yy.c is compiled by the C compiler to a file called a.out.
• The output of C compiler is the working lexical analyzer which takes stream of input characters and produces
a stream of tokens.
• yylval is a global variable which is shared by lexical analyzer and parser to return the name and an attribute
value of token.
• The attribute value can be numeric code, pointer to symbol table or nothing.
• Another tool for lexical analyzer generation is Flex.
22
The token is then given to parser for further processing.
. Lookahead Operator
• Lookahead operator is the additional operator that is read by lex in order to distinguish additional pattern for
a token.
• Lexical analyzer is used to read one character ahead of valid lexeme and then retracts to produce token.
%{
#define LT 256
#define LE 257
#define EQ 258
#define NE 259
#define GT 260
#define GE 261
#define ID 263
#define IF 265
int attribute;
%}
delim [
t\n]
ws {delim}+
letter [A
23
-Za
-z]
digit [0
-9]
id {letter}({letter}|{digit})*
num {digit}+(
\.{digit}+)?(E[+
-]?{digit}+)?
%%
{ws} {}
if { return(IF); }
then { return(THEN); }
else { return(ELSE); }
{id} { return(ID); }
{num} { return(NUM); }
"<" { attribute=LT;return(RELOP); }
"<=" { attribute=LE;return(RELOP); }
"<>" { attribute=NE;return(RELOP); }
"=" { attribute=EQ;return(RELOP); }
">" { attribute=GT;return(RELOP); }
">=" { attribute=GE;return(RELOP); }
%%
int yywrap(){
return 1;
int main() {
24
int token;
while(token=yylex())
printf("<%d,",token);
switch(token)
printf("%s>\n",yytext);
break;
case RELOP:
printf("%d>\n",attribute);
break;
default:
printf(")\n");
break; }}
return 0; }
AUTOMATA
An automation is defined as a system where information is transmitted and used for performing some
functions without direct participation of man.
1, an automation in which the output depends only on the input is called an automation without memory.
2, an automation in which the output depends on the input and state also is called as automation with
memory.
3, an automation in which the output depends only on the state of the machine is called a Moore machine.
4., an automation in which the output depends on the state and input at any instant of time is called a mealy
machine.
Finite automata may have outputs corresponding to each transition. There are two types of finite state
machines that generate output –
Mealy Machine andMoore machine
Mealy Machine Moore Machine
25
Output depends both upon the present Output depends only upon the present
state and the present input state.
Generally, it has fewer states than Moore Generally, it has more states than Mealy
Machine. Machine.
The value of the output function is a The value of the output function is a
function of the transitions and the function of the current state and the
changes, when the input logic on the changes at the clock edges, whenever state
present state is done. changes occur.
Mealy machines react faster to inputs. In Moore machines, more logic is required
They generally react in the same clock to decode the outputs resulting in more
cycle. circuit delays. They generally react one
clock cycle later.
Finite automata
Finite automata is a state machine that takes a string of symbols as input and changes its state accordingly.
Finite automata is a recognizer for regular expressions. When a regular expression string is fed into finite
automata, it changes its state for each literal. If the input string is successfully processed and the automata
reaches its final state, it is accepted
The mathematical model of finite automata consists of:
1)DETERMINISTIC AUTOMATA
A deterministic finite automata has at most one transition from each state on any input. A DFA is a special
case of a NFA in which:-
1, it has no transitions on input € ,
2, each input symbol has at most one transition from any state.
The Finite Automata is called DFA if there is only one path for a specific input from current state to next
state.
From state S0 for input ‘a’ there is only one path going to S2. similarly from S0 there is only one path for
input going to S1.
2)NONDETERMINISTIC AUTOMATA
A NFA is a mathematical model that consists of
▪ A set of states S.
▪ A set of input symbols Σ.
▪ A transition for move from one state to an other.
▪ A state so that is distinguished as the start (or initial) state.
▪ A set of states F distinguished as accepting (or final) state.
▪ A number of transition to a single symbol.
Example:NFA
28
Example:Transition Diagram for recognizing identifier or variables
The above Transition Diagram for an identifier, defined to be a letter followed by any number of
letters or digits
29
Case 4 − For a regular expression (a+b)*, we can construct the following FA −
Method
Step 1 Construct an NFA with Null moves from the given regular expression.
Step 2 Remove Null transition from the NFA and convert it into its equivalent DFA.
Problem
Convert the following RA into its equivalent DFA − 1 (0 + 1)* 0
Solution
We will concatenate three expressions "1", "(0 + 1)*" and "0"
Now we will remove the ε transitions. After we remove the ε transitions from the NDFA, we get the following −
30
Minimizing DFA
Algorithm
Input − DFA
Output − Minimized DFA
Step 1 − Draw a table for all pairs of states (Qi, Qj) not necessarily connected directly [All are unmarked
initially]
Step 2 − Consider every state pair (Qi, Qj) in the DFA where Qi ∈ F and Qj ∉ F or vice versa and mark them.
[Here F is the set of final states]
Step 3 − Repeat this step until we cannot mark anymore states −
If there is an unmarked pair (Qi, Qj), mark it if the pair {δ (Qi, A), δ (Qi, A)} is marked for some input alphabet.
Step 4 − Combine all the unmarked pair (Qi, Qj) and make them a single state in the reduced DFA.
Example
Let us use Algorithm 2 to minimize the DFA shown below.
31
a b c d e f
a b c d e f
c ✔ ✔
d ✔ ✔
e ✔ ✔
f ✔ ✔ ✔
Step 3 − We will try to mark the state pairs, with green colored check mark, transitively. If we input 1 to state
‘a’ and ‘f’, it will go to state ‘c’ and ‘f’ respectively. (c, f) is already marked, hence we will mark pair (a, f). Now,
we input 1 to state ‘b’ and ‘f’; it will go to state ‘d’ and ‘f’ respectively. (d, f) is already marked, hence we will
mark pair (b, f).
a b c d e f
32
a
c ✔ ✔
d ✔ ✔
e ✔ ✔
f ✔ ✔ ✔ ✔ ✔
After step 3, we have got state combinations {a, b} {c, d} {c, e} {d, e} that are unmarked.
We can recombine {c, d} {c, e} {d, e} into {c, d, e}
Hence we got two combined states as − {a, b} and {c, d, e}
So the final minimized DFA will contain three states {f}, {a, b} and {c, d, e}
Algorithm 3
Step 1 − All the states Q are divided in two partitions − final states and non-final states and are denoted by P0.
All the states in a partition are 0th equivalent. Take a counter k and initialize it with 0.
Step 2 − Increment k by 1. For each partition in Pk, divide the states in Pk into two partitions if they are k-
distinguishable. Two states within this partition X and Y are k-distinguishable if there is an input S such
that δ(X, S) and δ(Y, S) are (k-1)-distinguishable.
Step 3 − If Pk ≠ Pk-1, repeat Step 2, otherwise go to Step 4.
33
Step 4 − Combine kth equivalent sets and make them the new states of the reduced DFA.
Example
Let us consider the following DFA −
q δ(q,0) δ(q,1)
a b c
b a d
c e f
d e f
e e f
f f f
• P0 = {(c,d,e), (a,b,f)}
• P1 = {(c,d,e), (a,b),(f)}
• P2 = {(c,d,e), (a,b),(f)}
Hence, P1 = P2.
There are three states in the reduced DFA. The reduced DFA is as follows −
34
Q δ(q,0) δ(q,1)
UNIT II
SYNTAX ANALYSIS
In the syntax analysis phase, a compiler verifies whether or not the tokens generated by the lexical analyzer are
grouped according to the syntactic rules of the language. This is done by a parser. The parser obtains a string of
tokens from the lexical analyzer and verifies that the string can be the grammar for the source language. It
detects and reports any syntax errors and produces a parse tree from which intermediate code can be
35
generated.
Ambiguity
A grammar that produces more than one parse tree for some sentence is said to be ambiguous.
Eg- consider a grammar
S -> aS | Sa | a
Grammars are used to describe the syntax of a programming language. It specifies the structure of expression
and statements.
Types of grammar
• Type 0 grammar
• Type 1 grammar
• Type 2 grammar
• Type 3 grammar
36
The tasks of the Error Handling process are to detect each error, report it to the user, and then make some
recover strategy and implement them to handle error. During this whole process processing time of program
should not be slow. An Error is the blank entries in the symbol table.
Types or Sources of Error – There are two types of error: run-time and compile-time error:
1. A run-time error is an error which takes place during the execution of a program, and usually happens
because of adverse system parameters or invalid input data. The lack of sufficient memory to run an
application or a memory conflict with another program and logical error are example of this. Logic errors,
occur when executed code does not produce the expected result. Logic errors are best handled by
meticulous program debugging.
2. Compile-time errors rises at compile time, before execution of the program. Syntax error or missing file
reference that prevents the program from successfully compiling is the example of this.
Classification of Compile-time error –
1. Lexical : This includes misspellings of identifiers, keywords or operators
2. Syntactical : missing semicolon or unbalanced parenthesis
3. Semantical : incompatible value assignment or type mismatches between operator and operand
4. Logical : code not reachable, infinite loop.
Finding error or reporting an error – Viable-prefix is the property of a parser which allows early detection of
syntax errors.
• Goal: detection of an error as soon as possible without further consuming unnecessary input
• How: detect an error as soon as the prefix of the input does not match a prefix of any string in the
language.
• Example: for(;), this will report an error as for have two semicolons inside braces.
Error Recovery –
The basic requirement for the compiler is to simply stop and issue a message, and cease compilation. There are
some common recovery methods that are follows.
1. Panic mode recovery: This is the easiest way of error-recovery and also, it prevents the parser from
developing infinite loops while recovering error. The parser discards the input symbol one at a time until one
of the designated (like end, semicolon) set of synchronizing tokens (are typically the statement or
expression terminators) is found. This is adequate when the presence of multiple errors in same statement
is rare. Example: Consider the erroneous expression- (1 + + 2) + 3. Panic-mode recovery: Skip ahead to
next integer and then continue. Bison: use the special terminal error to describe how much input to skip.
E->int|E+E|(E)|error int|(error)
2. Phase level recovery: Perform local correction on the input to repair the error. But error correction is
difficult in this strategy.
3. Error productions: Some common errors are known to the compiler designers that may occur in the code.
Augmented grammars can also be used, as productions that generate erroneous constructs when these
errors are encountered. Example: write 5x instead of 5*x
4. Global correction: Its aim is to make as few changes as possible while converting an incorrect input string
to a valid string. This strategy is costly to implement.
Definition :CFG
where,
L-Language
G- Grammar
w - Input string
S - Start symbol
T - Terminal
Hence, CFL is a collection of input strings which are terminals, derived from the start
symbol of grammar on multiple steps.
Terminals are symbols from which strings are formed.
• Lowercase letters i.e., a, b, c.
• Operators i.e.,+,-,*·
• Punctuation symbols i.e., comma, parenthesis.
• Digits i.e. 0, 1, 2, · · · ,9.
• Boldface letters i.e., id, if.
38
Sentence and Sentential Form
Any α ∈ (N ∪ Σ)∗ derivable from the start symbol S is called a sentential form of the grammar. If α ∈ Σ∗, i.e. α ∈
L(G), then α is called a sentence of the grammar.
Parse Tree
Given a grammar G = (Σ, N, P, S), the parse tree of a sentential form x of the grammar is a rooted ordered tree
with the following properties:
39
o Example G:
list -> list + digit | list - digit |
digitdigit -> 0|1|2|3|4|5|6|7|8|9
40
Ambiguous Grammar
A grammar G is said to be ambiguous if there is a sentence x ∈ L(G) that has two distinct parse trees.
• Example. Suppose a grammar G that can not distinguish between lists and
digits: string → string + string | string - string |0|1|2|3|4|5|6|7|8|9
if-else Ambiguity
A statement of the form if(E1) if(E2) S2 else S3 can be parsed in two different ways. Normally we associate the
else to the nearest if.So it produces two different parse trees.
➢ if-else Modified
Consider the following production rules: S → if(E)S | if(E) ES else S | · · · ES → if(E) ES else ES | · · ·
We restrict the statement that can appeare in then-part. Now following statement has unique parse tree. if(E1)
if(E2) S2 else S3
41
RE= (a I b)*abb (set of strings ending with abb).
Grammar
Parsing
Parsing can be defined as top-down or bottom-up based on how the parse-tree is
constructed.
Top-Down Parsing
We have learnt in the last chapter that the top-down parsing technique parses the input, and
starts constructing a parse tree from the root node gradually moving down to the leaf nodes.
The types of top-down parsing are depicted below:
42
Bottom-up Parsing
Bottom-up parsing starts from the leaf nodes of a tree and works in upward direction till it
reaches the root node. Here, we start from a sentence and then apply production rules in
reverse manner in order to reach the start symbol. The image given below depicts the
bottom-up parsers available.
LL vs. LR parsing
LL parsing LR parsing
43
Starts with the root nonterminal on the stack. Ends with the root nonterminal on the stack.
Uses the stack for designating what is still to be Uses the stack for designating what is already seen.
expected.
Builds the parse tree top-down. Builds the parse tree bottom-up.
Continuously pops a nonterminal off the stack, Tries to recognize a right hand side on the stack,
and pushes the corresponding right hand side. pops it, and pushes the corresponding nonterminal.
Reads the terminals when it pops one off the Reads the terminals while it pushes them on the
stack. stack.
Pre-order traversal of the parse tree. Post-order traversal of the parse tree.
44
advances to the next input letter (i.e. ‘e’). The parser tries to expand non-terminal ‘X’ and
checks its production from the left (X → oa). It does not match with the next input symbol. So
the top-down parser backtracks to obtain the next production rule of X, (X → ea).
Now the parser matches all the input letters in an ordered manner. The string is accepted.
Predictive Parser
Predictive parser is a recursive descent parser, which has the capability to predict which
production is to be used to replace the input string. The predictive parser does not suffer from
backtracking.
To accomplish its tasks, the predictive parser uses a look-ahead pointer, which points to the
next input symbols. To make the parser back-tracking free, the predictive parser puts some
constraints on the grammar and accepts only a class of grammar known as LL(k) grammar.
Predictive parsing uses a stack and a parsing table to parse the input and generate a parse
tree. Both the stack and the input contains an end symbol $ to denote that the stack is empty
and the input is consumed. The parser refers to the parsing table to take any decision on the
input and stack element combination.
45
In recursive descent parsing, the parser may have more than one production to choose from
for a single instance of input, whereas in predictive parser, each step has at most one
production to choose. There might be instances where there is no production matching the
input string, making the parsing procedure to fail.
LL Parser
An LL Parser accepts LL grammar. LL grammar is a subset of context-free grammar but with
some restrictions to get the simplified version, in order to achieve easy implementation. LL
grammar can be implemented by means of both algorithms namely, recursive-descent or
table-driven.
LL parser is denoted as LL(k). The first L in LL(k) is parsing the input from left to right, the
second L in LL(k) stands for left-most derivation and k itself represents the number of look
aheads. Generally k = 1, so LL(k) may also be written as LL(1).
46
LL Parsing Algorithm
We may stick to deterministic LL(1) for parser explanation, as the size of table grows
exponentially with the value of k. Secondly, if a given grammar is not LL(1), then usually, it is
not LL(k), for any given k.
Given below is an algorithm for LL(1) Parsing:
Input:
string ω
parsing table M for grammar G
Output:
If ω is in L(G) then left-most derivation of ω,
error otherwise.
repeat
let X be the top stack symbol and a the symbol pointed by ip.
if X∈ V or $t
if X = a
POP X and advance ip.
else
error()
endif
else /* X is non-terminal */
if M[X,a] = X → Y1, Y2,... Yk
POP X
PUSH Yk, Yk-1,... Y1 /* Y1 on top */
Output the production X → Y1, Y2,... Yk
else
error()
endif
endif
until X = $ /* empty stack */
47
Rule 3:For a production rule X → Y1Y2Y3,
Follow Function-
Follow(α) is a set of terminal symbols that appear immediately to the right of α.
Rule-01:
Rule-02:
Note-01:
Note-02:
• Before calculating the first and follow functions, eliminate Left Recursion from the grammar, if present.
Note-03:
• We calculate the follow function of a non-terminal by looking where it is present on the RHS of a production rule.
48
Problem-01:
Calculate the first and follow functions for the given grammar-
S → aBDh
B → cC
C → bC / ∈
D → EF
E→g/∈
F→f/∈
Solution-
First Functions-
• First(S) = { a }
• First(B) = { c }
• First(C) = { b , ∈ }
• First(D) = { First(E) – ∈ } ∪ First(F) = { g , f , ∈ }
• First(E) = { g , ∈ }
• First(F) = { f , ∈ }
Follow Functions-
• Follow(S) = { $ }
• Follow(B) = { First(D) – ∈ } ∪ First(h) = { g , f , h }
• Follow(C) = Follow(B) = { g , f , h }
• Follow(D) = First(h) = { h }
• Follow(E) = { First(F) – ∈ } ∪ Follow(D) = { f , h }
• Follow(F) = Follow(D) = { h }
Problem-02:
Calculate the first and follow functions for the given grammar-
S→A
A → aB / Ad
B→b
49
C→g
Solution-
We have-
S→A
A → aBA’
A’ → dA’ / ∈
B→b
C→g
First Functions-
• First(S) = First(A) = { a }
• First(A) = { a }
• First(A’) = { d , ∈ }
• First(B) = { b }
• First(C) = { g }
Follow Functions-
• Follow(S) = { $ }
• Follow(A) = Follow(S) = { $ }
• Follow(A’) = Follow(A) = { $ }
• Follow(B) = { First(A’) – ∈ } ∪ Follow(A) = { d , $ }
• Follow(C) = NA
Problem-03:
Calculate the first and follow functions for the given grammar-
S → (L) / a
L → SL’
L’ → ,SL’ / ∈
50
Solution-
First Functions-
• First(S) = { ( , a }
• First(L) = First(S) = { ( , a }
• First(L’) = { , , ∈ }
Follow Functions-
Calculate the first and follow functions for the given grammar-
S → AaAb / BbBa
A→∈
B→∈
Solution-
First Functions-
Follow Functions-
• Follow(S) = { $ }
• Follow(A) = First(a) ∪ First(b) = { a , b }
• Follow(B) = First(b) ∪ First(a) = { a , b }
51
Left Recursion-
• A production of grammar is said to have left recursion if the leftmost variable of its RHS is
same as variable of its LHS.
• A grammar containing a production having left recursion is called as Left Recursive Grammar.
Example-
S → Sa / ∈
(Left Recursive Grammar)
Left recursion is eliminated by converting the grammar into a right recursive grammar.
If we have the left-recursive pair of productions-
A → Aα / β
(Left Recursive Grammar)
where β does not begin with an A.
Then, we can eliminate left recursion by replacing the pair of productions with-
A → βA’
A’ → αA’ / ∈
Problem-01:
Solution-
A’ → BdA’ / aA’ / ∈
B → bB’
B’ → eB’ / ∈
52
Problem-02:
Solution-
Problem-03
Solution-
Problem-04:
Solution-
53
The grammar after eliminating left recursion is-
S → (L) / a
L → SL’
L’ → ,SL’ / ∈
Problem-05:
Solution-
A → 0S1SA / ∈
Problem-06:
Solution-
Step-01:
A’ → aA’ / ∈
A’ → aA’ / ∈
B → Bb / Ab / d
54
Step-02:
A’ → aA’ / ∈
B → Bb / BaA’b / cA’b / d
Step-03:
Now, eliminating left recursion from the productions of B, we get the following grammar-
A → BaA’ / cA’
A’ → aA’ / ∈
B → cA’bB’ / dB’
B’ → bB’ / aA’bB’ / ∈
Left Factoring
• If a grammar contains two productions of
formS→ aα and S → aβ
it is not suitable for top down parsing without backtracking. Troubles of this form can
sometimes be removed from the grammar by a technique called the left factoring.
Example:
1)In the left factoring,
we replace { S→ aα, S→ aβ } by
{ S → aS', S'→ α, S'→ β }
2)Consider the grammar , G : S→iEtS | iEtSeS | a
E→b
Solution:
55
Example:
Consider the following grammar :
E → E+T | T
T→T*F | F
F → (E) | id
After eliminating left-recursion the grammar is
E → TE’
E’ → +TE’ |ε
T → FT’
T’ → *FT’ | εF
→ (E) | id
First( ) :
FIRST(E) = { ( , id}
FIRST(E’) ={+ ,ε}
FIRST(T) = { ( , id}
FIRST(T’) = {*, ε }
FIRST(F) = { ( , id }
Follow( ):
FOLLOW(E) = { $, ) }
FOLLOW(E’) = { $, ) }
FOLLOW(T) = { +, $, ) }
FOLLOW(T’) = { +, $, ) }
FOLLOW(F) = {+, * , $ , ) }
56
Conditions for a LL(1)Grammar:
• if β → t, then α does not derive any string beginning with a terminal in FOLLOW(A).
SHIFT-REDUCE PARSING
Shift-reduce parsing is a type of bottom-up parsing that attempts to construct a parse tree for
an input string beginning at the leaves (the bottom) and working up towards the root (the
top).
Example:
Consider the grammar:
S → aABe
A → Abc | b
B→d
The sentence to be recognized is abbcde.
57
REDUCTION (LEFTMOST) RIGHTMOST DERIVATION
abbcde (A → b) S→ aABe
aAde (B → d) → aAbcde
Handles:
A handle of a string is a substring that matches the right side of a production, and whose
reduction to the non-terminal on the left side of the production represents one step along the
reverse of a rightmost derivation.
Example:
E → E+E
E → E*EE
→ (E)
E → id
E →E+E
→ E+E*E
→ E+E*id3
→ E+id2*id3
→id 1+id2*id3
58
In the above derivation the underlined substrings are calledhandles.
Handle pruning:
(i.e.) ifwis a sentence or string of the grammar at hand, thenw= y n, where yn is then th right-
sentinel form of some rightmost derivation.
59
Stack implementation of shift-reduce parsing :
$E +id2*id3 $ shift
$ E+ id2*id3 $ shift
$ E+E*E $ reduce by E→ E *E
$E $ accept
• shift – The next input symbol is shifted onto the top of the stack.
• reduce – The parser replaces the handle within a stack with a non-terminal.
• accept – The parser announces successful completion of parsing.
• error – The parser discovers that a syntax error has occurred and calls an error
recoveryroutine.
2. Reduce-reduce conflict: The parser cannot decide which of several reductions to make.
1. Shift-reduce conflict:
Example:
61
Stack Input Action Stack Input Action
$ E+E *id $ Reduce by $E+E *id $ Shift
E→E+E
$E *id $ Shift $E+E* id $ Shift
2. Reduce-reduce conflict:
Consider the
grammar:M → R+R |
R+c | R
R→c
$c +c $ Reduce by $c +c $ Reduce by
R→c R→c
$R +c $ Shift $R +c $ Shift
$ R+ c$ Shift $ R+ c$ Shift
62
Viable prefixes:
➢ a is a viable prefix of the grammar if there iswsuch that awis a right sentinel form.
➢ The set of prefixes of right sentinel forms that can appear on the stack of a shift-reduce
parserare called viable prefixes.
➢ The set of viable prefixes is a regular language.
LR PARSERS
An efficient bottom-up syntax analysis technique that can be used to parse a large class
of CFG is called LR(k) parsing. The ‘L’ is for left-to-right scanning of the input, the ‘R’ for
constructing a rightmost derivation in reverse, and the ‘k’ for the number of input
symbols.When ‘k’ is omitted, it is assumed to be 1.
Advantages of LR parsing:
✓ It recognizes virtually all programming language constructs for which CFG can be
written.
✓ It is an efficient non-backtracking shift-reduce parsing method.
✓ A grammar that can be parsed using LR method is a proper superset of a grammar
thatcan be parsed with predictive parser.
✓ It detects a syntactic error as soon as possible.
Drawbacks of LR method:
1. SLR- Simple LR
▪ Easiest to implement, least powerful.
2. CLR- Canonical LR
▪ Most powerful, most expensive.
3. LALR- Look-Ahead LR
▪ Intermediate in size and cost between the other two methods.
INPUT a1 ai an $
… …
Sm LR parsing program 63
Xm
Sm-1
Xm-1
OUTPUT
STACK
64
It consists of : an input, an output, a stack, a driver program, and a parsing table that has
twoparts (actionandgoto).
➢ The parsing program reads characters from an input buffer one at a time.
➢ The program uses a stack to store a string of the form s 0X1s1X2s2…Xmsm, where sm is on
top. Each Xi is a grammar symbol and each si is a state.
Action: The parsing program determines s m, the state currently on top of stack, and ai,
the current input symbol. It then consultsaction[s m,ai] in the action table which can have one of
four values :
Goto: The function goto takes a state and grammar symbol as arguments and produces a state.
LR Parsing algorithm:
Method: Initially, the parser has s0 on its stack, where s0 is the initial state, andw$ in the input
buffer. The parser then executes the following program :
end
end
elseerror( )
end
LR(O) items:
A →.XYZ A
→ X.YZA →
XY.Z A →
XYZ.
Closure operation:
If I is a set of items for a grammar G, then closure(I) is the set of items constructed from I
by the two rules:
Goto operation:
Goto(I, X) is defined to be the closure of the set of all items [A→ aX . β] such that
[A→ a . Xβ] is in I.
Method:
1. Construct C = {I0, I1, …. In}, the collection of sets of LR(0) items for G’.
2. Stateiis constructed from I i.. The parsing functions for stateiare determined as follows:
(a) If [A→a·aβ] is in Ii and goto(Ii,a) = Ij, then setaction[i,a] to “shift j”. Hereamust be
terminal.
(b) If [A→a·] is in Ii , then setaction[i,a] to “reduce A→a” for allain FOLLOW(A).
(c) If [S’→S.] is in Ii, then setaction[i,$] to “accept”.
If any conflicting actions are generated by the above rules, we say grammar is not SLR(1).
3. Thegototransitions for stateiare constructed for all non-terminals A using the rule:
Ifgoto(I i,A) = Ij, thengoto[i,A] =j.
4. All entries not defined by rules (2) and (3) are made “error”
5. The initial state of the parser is the one constructed from the set of items containing
[S’→.S].
T → T * F | FF
→ (E) | id
67
Augmented grammar :
E’ → E
E→E+T
E→T
T → T * FT
→F
F → (E)
F → id
: E’ →.E
E →.E + T
E →.T
T →.T * FT
→.F
F →.(E)
F →.id
68
GOTO ( I0 , E)I1 : E’ → E.
E → E.+ T
69
GOTO ( I4 , id )
I5 : F → id.
GOTO ( I6 , T )
GOTO ( I0 , T) I9 : E → E + T.
I2 : E → T. T → T.* F
T → T.* F
GOTO ( I6 , F )I3 :
GOTO ( I0 , F)I3 T → F.
: T → F.
GOTO ( I6 , ( )
I4 : F → (.E )
I4 : F → (.E) F →.(E)
E →.E + TE F →.id
→.T
GOTO ( I0 , id )
GOTO ( I7 , F )I10 :
I5 : F → id. T → T * F.
F →.(E)
70
F GOTO ( I8 , ) )
I11 :F → ( E).
→
. GOTO( I8 , + )
i
d I6 : E → E +.T
T →.T * FT
→.F
GOTO ( I7 , id ) F →.( E )
I5 : F → id. F →.id
GOTO ( I4 , F)I3
: T → F.
71
GOTO ( I4 , ( )
I4 : F → (.E)
E →.E + T E
→.T
T →.T * F T
→.F
F →.(E)
FOLLOW (E) = { $ , ) , +)
F → id
FOLLOW (T) = { $ , + , ) , * }
FOOLOW (F) = { * , + , ) , $ }
ACTION GOTO
id + * ( ) $ E T F
IO s5 s4 1 2 3
I1 s6 ACC
I2 r2 s7 r2 r2
I3 r4 r4 r4 r4
I4 s5 s4 8 2 3
I5 r6 r6 r6 r6
I6 s5 s4 9 3
I7 s5 s4 10
I8 s6 s11
I9 r1 s7 r1 r1
I1O r3 r3 r3 r3
I11 r5 r5 r5 r5
72
Stack implementation:
73
STACK INPUT ACTION
0 id + id * id $ GOTO ( I0 , id ) = s5 ;shift
0F3 + id * id $ GOTO ( I0 , F ) = 3
GOTO ( I3 , + ) = r4 ;reduceby T → F
0T2 + id * id $ GOTO ( I0 , T ) = 2
GOTO ( I2 , + ) = r2 ;reduceby E → T
0E1 + id * id $ GOTO ( I0 , E ) = 1
GOTO ( I1 , + ) = s6 ;shift
0 E 1 + 6 id 5 * id $ GOTO ( I5 , * ) = r6 ;reduceby F → id
0E1+6F3 * id $ GOTO ( I6 , F ) = 3
GOTO ( I3 , * ) = r4 ;reduceby T → F
0E1+6T9 * id $ GOTO ( I6 , T ) = 9
GOTO ( I9 , * ) = s7 ;shift
0 E 1 + 6 T 9 * 7 id 5 $ GOTO ( I5 , $ ) = r6 ;reduceby F → id
0 E 1 + 6 T 9 * 7 F 10 $ GOTO ( I7 , F ) = 10
GOTO ( I10 , $ ) = r3 ;reduceby T → T * F
0E1+6T9 $ GOTO ( I6 , T ) = 9
GOTO ( I9 , $ ) = r1 ;reduceby E → E + T
0E1 $ GOTO ( I0 , E ) = 1
GOTO ( I1 , $ ) =accept
74
LR(O) Items
An LR(O) item of a grammar G is a production of G with a dot at some position of the body.
(eg.)
A ---> •XYZ
A ---> XeYZ
A ---> XYeZ
A ---> XYZ•
One collection of set of LR(O) items, called the canonical LR(O) collection, provides finite
automaton that is used to make parsing decisions. Such an automaton is called an LR(O)
automaton.
75
Error handling routines are used to restart the parser to continue its process even after the
occurrence of error.
• Tokens following the error get discarded to restart the parser.
• The YACC command uses a special token name error, for error handling. The token is placed
at places where error might occur so that it provides a recovery subroutine.
• To prevent subsequent occurrence of errors, the parser remains in error state until it
processes three tokens following an error.
• The input is discarded and no message is produced, if an error occurred while the
parser remains in error state.
(eg.) stat : error ‘;’
• The above rule tells the parser that when there is an error, it should ignore the
token and all following tokens until it finds the next semicolon.
• It discards all the tokens after the error and before the next semicolon.
• Once semicolon is found, the rule is reduced by parser and cleanup action
associated with that rule will be performed.
Providing for error correction
The input errors can be corrected by entering a line in the data stream again.
input : error ‘\n’
{
printf (“Reenter last line:”);
}
input
{
$$ = $4;
};
The YACC statement, yyerrok is used to indicate that error recovery is complete.
This statement leaves the error state and begins processing normally.
input : error ‘\n’
yyerrok;
printf (“Reenter last line:”);
}
input
76
{
$$ = $4;
};
Clearing the Lookahead token
• When an error occurs, the lookahead token becomes the token at which the error was
detected.
• The lookahead token must be changed if the error recovery action includes code to find the
correct place to start processing again.
• To clear the lookahead token, the error-recovery action issues the following statement:
yyclearin;
To assist in error handling, macros can be placed in YACC actions.
Macros for error handling
YACC
YACC in compiler design, also known as Yet Another Compiler Compiler is used to produce the source code
of the syntactic analyzer of the language produced by Look Ahead Left to Right LALR (1) parser generator. As an
input, a parser generator takes input a syntax specification and produces a procedure for recognizing that language
as output.
77
Stephen C. Johnson developed YACC in compiler design in the early 1970s. Initially, the YACC was written in the B
programming language and was soon rewritten in C
The parts of YACC program are divided into three sections:
/* definitions */
....
%%
/* rules */
....
%%
/* auxiliary routines */
....
Definitions: these include the header files and any token information used in the syntax. These are located at the
top of the input file. Here, the tokens are defined using a modulus sign. In the YACC, numbers are automatically
assigned for tokens.
examples:
%token ID
{% #include <stdio.h> %}
Rules: The rules are defined between %% and %%. These rules define the actions for when the token is scanned
and are executed when a token matches the grammar.
Auxiliary Routines: Auxiliary routines contain the function required in the rules section. This Auxiliary section
includes the main() function, where the yyparse() function is always called.
This yyparse() function plays the role of reading the token, performing actions and then returning to the main() after
the execution or in the case of an error.
78
Workings of YACC
YACC in compiler design is set to work in C programming language along with its parser generator.
If called with the –d option in the command line, YACC produces y.tab.h with all its specific definitions.
Example of YACC Program
%{
#include <ctype.h>
#include <stdio.h>
int yylex();
void yyerror();
int tmp=0;
%}
%token num
%%
| num {$$=$1;};
79
%%
void yyerror(){
printf("Incorrect\n");
tmp=1;
int main(){
yyparse();
%{
#include <stdlib.h>
#include <stdio.h>
#include "y.tab.h"
%}
%%
[\t] ;
[\n] return 0;
return num;
. return yytext[0];
%%
80
UNIT III SYNTAX DIRECTED TRANSLATION &
INTERMEDIATE CODE GENERATION
1
2
3
4
5
6
7
8
9
ARRAY REFERENCES IN ARITHMETIC EXPRESSIONS
10
11
12
13
TYPE CHECKING
14
15
16
17
BACKPATCHING
Backpatching is a technique used in compiler design to update previously generated code with the correct
target addresses or labels. It helps establish the connections between control flow constructs, such as
conditionals and loops, by setting the appropriate target addresses during code generation.
Backpatching can be used to generate a boolean expressions program and the flow of
control statements in a single pass. Label handling for Boolean statements in jumping code
is done by non-terminal B’s synthesized true list and false list attributes. If B is true, the label
to which control should go should be added to the list of a jump or conditional jump
instructions in B.truelist. The set of instructions known as B.falselist is what ultimately
receives the label to which control is sent when B is false. When the program is generated
for B, the jumps to true and false exist as well as the label field are left empty. These early
jumps are found in the lists B.truelist and B.falselist, respectively.
• Makelist (i): This function creates a new list that contains only the index i, which
corresponds to an instruction in the array. Additionally, the function returns a pointer to the
newly generated list.
• Merge (p1, p2): The Merge function concatenates the lists pointed to by p1 and p2,
resulting in a single list that combines the jumps from both lists. The function then returns a
pointer to the concatenated list.
• Backpatch (p, i): The Backpatch function is responsible for inserting the index i as the
target label for each instruction on the list pointed to by p. This operation ensures that the
jumps in the list are redirected to the appropriate target location represented by the index i.
18
By employing a translation technique, code generation for Boolean expressions can be
achieved through bottom-up parsing. In grammar, a non-terminal symbol M triggers a
semantic action that retrieves the index of the subsequent instruction to be generated at the
appropriate moment.
To illustrate this, let’s consider the concept of backpatching using boolean expressions and
a production rules table. Backpatching involves updating previously generated code with the
correct target addresses or labels.
19
Step 2: We have to find the TAC(Three address code) for the given expression using
backpatching:
A < B OR C < D AND P < Q
20
Flow-of-Control Statements:
(1)S→ifEthenS
(2) |ifEthenSelseS
(3) |whileEdoS
(4) |beginLend
(5) |A
(6) L→L ; S
(7) |S
(1)S→ifEthenM1 S1 NelseM 2 S2
21
{backpatch(E.truelist,M1.quad);
backpatch(E.falselist,M2.quad);
S.nextlist: =merge(S1.nextlist,merge(N.nextlist,S2.nextlist)) }
(2)N→ɛ{N.nextlist: =makelist(nextquad);
emit(‘goto_’) }
(3)M→ɛ{M.quad: =nextquad}
(6)S→beginLend{S.nextlist: =L.nextlist}
(7)S→A{S.nextlist: =nil}
The statement followingL1 in order of execution is the beginning of S. Thus the L1.nextlistlist is
backpatched to the beginning of the code for S, which is given byM.quad.
(9)L→S{L.nextlist: =S.nextlist}
22
UNIT IV RUN-TIME ENVIRONMENT AND CODE
GENERATION
23
24
25
26
27
28
29
INTERMEDIATE CODE GENERATION
INTRODUCTION
The front end translates a source program into an intermediate representation from which
the back end generates target code.
1. Retargeting is facilitated. That is, a compiler for a different machine can be created by
attaching a back end for the new machine to an existing front end.
INTERMEDIATE LANGUAGES
• Syntax tree
• Postfix notation
The semantic rules for generating three-address code from common programming language
constructs are similar to those for constructing syntax trees or for generating postfix notation.
Graphical Representations:
Syntax tree:
A syntax tree depicts the natural hierarchical structure of a source program. Adag
(Directed Acyclic Graph)gives the same information but in a more compact way because
common subexpressions are identified. A syntax tree and dag for the assignment statementa : =
b * - c + b * - care as follows:
30
assign assign
a + a +
* * *
b uminus b uminus b uminus
c c c
Postfix notation:
Syntax-directed definition:
Syntax trees for assignment statements are produced by the syntax-directed definition.
Non-terminal S generates an assignment statement. The two binary operators + and * are
examples of the full operator set in a typical language. Operator associativities and precedences
are the usual ones, even though they have not been put into the grammar. This definition
constructs the tree from the input a : = b * - c + b* - c.
31
The tokenidhas an attributeplacethat points to the symbol-table entry for the identifier.
A symbol-table entry can be found from an attributeid.name, representing the lexeme associated
with that occurrence ofid.If the lexical analyzer holds all lexemes in a single array of
characters, then attributenamemight be the index of the first character of the lexeme.
Two representations of the syntax tree are as follows. In (a) each node is represented as a
record with a field for its operator and additional fields for pointers to its children. In (b), nodes
are allocated from an array of records and the index or position of the node serves as the pointer
to the node. All the nodes in the syntax tree can be visited by following pointers, starting from
the root at position 10.
aaaaaaaaaaaaa
assign 0 id b
1 id c
id a
2 uminus
2 1
3 * 0 2
+
4 id b
5 id c
* *
6 uminus 5
id b id b
7 * 4 6
uminus uminus 8 + 3 7
9 id a
id c id c
10 assign 9 8
(a) (b)
Three-Address Code:
x : = yopz
t1 : = y * z
t2 : = x + t1
➢ The use of names for the intermediate values computed by a program allows three-
address code to be easily rearranged – unlike postfix notation.
Three-address code corresponding to the syntax tree and dag given above
t1 : = - c t1 : = -c
t2 : = b * t1 t2 : = b * t1
t3 : = - c t5 : = t2 + t2
t4 : = b * t3 a : = t5
t5 : = t2 + t4
a : = t5
(a) Code for the syntax tree (b) Code for the dag
The reason for the term “three-address code” is that each statement usually contains three
addresses, two for the operands and one for the result.
2. Assignment instructions of the formx : =opy, whereopis a unary operation. Essential unary
operations include unary minus, logical negation, shift operators, and conversion operators
that, for example, convert a fixed-point number to a floating-point number.
3.Copy statementsof the formx : = ywhere the value ofyis assigned tox.
4. The unconditional jump goto L. The three-address statement with label L is the next to be
executed.
33
5. Conditional jumps such asifx relop ygoto L. This instruction applies a relational operator (
<, =, >=, etc. ) toxandy, and executes the statement with label L next ifxstands in relation
34
relop to y. If not, the three-address statement following ifx relop ygoto L is executed next,
as in the usual sequence.
6.param xandcall p, nfor procedure calls andreturn y, where y representing a returned value
is optional. For example,
param x1
param x2
...
param xn
call p,n
generated as part of a call of the procedure p(x1, x2, …. ,xn ).
When three-address code is generated, temporary names are made up for the interior
nodes of a syntax tree. For example,id : =Econsists of code to evaluateEinto some temporary
t, followed by the assignmentid.place: =t.
35
S id : = E S.code : = E.code||gen(id.place ‘:=’ E.place)
E E1 + E2 E.place := newtemp;
E.code := E1.code||E 2.code||gen(E.place ‘:=’ E 1.place ‘+’ E2.place)
E E1 * E2 E.place := newtemp;
E.code := E1.code || E2.code || gen(E.place ‘:=’ E1.place ‘*’ E2.place)
E - E1 E.place := newtemp;
E.code := E1.code || gen(E.place ‘:=’ ‘uminus’ E1.place)
E ( E1 ) E.place : = E1.place;
E.code : = E1.code
E id E.place : = id.place;
E.code : = ‘ ‘
36
Semantic rules generating code for a while statement
S.begin:
E.code
S1.code
goto S.begin
S.after:. . .
37
Three such representations are:
38
➢ Quadruples
➢ Triples
➢ Indirect triples
Quadruples:
➢ A quadruple is a record structure with four fields, which are,op, arg1, arg2andresult.
➢ Theopfield contains an internal code for the operator. The three-address statementx : =
y op zis represented by placingyinarg1,zinarg2andxinresult.
➢ The contents of fields arg1, arg2 and result are normally pointers to the symbol-table
entries for the names represented by these fields. If so, temporary names must be entered
into the symbol table as they are created.
Triples:
➢ To avoid entering temporary names into the symbol table, we might refer to a temporary
value by the position of the statement that computes it.
➢ If we do so, three-address statements can be represented by records with only three fields:
op, arg1andarg2.
➢ The fieldsarg1andarg2, for the arguments ofop, are either pointers to the symbol table
or pointers into the triple structure ( for temporary values ).
➢ Since three fields are used, this intermediate code format is known astriples.
39
A ternary operation like x[i] : = y requires two entries in the triple structure as shown as below
while x : = y[i] is naturally represented as two operations.
Indirect Triples:
➢ For example, let us use an array statement to list pointers to triples in the desired order.
Then the triples shown above might be represented as follows:
DECLARATIONS
40
Declarations in a Procedure:
The syntax of languages such as C, Pascal and Fortran, allows all the declarations in a
single procedure to be processed as a group. In this case, a global variable, sayoffset, can keep
track of the next available relative address.
➢ Before the first declaration is considered,offsetis set to 0. As each new name is seen ,
that name is entered in the symbol table with offset equal to the current value ofoffset,
andoffsetis incremented by the width of the data object denoted by that name.
➢ The procedureenter( name, type, offset) creates a symbol-table entry forname, gives its
typetypeand relative addressoffsetin its data area.
➢ The width of an array is obtained by multiplying the width of each element by the
number of elements in the array. The width of each pointer is assumed to be 4.
P D { offset : = 0 }
D D;D
41
Keeping Track of Scope Information:
P D
One possible implementation of a symbol table is a linked list of entries for names.
A new symbol table is created when a procedure declarationD proc idD 1;Sis seen,
and entries for the declarations in D1 are created in the new table. The new table points back to
the symbol table of the enclosing procedure; the name represented by id itself is local to the
enclosing procedure. The only change from the treatment of variable declarations is that the
procedureenteris told which symbol table to make an entry in.
For example, consider the symbol tables for proceduresreadarray, exchange, and
quicksortpointing back to that for the containing proceduresort, consisting of the entire
program. Sincepartitionis declared withinquicksort, its table points to that ofquicksort.
sort
nil header
a
x
readarray to readarray
exchange to exchange
quicksort
partition
h d
i
j
42
The semantic rules are defined in terms of the following operations:
1. mktable(previous)creates a new symbol table and returns a pointer to the new table. The
argumentpreviouspoints to a previously created symbol table, presumably that for the
enclosing procedure.
2. enter(table, name, type, offset)creates a new entry for namenamein the symbol table pointed
to bytable.Again,enterplaces typetypeand relative addressoffsetin fields within the entry.
3. addwidth(table, width)records the cumulative width of all the entries in table in the header
associated with this symbol table.
4. enterproc(table, name, newtable)creates a new entry for procedurenamein the symbol table
pointed to bytable. The argumentnewtablepoints to the symbol table for this procedure
name.
M ɛ { t : = mktable (nil);
push (t,tblptr); push (0,offset) }
D D1 ; D2
➢ The top element of stackoffsetis the next available relative address for a local of the
current procedure.
A→BC {action A}
43
are done beforeaction A at the end of the production occurs. Hence, the action associated
with the marker M is the first to be done.
44
➢ The action for nonterminal M initializes stacktblptrwith a symbol table for the
outermost scope, created by operationmktable(nil).The action also pushes relative
address 0 onto stack offset.
➢ For each variable declarationid:T, an entry is created foridin the current symbol table.
The top of stack offset is incremented by T.width.
➢ When the action on the right side ofD proc id; ND1; Soccurs, the width of all
declarations generated by D1 is on the top of stack offset; it is recorded usingaddwidth.
Stackstblptrandoffsetare then popped.
At this point, the name of the enclosed procedure is entered into the symbol table of its
enclosing procedure.
ASSIGNMENT STATEMENTS
Suppose that the context in which an assignment appears is given by the following grammar.
P→M D
M→ɛ
N→ɛ
Nonterminal P becomes the new start symbol when these productions are added to those in the
translation scheme shown below.
45
E→id { p : = lookup (id.name);
ifp≠nilthen
E.place : = p
elseerror }
➢ Temporaries can be reused by changingnewtemp. The code generated by the rules for E
→E 1 + E2 has the general form:
evaluate E1 into t1
evaluate E2 into t2
t : = t1 + t2
➢ The lifetimes of these temporaries are nested like matching pairs of balanced parentheses.
statement value of c
0
$0 := a * b 1
$1 := c * d 2
$0 := $0 + $1 1
$1 := e * f 2
$0 := $0 - $1 1
x := $0 0
Elements of an array can be accessed quickly if the elements are stored in a block of
consecutive locations. If the width of each array element isw, then theith element of array A
begins in location
wherelowis the lower bound on the subscript andbaseis the relative address of the storage
allocated for the array. That is,baseis the relative address of A[low].
46
The expression can be partially evaluated at compile time if it is rewritten as
ixw+ (base–lowxw)
The subexpressionc = base – lowxwcan be evaluated when the declaration of the array is seen.
We assume that c is saved in the symbol table entry for A , so the relative address of A[i] is
obtained by simply addingixwtoc.
➢ Row-major (row-by-row)
➢ Column-major (column-by-column)
A[ 1 1 ] A[11]
first column
first row A[ 1,2 ] A[21]
A[ 1 3 ] A [ 1,2 ]
A[ 2,1 ] A [ 2,2 ] second column
In the case of row-major form, the relative address of A[ i1 , i2] can be calculated by the formula
where,low 1 andlow 2 are the lower bounds on the values ofi 1 andi 2 andn 2 is the number of
values thati 2 can take. That is, ifhigh 2 is the upper bound on the value ofi 2, thenn 2 = high2 –
low2 +1.
Assuming that i1 and i2 are the only values that are known at compile time, we can rewrite the
above expression as
47
The Translation Scheme for Addressing Array Elements :
(1) S L:=E
(2) E E+E
(3) E (E)
(4) E L
(5) L→Elist]
(6) L id
(7) Elist Elist , E
(8) Elist id[E
We generate a normal assignment ifLis a simple name, and an indexed assignment into the
location denoted byLotherwise :
When an array referenceLis reduced toE, we want ther-value ofL. Therefore we use indexing
to obtain the contents of the locationL.place[L.offset] :
49
Elist.place : = t;
Elist.ndim : = m}
Elist.place : = E.place;
Elist.ndim : =1 }
Consider the grammar for assignment statements as above, but suppose there are two
types – real and integer , with integers converted to reals when necessary. We have another
attributeE.type, whose value is eitherrealorinteger. The semantic rule forE.typeassociated
with the productionE E + Eis :
E E + E{E.type: =
ifE 1.type = integerand
E2.type = integertheninteger
elsereal}
The entire semantic rule forE E + Eand most of the other productions must be
modified to generate, when necessary, three-address statements of the form x : = inttoreal y,
whose effect is to convert integer y to a real of equal value, called x.
E.place := newtemp;
ifE 1.type = integerandE 2.type = integerthen begin
emit( E.place ‘: =’ E1.place ‘int +’ E2.place);
E.type : = integer
end
else ifE 1.type = realandE 2.type = realthen begin
emit( E.place ‘: =’ E1.place‘real +’E 2.place);
E.type : = real
end
else ifE 1.type = integerandE 2.type = realthen begin
u : = newtemp;
emit( u ‘: =’‘inttoreal’E 1.place);
emit( E.place ‘: =’ u‘ real +’E 2.place);
E.type : = real
end
else ifE 1.type = realandE 2.type =integerthen begin
u : = newtemp;
emit( u ‘: =’‘inttoreal’E 2.place);
emit( E.place ‘: =’ E1.place ‘real +’ u);
E.type : = real
end
50
else
E.type : = type_error;
51
For example, for the input x : = y + i * j
assumingxandyhave typereal, and i and j have typeinteger, the output would look like
t1 : = i int* j
t3 : = inttoreal t1
t2 : = y real+ t3
x : = t2
BOOLEAN EXPRESSIONS
Boolean expressions have two primary purposes. They are used to compute logical
values, but more often they are used as conditional expressions in statements that alter the flow
of control, such as if-then-else, or while-do statements.
Boolean expressions are composed of the boolean operators (and, or,andnot) applied
to elements that are boolean variables or relational expressions. Relational expressions are of the
formE 1 relopE 2, where E1 and E2 are arithmetic expressions.
There are two principal methods of representing the value of a boolean expression. They are :
➢ To implement boolean expressions byflow of control, that is, representing the value of a
boolean expression by a position reached in a program. This method is particularly
convenient in implementing the boolean expressions in flow-of-control statements, such
as the if-then and while-do statements.
Numerical Representation
Here, 1 denotes true and 0 denotes false. Expressions will be evaluated completely from
left to right, in a manner similar to arithmetic expressions.
For example :
52
➢ A relational expression such as a < b is equivalent to the conditional statement
if a < b then 1 else 0
53
which can be translated into the three-address code sequence (again, we arbitrarily start
statement numbers at 100) :
Short-Circuit Code:
We can also translate a boolean expression into three-address code without generating
code for any of the boolean operators and without having the code necessarily evaluate the entire
expression. This style of evaluation is sometimes called “short-circuit” or “jumping” code. It is
possible to evaluate boolean expressions without generating code for the boolean operatorsand,
or,andnotif we represent the value of an expression by a position in the code sequence.
54
105 : t2 : = 0 112 : t4 : = t2 and t3
55
Flow-of-Control Statements
We now consider the translation of boolean expressions into three-address code in the
context of if-then, if-then-else, and while-do statements such as those generated by the following
grammar:
S→ifEthenS 1
|ifEthenS 1 elseS 2
| whileEdoS 1
➢ E.true is the label to which control flows if E is true, and E.false is the label to which
control flows if E is false.
➢ The semantic rules for translating a flow-of-control statement S allow control to flow
from the translation S.code to the three-address instruction immediately following
S.code.
➢ S.next is a label that is attached to the first three-address instruction to be executed after
the code for S.
toE.true
E.code
toE.false
E.code toE.true E.true: S1.code
E.true: toE.false
S1.code gotoS.next
E.false:
S2.code
E.false: ...
S.next: ...
S.begin: E.code :. . .
E
.
f
E.true: S1.code
a
l
gotoS.begin s
e
56
t
t
(c) while-do
r
57
Syntax-directed definition for flow-of-control statements
59
E1.false : = E.false;
E.code : = E1.code
CASE STATEMENTS
switchexpression
begin
casevalue:statement
casevalue:statement
...
casevalue:statement
default :statement
end
60
labels, with the label of the statement for value j in the entry of the table with offset j -
imin and the label for the default in entries not filled otherwise. To perform switch,
61
evaluate the expression to obtain the value ofj, check the value is within range and
transfer to the table entry at offset j-imin .
switchE
begin
caseV 1 :S 1
caseV 2 :S 2
...
caseV n-1 :S n-1
default :S n
end
This case statement is translated into intermediate code that has the following form :
code to evaluateEinto t
goto test
L1 : code forS 1
goto next
L2 : code forS 2
goto next
. . .
Ln-1 : code forS n-1
goto next
Ln : code forS n
goto next
test : if t =V 1 goto L1
if t =V 2 goto L2
. . .
if t =V n-1 goto Ln-1
goto Ln
next :
➢ As eachcasekeyword occurs, a new label L is created and entered into the symbol table.
i
A pointer to this symbol-table entry and the valueV i of case constant are placed on a
62
stack (used only to store cases).
63
➢ Each statementcaseV i : Si is processed by emitting the newly created label Li, followed
by the code forS i , followed by the jumpgoto next.
➢ Then when the keywordendterminating the body of the switch is found, the code can be
generated for the n-way branch. Reading the pointer-value pairs on the case stack from
the bottom to the top, we can generate a sequence of three-address statements of the form
caseV 1 L1
caseV 2 L2
...
caseV n-1 Ln-1
case t Ln
label next
where t is the name holding the value of the selector expressionE, and L n is the label for
the default statement.
64
65
CODE GENERATION
The final phase in compiler model is the code generator. It takes as input an intermediate
representation of the source program and produces as output an equivalent target program. The
code generation techniques presented below can be used whether or not an optimizing phase
occurs before code generation.
symbol
table
• Prior to code generation, the front end must be scanned, parsed and translated into
intermediate representation along with necessary type checking. Therefore, input to code
generation is assumed to be error-free.
2. Target program:
• The output of the code generator is the target program. The output may be :
a. Absolute machine language
66
- It can be placed in a fixed memory location and can be executed immediately.
67
b. Relocatable machine language
- It allows subprograms to be compiled separately.
c. Assembly language
- Code generation is made easier.
3. Memory management:
• Names in the source program are mapped to addresses of data objects in run-time
memory by the front end and code generator.
• It makes use of symbol table, that is, a name in a three-address statement refers to a
symbol-table entry for the name.
4. Instruction selection:
• The instructions of target machine should be complete and uniform.
• Instruction speeds and machine idioms are important factors when efficiency of target
program is considered.
• The quality of the generated code is determined by its speed and size.
• The former statement can be translated into the latter statement as shown below:
5. Register allocation
• Instructions involving register operands are shorter and faster than those involving
operands in memory.
69
➢ Register assignment– the specific register that a variable will reside in is
picked.
6. Evaluation order
• The order in which the computations are performed can affect the efficiency of the
target code. Some computation orders require fewer registers to hold intermediate
results than others.
TARGET MACHINE
• Familiarity with the target machine and its instruction set is a prerequisite for designing a
good code generator.
• The target computer is a byte-addressable machine with 4 bytes to a word.
• It hasngeneral-purpose registers, R 0, R1, . . . , Rn-1.
• It has two-address instructions of the form:
op source, destination
where,opis an op-code, andsourceanddestinationare data fields.
absoluteM M 1
registerR R 0
indexed c(R)c+contents(R) 1
indirect indexed*c(R)contents(c+ 1
contents(R))
literal#c c1
70
• For example : MOV R 0, M stores contents of Register R0 into memory location M ;
MOV 4(R0), M stores the valuecontents(4+contents(R 0)) into M.
Instruction costs :
• Instruction cost = 1+cost for source and destination address modes. This cost corresponds
to the length of the instruction.
• Address modes involving registers have cost zero.
• Address modes involving memory location or literal have cost one.
• Instruction length should be minimized if space is important. Doing so also minimizes the
time taken to fetch and perform the instruction.
For example : MOV R0, R1 copies the contents of register R0 into R1. It has cost one,
since it occupies only one word of memory.
• The three-address statementa : = b + ccan be implemented by many different instruction
sequences :
i) MOV b, R0
ADD c, R0 cost = 6
MOV R0, a
ii) MOV b, a
ADD c, a cost = 6
• In order to generate good code for target machine, we must utilize its addressing
capabilities efficiently.
72
Static allocation
GOTOcallee.code_area/*It transfers control to the target code for the called procedure */
where,
callee.static_area– Address of the activation record
callee.code_area– Address of the first instruction for called procedure
#here+ 20 – Literal return address which is the address of the instruction following GOTO.
GOTO*callee.static_area
This transfers control to the address saved at the beginning of the activation record.
The statement HALT is the final instruction that returns control to the operating system.
Stack allocation
Static allocation can become stack allocation by using relative addresses for storage in
activation records. In stack allocation, the position of activation record is stored in register so
words in activation records can be accessed as offsets from the value in this register.
Initialization of stack:
74
where,
caller.recordsize– size of the activation record
#here+ 16 – address of the instruction following theGOTO
Basic Blocks
Output:A list of basic blocks with each three-address statement in exactly one block
Method:
1. We first determine the set ofleaders, the first statements of basic blocks. The rules
we use are of the following:
a. The first statement is a leader.
b. Any statement that is the target of a conditional or unconditional goto is a
leader.
c. Any statement that immediately follows a goto or conditional goto statement
is a leader.
2. For each leader, its basic block consists of the leader and all statements up to but not
including the next leader or the end of the program.
75
• Consider the following source code for dot product of two vectors a and b of length 20
begin
prod :=0;
i:=1;
do begin
i :=i+1;
end
while i <= 20
end
(2) i := 1
(3) t1 := 4* i
(5) t3 := 4* i
(7) t5 := t2*t4
(8) t6 := prod+t5
(9) prod := t6
(10) t7 := i+1
(11) i := t7
A number of transformations can be applied to a basic block without changing the set of
expressions computed by the block. Two important classes of transformation are :
• Structure-preserving transformations
• Algebraic transformations
a:=b+c a:=b+c
b:=a–d b:=a-d
c:=b+c c:=b+c
d:=a–d d:=b
Since the second and fourth expressions compute the same expression, the basic block can be
transformed as above.
b) Dead-code elimination:
Supposexis dead, that is, never subsequently used, at the point where the statement x : =
y + z appears in a basic block. Then this statement may be safely removed without changing
the value of the basic block.
d) Interchange of statements:
t1 : = b + c
t2 : = x + y
We can interchange the two statements without affecting the value of the block if and
only if neitherxnoryist 1 and neitherbnorcist 2.
2. Algebraic transformations:
Algebraic transformations can be used to change the set of expressions computed by a basic
block into an algebraically equivalent set.
Examples:
i) x : = x + 0 or x : = x * 1 can be eliminated from a basic block without changing the set of
77
expressions it computes.
ii) The exponential statement x : = y * * 2 can be replaced by x : = y * y.
78
Flow Graphs
• Flow graph is a directed graph containing the flow-of-control information for the set of
basic blocks making up a program.
• The nodes of the flow graph are basic blocks. It has a distinguished initial node.
• E.g.: Flow graph for the vector dot product is given as follows:
prod : = 0 B1
i:=1
t1 : = 4 * i
t2 : = a [ t1 ]
t3 : = 4 * i
B2
t4 : = b [ t3 ]
t5 : = t2 * t4
t6 : = prod + t5
prod : = t6
t7 : = i + 1
i : = t7
if i <= 20 goto B2
Loops
NEXT-USE INFORMATION
• If the name in a register is no longer needed, then we remove the name from the register
and the register can be used to store some other names.
79
Input:Basic block B of three-address statements
Symbol Table:
y Live i
z Live i
• A code generator generates target code for a sequence of three- address statements and
effectively uses registers to store operands of the statements.
(or)
(or)
ADD Rj, Ri
• A register descriptor is used to keep track of what is currently in each registers. The
register descriptors show that initially all the registers are empty.
80
• An address descriptor stores the location where the current value of the name can be
found at run time.
81
A code-generation algorithm:
The algorithm takes as input a sequence of three-address statements constituting a basic block.
For each three-address statement of the form x : = y op z,perform the following actions:
1. Invoke a functiongetregto determine the location L where the result of the computation y op
z should be stored.
2. Consult the address descriptor for y to determine y’, the current location of y. Prefer the
register for y’ if the value of y is currently both in memory and a register. If the value of y is
not already in L, generate the instructionMOV y’ , Lto place a copy of y in L.
4. If the current values of y or z have no next uses, are not live on exit from the block, and are in
registers, alter the register descriptor to indicate that, after execution of x : = y op z , those
registers will no longer contain y or z.
• The assignment d : = (a-b) + (a-c) + (a-c) might be translated into the following three-
address code sequence:
t:=a–b
u:=a–c
v:=t+u
d:=v+u
with d live at the end.
Register empty
82
Generating Code for Indexed Assignments
The table shows the code sequences generated for the indexed assignment statements
a : = b [ i ]anda [ i ] : = b
The table shows the code sequences generated for the pointer assignments
a : = *pand*p : = a
a : = *p MOV *Rp, a 2
*p : = a MOV a, *Rp 2
Statement Code
x : = y +z MOV y, R0
if x < 0 goto z ADD z, R0
MOV R0,x
CJ< z
• A DAG for a basic block is adirected acyclic graphwith the following labels on nodes:
1. Leaves are labeled by unique identifiers, either variable names or constants.
2. Interior nodes are labeled by an operator symbol.
3. Nodes are also optionally given a sequence of identifiers for labels to store the
computed values.
• DAGs are useful data structures for implementing transformations on basic blocks.
• It gives a picture of how the value computed by a statement is used in subsequent
statements.
83
• It provides a good way of determining common sub - expressions.
84
Algorithm for construction of DAG
Output:A DAG for the basic block containing the following information:
1. A label for each node. For leaves, the label is an identifier. For interior nodes, an
operator symbol.
2. For each node a list of attached identifiers to hold the computed values.
Case (i) x : = y OP z
Case (ii) x : = OP y
Case (iii) x : = y
Method:
Step 2:For the case(i), create a node(OP) whose left child is node(y) and right child is
For case(ii), determine whether there is node(OP) with one child node(y). If not create such
a node.
Step 3:Delete x from the list of identifiers for node(x). Append x to the list of attached
1. t1 := 4* i
2. t2 := a[t1]
3. t3 := 4* i
4. t4 := b[t3]
5. t5 := t2*t4
6. t6 := prod+t5
7. prod := t6
8. t7 := i+1
9. i := t7
10. if i<=20 goto (1)
85
Stages in DAG Construction
86
87
Application of DAGs:
88
GENERATING CODE FROM DAGs
The advantage of generating code for a basic block from its dag representation is that,
from a dag we can easily see how to rearrange the order of the final computation sequence than
we can starting from a linear sequence of three-address statements or quadruples.
MOV a , R0
ADD b , R0
MOV c , R1
ADD d , R1
MOV R0 , t1
MOV e , R0
SUB R1 , R0
MOV t1 , R1
SUB R0 , R1
MOV R1 , t4
t2 : = c + d
t3 : = e – t2
t1 : = a + b
t4 : = t1 – t3
MOV c , R0
ADD d , R0
MOV a , R0
SUB R0 , R1
MOV a , R0
ADD b , R0
SUB R1 , R0
MOV R0 , t4
The heuristic ordering algorithm attempts to make the evaluation of a node immediately follow
the evaluation of its leftmost argument.
Algorithm:
1
*
2 + - 3
4
*
5 - +
8
6 + 7 c d 11 e 12
a b
9 10
Initially, the only node with no unlisted parents is 1 so set n=1 at line (2) and list 1 at line (3).
Now, the left argument of 1, which is 2, has its parents listed, so we list 2 and set n=2 at line (6).
Now, at line (4) we find the leftmost child of 2, which is 6, has an unlisted parent 5. Thus we
select a new n at line (2), and node 3 is the only candidate. We list 3 and proceed down its left
chain, listing 4, 5 and 6. This leaves only 8 among the interior nodes so we list that.
90
Code sequence:
t8 : = d + e
t6 : = a + b
t5 : = t6 – c
t4 : = t5 * t8
t3 : = t4 – e
t2 : = t6 + t4
t1 : = t2 * t3
This will yield an optimal code for the DAG on machine whatever be the number of registers.
1. Contiguous Evaluation
The dynamic programming algorithm partitions the problem of generating op-timal code
for an expression into the subproblems of generating optimal code for the subexpressions
of the given expression. As a simple example, consider an expression E of the form E1 +
E . An optimal program for E is formed by combining optimal programs for E1 and E , in
2 2
one or the other order, followed by code to evaluate the operator +. The subproblems of
generating optimal code for E1 and E are solved similarly.
2
91
We say a program P evaluates a tree T contiguously if it first evaluates those subtrees
of T that need to be computed into memory. Then, it evaluates the remainder of T either in
the order Ti, T , and then the root, or in the order T , Ti, and then the root, in either case
2 2
using the previously computed values from memory whenever necessary. As an example of
noncontiguous evaluation, P might first evaluate part of Ti leaving the value in a register
(instead of memory), next evaluate T , and then return to evaluate the rest of Ti.
2
The contiguous evaluation property defined above ensures that for any ex-pression tree T
there always exists an optimal program that consists of optimal programs for subtrees of the
root, followed by an instruction to evaluate the root. This property allows us to use a
dynamic programming algorithm to generate an optimal program for T.
The dynamic programming algorithm proceeds in three phases (suppose the target machine
has r registers):
1. Compute bottom-up for each node n of the expression tree T an array C of costs, in
which the zth component C[i] is the optimal cost of computing the subtree S rooted at n
into a register, assuming i registers are available for the computation, for 1 < i < r.
2. Traverse T, using the cost vectors to determine which subtrees of T must be computed
into memory.
Traverse each tree using the cost vectors and associated instructions to generate the final
target code. The code for the subtrees computed into memory locations is generated first.
Each of these phases can be implemented to run in time linearly proportional to the size of
the expression tree.
The cost of computing a node n includes whatever loads and stores are necessary to
evaluate S in the given number of registers. It also includes the cost of computing the
operator at the root of S. The zeroth component of the cost vector is the optimal cost of
computing the subtree S into memory. The contiguous evaluation property ensures that an
optimal program for S can be generated by considering combinations of optimal programs
only for the subtrees of the root of S. This restriction reduces the number of cases that need
to be considered.
92
Consider a machine having two registers RO and Rl, and the following instructions, each of
unit cost:
LD Ri, Kj // Ri = Mj
op Ri, Ri, Ri // Ri = Ri Op Rj
op Ri, Ri, Mi // Ri = Ri Op Kj
LD Ri, Ri // Ri = Ri
ST Hi, Ri // Mi = Rj
Let us apply the dynamic programming algorithm to generate optimal code for the syntax
tree in Fig 8.26. In the first phase, we compute the cost vectors shown at each node. To
illustrate this cost computation, consider the cost vector at the leaf a. C[0], the cost of
computing a into memory, is 0 since it is already there. C[l], the cost of computing a into a
register, is 1 since we can load it into a register with the instruction LD RO, a. C[2], the
cost of loading a into a register with two registers available, is the same as that with one
register available. The cost vector at leaf a is therefore (0,1,1).
Consider the cost vector at the root. We first determine the minimum cost of computing
the root with one and two registers available. The machine instruction ADD RO, RO,
M matches the root, because the root is labeled with the operator +. Using this instruction,
the minimum cost of evaluating the root with one register available is the minimum cost of
computing its right subtree into memory, plus the minimum cost of computing its left
subtree into the register, plus 1 for the instruction. No other way exists. The cost vectors at
the right and left children of the root show that the minimum cost of computing the root
with one register available is 5 + 2 + 1 = 8.
93
Now consider the minimum cost of evaluating the root with two registers available. Three
cases arise depending on which instruction is used to compute the root and in what order
the left and right subtrees of the root are evaluated.
Compute the left subtree with two registers available into register RO, compute the right
subtree with one register available into register Rl, and use the instruction ADD RO,
RO, Rl to compute the root. This sequence has cost 2 + 5 + 1 = 8.
Compute the right subtree with two registers available into Rl, compute
the left subtree with one register available into RO, and use the instruction ADD RO,
RO, Rl. This sequence has cost 4 + 2 + 1 = 7.
Compute the right subtree into memory location M, compute the left sub-tree with two
registers available into register RO, and use the instruction ADD RO, RO, M. This
sequence has cost 5 + 2 + 1 = 8.
The minimum cost of computing the root into memory is determined by adding one to
the minimum cost of computing the root with all registers avail-able; that is, we compute
the root into a register and then store the result. The cost vector at the root is therefore
(8,8,7).
94
UNIT-V CODE OPTIMIZATION
INTRODUCTION
➢ The code produced by the straight forward compiling algorithms can often be made to run
faster or take less space, or both. This improvement is achieved by program transformations
that are traditionally called optimizations. Compilers that apply code-improving
transformations are called optimizing compilers.
• Machine independent optimizations are program transformations that improve the target code
without taking into consideration any properties of the target machine.
• Machine dependant optimizations are based on register allocation and utilization of special
machine-instruction sequences.
Organization for an Optimizing Compiler:
➢ Function-Preserving Transformations
• There are a number of ways in which a compiler can improve a program without
changing the function it computes.
• The transformations
96
• Frequently, a program will include several calculations of the same value, such as an
offset in an array. Some of the duplicate calculations cannot be avoided by the
programmer because they lie below the level of detail accessible within the source
language.
The above code can be optimized using the common sub-expression elimination as
t1: = 4*i
t2: = a [t1]
t3: = 4*j
t5 : = n
t6: = b [t1] +t5
The common sub expression t4: =4*i is eliminated as its computation is alre ady in t1. And
value of i is not been changed from definition to use.
➢ Copy Propagation:
• Assignments of the form f : = g called copy statements, or copies for short. The idea
behind the copy-propagation transformation is to use g for f, whenever possible after the
copy statement f: = g. Copy propagation means use of one variable instead of another.
This may not appear to be an improvement, but as we shall see it gives us an opportunity
to eliminate x.
• For example:
x=Pi;
……
A=x*r*r;
A=Pi*r*r;
➢ Dead-Code Eliminations:
97
• A variable is live at a point in a program if its value can be used subsequently; otherwise,
it is dead at that point. A related idea is dead or useless code, statements that compute
98
values that never get used. While the programmer is unlikely to introduce any dead code
intentionally, it may appear as the result of previous transformations. An optimization can
be done by eliminating dead code.
Example:
i=0;
if(i=1)
{
a=b+5;
}
Here, ‘if’ statement is dead code because this condition will never get satisfied.
➢ Constant folding:
• We can eliminate both the test and printing from the object code. More generally,
deducing at compile time that the value of an expression is a constant and using the
constant instead is known as constant folding.
• One advantage of copy propagation is that it often turns the copy statement into dead
code.
✓ For example,
a=3.14157/2 can be replaced by
a=1.570 there by eliminating a division operation.
➢ Loop Optimizations:
• We now give a brief introduction to a very important place for optimizations, namely
loops, especially the inner loops where programs tend to spend the bulk of their time. The
running time of a program may be improved if we decrease the number of instructions in
an inner loop, even if we increase the amount of code outside that loop.
• Three techniques are important for loop optimization:
➢ Code Motion:
• An important modification that decreases the amount of code in a loop is code motion.
This transformation takes an expression that yields the same result independent of the
number of times a loop is executed ( a loop-invariant computation) and places the
expression before the loop. Note that the notion “before the loop” assumes the existence
of an entry for the loop. For example, evaluation of limit-2 is a loop-invariant
computation in the following while-statement:
99
t= limit-2;
while (i<=t) /* statement does not change limit or t */
➢ Induction Variables :
• Loops are usually processed inside out. For example consider the loop around B3.
• Note that the values of j and t 4 remain in lock-step; every time the value of j decreases by
1, that of t4 decreases by 4 because 4*j is assigned to t4. Such identifiers are called
induction variables.
• When there are two or more induction variables in a loop, it may be possible to get rid of
all but one, by the process of induction-variable elimination. For the inner loop around
B3 in Fig. we cannot get rid of either j or t4 completely; t4 is used in B3 and j in B4.
However, we can illustrate reduction in strength and illustrate a part of the process of
induction-variable elimination. Eventually j will be eliminated when the outer loop of B2
- B5 is considered.
Example:
As the relationship t4:=4*j surely holds after such an assignment to t4 in Fig. and t4 is not
changed elsewhere in the inner loop around B3, it follows that just after the statement
j:=j-1 the relationship t4:= 4*j-4 must hold. We may therefore replace the assignment t4:=
4*j by t4:= t4-4. The only problem is that t4 does not have a value when we enter block B3
for the first time. Since we must maintain the relationship t4=4*j on entry to the block B3,
we place an initializations of t4 at the end of the block where j itself is
before after
100
initialized, shown by the dashed addition to block B1 in second Fig.
101
• The replacement of a multiplication by a subtraction will speed up the object code if
multiplication takes more time than addition or subtraction, as is the case on many
machines.
➢ Reduction In Strength:
✓ Structure-Preserving Transformations
✓ Algebraic Transformations
Structure-Preserving Transformations:
Common sub expressions need not be computed over and over again. Instead they can be
computed once and kept in store from where it’s referenced when encountered aga in – of course
providing the variable values in the expression still remain constant.
Example:
a: =b+c
b: =a-d
c: =b+c
d: =a-d
The 2nd and 4th statements compute the same expression: b+c and a-d
a: = b+c
b: = a-d
102
c: = a
d: = b
103
➢ Dead code elimination:
It’s possible that a large amount of dead (useless) code may exist in the program. This
might be especially caused when introducing variables and procedures as part of construction or
error-correction of a program – once declared and defined, one forgets to remove them in case
they serve no purpose. Eliminating these will definitely optimize the code.
• Two statements
t1:=b+c
t2:=x+y
can be interchanged or reordered in its computation in the basic block when value of t 1
does not affect the value of t2.
Algebraic Transformations:
a :=b+c
e :=c+d+b
a :=b+c
t :=c+d
e :=t+b
• Example:
104
x:=y**2 can be replaced by a cheaper statement x:=y*y
105
• The compiler writer should examine the language carefully to determine what
rearrangements of computations are permitted, since computer arithmetic does not always
obey the algebraic identities of mathematics. Thus, a compiler may evaluate x*y-x*z as
x*(y-z) but it may not evaluate a+(b-c) as (a+b)-c.
Dominators:
In a flow graph, a node d dominates node n, if every path from initial node of the flow
graph to n goes through d. This will be denoted byd dom n. Every initial node dominates all the
remaining nodes in the flow graph and the entry of a loop dominates all nodes in the loop.
Similarly every node dominates itself.
Example:
106
• The way of presenting dominator information is in a tree, called the dominator tree in
which the initial node is the root.
• The parent of each other node is its immediate dominator.
• Each node d dominates only its descendents in the tree.
• The existence of dominator tree follows from a property of dominators; each node has a
unique immediate dominator in that is the last dominator of n on any path from the initial
node to n.
• In terms of the dom relation, the immediate dominator m has the property is d=!n and d
dom n, then d dom m.
D(1)={1}
D(2)={1,2}
D(3)={1,3}
D(4)={1,3,4}
D(5)={1,3,4,5}
D(6)={1,3,4,6}
D(7)={1,3,4,7}
D(8)={1,3,4,7,8}
D(9)={1,3,4,7,8,9}
107
D(10)={1,3,4,7,8,10}
108
Natural Loop:
• One application of dominator information is in determining the loops of a flow graph suitable
for improvement.
✓ A loop must have a single entry point, called the header. This entry point-dominates all
nodes in the loop, or it would not be the sole entry to the loop.
✓ There must be at least one way to iterate the loop(i.e.)at least one path back to the header.
• One way to find all the loops in a flow graph is to search for edges in the flow graph whose
heads dominate their tails. If a→b is an edge, b is the head and a is the tail. These types of
edges are called as back edges.
✓ Example:
7→4 4 DOM 7
10→7 7 DOM 10
4→3
8→3
9→1
Output:The set loop consisting of all nodes in the natural loop n→d.
Method:Beginning with node n, we consider each node m*d that we know is in loop, to make
sure that m’s predecessors are also placed in loop. Each node in loop, except for d, is placed once
on stack, so its predecessors will be examined. Note that because d is put in the loop initially, we
never examine its predecessors, and thus find only those nodes that reach n without going
through d.
Procedureinsert(m);
ifm is not inloopthen begin
loop:=loopU {m};
pushmontostack
end;
109
stack: = empty;
110
loop: = {d};
insert(n);
whilestackis not emptydo begin
popm, the first element ofstack, offstack;
foreach predecessorpofmdoinsert(p)
end
Inner loop:
• If we use the natural loops as “the loops”, then we have the useful property that unless
two loops have the same header, they are either disjointed or one is entirely contained in
the other. Thus, neglecting loops with the same header for the moment, we have a natural
notion of inner loop: one that contains no other loop.
• When two natural loops have the same header, but neither is nested within the other, they
are combined and treated as a single loop.
Pre-Headers:
• The pre-header has only the header as successor, and all edges which formerly entered
the header of L from outside L instead enter the pre-header.
• Initially the pre-header is empty, but transformations on L may place statements in it.
header pre-header
loop L
he der
loop L
• Reducible flow graphs are special flow graphs, for which several code optimization
transformations are especially easy to perform, loops are unambiguously defined,
dominators can be easily calculated, data flow analysis problems can also be solved
efficiently.
111
reducible.
112
• The most important properties of reducible flow graphs are that there are no jumps into
the middle of loops from outside; the only entry to a loop is through its header.
• Definition:
A flow graph G is reducible if and only if we can partition the edges into two disjoint
groups,forwardedges andbackedges, with the following properties.
✓ The forward edges from an acyclic graph in which every node can be reached from initial
node of G.
✓ The back edges consist only of edges where heads dominate theirs tails.
• If we know the relation DOM for a flow graph, we can find and remove all the back
edges.
• If the forward edges form an acyclic graph, then we can say the flow graph reducible.
• In the above example remove the five back edges 4→3, 7→4, 8→3, 9→1 and 10→7
whose heads dominate their tails, the remaining graph is acyclic.
• The key property of reducible flow graphs for loop analysis is that in such flow graphs
every set of nodes that we would informally regard as a loop must contain a back edge.
PEEPHOLE OPTIMIZATION
✓ Redundant-instructions elimination
✓ Flow-of-control optimizations
✓ Algebraic simplifications
✓ Use of machine idioms
✓ Unreachable Code
113
Redundant Loads And Stores:
we can delete instructions (2) because whenever (2) is executed. (1) will ensure that the value of
ais already in register R 0.If (2) had a label we could not be sure that (1) was always executed
immediately before (2) and so we could not remove (2).
Unreachable Code:
#define debug 0
….
If ( debug ) {
If debug =1 goto L2
goto L2
• One obvious peephole optimization is to eliminate jumps over jumps .Thus no matter what
the value ofdebug; (a) can be replaced by:
If debug≠1 goto L2
115
If debug≠0 goto L2
• As the argument of the first statement of (c) evaluates to a constant true, it can be replaced by
goto L2. Then all the statement that print debugging aids are manifestly unreachable and
can be eliminated one at a time.
Flows-Of-Control Optimizations:
• The unnecessary jumps can be eliminated in either the intermediate code or the target code
by the following types of peephole optimizations. We can replace the jump sequence
goto L1
….
L1: gotoL2
by the sequence
goto L2
….
L1: goto L2
• If there are now no jumps to L1, then it may be possible to eliminate the statement L1:goto
L2 provided it is preceded by an unconditional jump .Similarly, the sequence
if a < b goto L1
….
L1: goto L2
can be replaced by
If a < b goto L2
….
L1: goto L2
• Finally, suppose there is only one jump to L1 and L1 is preceded by an unconditional goto.
Then the sequence
goto L1
116
……..
117
L1: if a < b goto L2
• May be replaced by
If a < b goto L2
goto L3
…….
• While the number of instructions in (1) and (2) is the same, we sometimes skip the
unconditional jump in (2), but never in (1).Thus (2) is superior to (1) in execution time
Algebraic Simplification:
• There is no end to the amount of algebraic simplification that can be attempted through
peephole optimization. Only a few algebraic identities occur frequently enough that it is
worth considering implementing them .For example, statements such as
x := x+0
Or
x := x * 1
• Are often produced by straightforward intermediate code-generation algorithms, and they can
be eliminated easily through peephole optimization.
Reduction in Strength:
• Reduction in strength replaces expensive operations by equivalent cheaper ones on the target
machine. Certain machine instructions are considerably cheaper than others and can often be
used as special cases of more expensive operators.
• For example, x² is invariably cheaper to implement as x*x than as a call to an exponentiation
routine. Fixed-point multiplication or division by a power of two is cheaper to implement as
a shift. Floating-point division by a constant can be implemented as multiplication by a
constant, which may be cheaper.
X2 →X*X
• The target machine may have hardware instructions to implement certain specific operations
efficiently. For example, some machines have auto-increment and auto-decrement addressing
modes. These add or subtract one from an operand before or after using its value.
• The use of these modes greatly improves the quality of code when pushing or popping a
118
stack, as in parameter passing. These modes can also be used in code for statements like i :
=i+1.
119
i:=i+1→i++
i:=i-1→i- -
• In order to do code optimization and a good job of code generation , compiler needs to
collect information about the program as a whole and to distribute this information to
each block in the flow graph.
This equation can be read as “ the information at the end of a statement is either generated
within the statement , or enters at the beginning and is not killed as control flows through
the statement.”
• The details of how data-flow equations are set and solved depend on three factors.
✓ The notions of generating and killing depend on the desired information, i.e., on the data
flow analysis problem to be solved. Moreover, for some problems, instead of proceeding
along with flow of control and defining out[s] in terms of in[s], we need to proceed
backwards and define in[s] in terms of out[s].
✓ Since data flows along control paths, data-flow analysis is affected by the constructs in a
program. In fact, when we write out[s] we implicitly assume that there is unique end
point where control leaves the statement; in general, equations are set up at the level of
basic blocks rather than statements, because blocks do have unique end points.
✓ There are subtleties that go along with such statements as procedure calls, assignments
through pointer variables, and even assignments to array variables.
• Within a basic block, we talk of the point between two adjacent statements, as well as the
point before the first statement and after the last. Thus, block B1 has four points: one
before any of the assignments and one after each of the three assignments.
120
B1
d1 : i :=m-1
d2: j :=n
d3 = u1
B2
d4 : I := i+1
B3
d5: j := j-1
B4
B5 B6
d6 :a :=u2
• Now let us take a global view and consider all the points in all the blocks. A path from p 1
to pn is a sequence of points p1, p2,….,pn such that for each i between 1 and n-1, either
✓ P i is the point immediately preceding a statement and pi+1 is the point immediately
following that statement in the same block, or
✓ P i is the end of some block and pi+1 is the beginning of a successor block.
Reaching definitions:
• These statements certainly define a value for x, and they are referred to asunambiguous
definitions of x. There are certain kinds of statements that may define a value for x; they
are calledambiguousdefinitions. The most usual forms ofambiguousdefinitions of x
are:
✓ An assignment through a pointer that could refer to x. For example, the assignment *q: =
y is a definition of x if it is possible that q points to x. we must assume that an assignment
through a pointer is a definition of every variable.
121
• We say a definition d reaches a point p if there is a path from the point immediately
following d to p, such that d is not “killed” along that path. Thus a point can be reached
122
by an unambiguous definition and an ambiguous definition of the same variable
appearing later along one path.
• Flow graphs for control flow constructs such as do-while statements have a useful
property: there is a single beginning point at which control enters and a single end point
that control leaves from when execution of the statement is over. We exploit this property
when we talk of the definitions reaching the beginning and the end of statements with the
following syntax.
E id + id| id
• Expressions in this language are similar to those in the intermediate code, but the flow
graphs for statements have restricted forms.
S1
S1
If E goto s1
S2
S1 S2 If E goto s1
S1 ; S2
• We define a portion of a flow graph called aregionto be a set of nodes N that includes a
header, which dominates all other nodes in the region. All edges between nodes in N are
in the region, except for some that enter the header.
• The portion of flow graph corresponding to a statement S is a region that obeys the
123
further restriction that control can flow to just one outside block when it leaves the
region.
124
• We say that the beginning points of the dummy blocks at the entry and exit of a
statement’s region are the beginning and end points, respectively, of the statement. The
equations are inductive, or syntax-directed, definition of the sets in[S], out[S], gen[S],
and kill[S] for all statements S.
• gen[S] is the set of definitions “generated” by S while kill[S] is the set of definitions
that never reach the end of S.
• Consider the following data-flow equations for reaching definitions :
i)
S d:a:=b+c
gen [S] = { d }
kill [S] = Da – { d }
out [S] = gen [S] U ( in[S] – kill[S] )
• Observe the rules for a single assignment of variable a. Surely that assignment is a
definition of a, say d. Thus
Gen[S]={d}
Kill[S] = Da – {d}
ii )
S S1
S2
gen[S]=gen[S2] U (gen[S1]-kill[S2])
Kill[S] = kill[S2] U (kill[S1] – gen[S2])
in [S1] = in [S]
in [S2] = out [S1]
125
out [S] = out [S2]
126
• Under what circumstances is definition d generated by S=S 1; S2? First of all, if it is
generated by S2, then it is surely generated by S. if d is generated by S 1, it will reach the
end of S provided it is not killed by S2. Thus, we write
gen[S]=gen[S2] U (gen[S1]-kill[S2])
• However, there are other kinds of data-flow information, such as the reaching-definitions
problem. It turns out that in is an inherited attribute, and out is a synthesized attribute
depending on in. we intend that in[S] be the set of definitions reaching the beginning of
S, taking into account the flow of control throughout the entire program, including
statements outside of S or within which S is nested.
• The set out[S] is defined similarly for the end of s. it is important to note the distinction
between out[S] and gen[S]. The latter is the set of definitions that reach the end of S
without following paths outside S.
• Considering if-statement we have conservatively assumed that control can follow either
branch, a definition reaches the beginning of S1 or S2 exactly when it reaches the
beginning of S.
• If a definition reaches the end of S if and only if it reaches the end of one or both sub
statements; i.e,
Out[S]=out[S1] U out[S2]
Representation of sets:
• Sets of definitions, such as gen[S] and kill[S], can be represented compactly using bit
vectors. We assign a number to each definition of interest in the flow graph. Then bit
127
vector representing a set of definitions will have 1 in position I if and only if the
definition numbered I is in the set.
• The number of definition statement can be taken as the index of statement in an array
holding pointers to statements. However, not all definitions may be of interest during
global data-flow analysis. Therefore the number of definitions of interest will typically be
recorded in a separate table.
• A bit vector representation for sets also allows set operations to be implemented
efficiently. The union and intersection of two sets can be implemented by logical or and
logical and, respectively, basic operations in most systems-oriented programming
languages. The difference A-B of sets A and B can be implemented by taking the
complement of B and then using logical and to compute A .
• Space for data-flow information can be traded for time, by saving information only at
certain points and, as needed, recomputing information at intervening points. Basic
blocks are usually treated as a unit during global flow analysis, with attention restricted to
only those points that are the beginnings of blocks.
• Since there are usually many more points than blocks, restricting our effort to blocks is a
significant savings. When needed, the reaching definitions for all points in a block can be
calculated from the reaching definitions for the beginning of a block.
Use-definition chains:
Evaluation order:
• The techniques for conserving space during attribute evaluation, also apply to the
computation of data-flow information using specifications. Specifically, the only
constraint on the evaluation order for the gen, kill, in and out sets for statements is that
imposed by dependencies between these sets. Having chosen an evaluation order, we are
free to release the space for a set after all uses of it have occurred.
• Earlier circular dependencies between attributes were not allowed, but we have seen that
data-flow equations may have circular dependencies.
• Global transformations are not substitute for local transformations; both must be performed.
• The available expressions data-flow problem discussed in the last section allows us to
determine if an expression at point p in a flow graph is a common sub-expression. The
following algorithm formalizes the intuitive ideas presented for eliminating common sub-
expressions.
METHOD: For every statement s of the form x := y+z6 such that y+z is available at the
beginning of block and neither y nor r z is defined prior to statement s in that block,
do the following.
✓ To discover the evaluations of y+z that reach s’s block, we follow flow graph
edges, searching backward from s’s block. However, we do not go through
any block that evaluates y+z. The last evaluation of y+z in each block
encountered is an evaluation of y+z that reaches s.
Copy propagation:
• Various algorithms introduce copy statements such as x :=copies may also be generated
directly by the intermediate code generator, although most of these involve temporaries
local to one block and can be removed by the dag construction. We may substitute y for x
in all these places, provided the following conditions are met every such use u of x.
• On every path from s to including paths that go through u several times, there are no
129
assignments to y.
• Condition (1) can be checked using ud-changing information. We shall set up a new data-
flow analysis problem in which in[B] is the set of copies s: x:=y such that every path
from initial node to the beginning of B contains the statement s, and subsequent to the
last occurrence of s, there are no assignments to y.
• Ud-chains can be used to detect those computations in a loop that are loop-invariant, that
is, whose value does not change as long as control stays within the loop. Loop is a region
consisting of set of blocks with a header that dominates all the other blocks, so the only
way to enter the loop is through the header.
• Having found the invariant statements within a loop, we can apply to some of them an
optimization known as code motion, in which the statements are moved to pre-header of
the loop. The following three conditions ensure that code motion does not change what
the program computes. Consider s: x: =y+z.
✓ The block containing s dominates all exit nodes of the loop, where an exit of a loop is a
node with a successor not in the loop.
• However, our methods deal with variables that are incremented or decremented zero, one,
two, or more times as we go around a loop. The number of changes to an induction
variable may even differ at different iterations.
130
• A common situation is one in which an induction variable, say i, indexes an array, and
some other induction variable, say t, whose value is a linear function of i, is the actual
offset used to access the array. Often, the only use made of i is in the test for loop
termination. We can then get rid of i by replacing its test by one on t.
• We shall look for basic induction variables, which are those variables i whose only
assignments within loop L are of the form i := i+c or i-c, where c is a constant.
Leveraging machine-learning (ML) techniques for compiler optimizations has been widely used.
Generic view of machine learning in compilers. In stages (a) to (c) the model is learned from input
examples; in stage (d) the model is deployed and predicts a heuristic value for a new program. In stage
(a) the compiler writer investigates data structures that may be useful which are then summarized as
feature vectors in stage (b). In stage (c) training examples consisting of feature vectors and the correct
answer are passed to a machine learning tool. In stage (d) the learned model is inserted in the
131
compiler.
2) improving code performance with register allocation (regalloc). Both optimizations are available in the LLVM repository,
and have been deployed in production.
The DLVM consists of a virtual instruction set, control flow graph and data flow representation. Passes are functions that
traverse the intermediate representation of a program, either producing useful results as analyses of the program (analysis
passes) or mutating the program for differentiation and optimizations (transform passes).
132