CD All Units
CD All Units
1.1 INTRODUCTION
A compiler is a translator that converts the high-level language into the machine language.
High-level language is written by a developer and machine language can be understood by
the processor. The compiler also makes the end code efficient which is optimized for
execution time and memory space. The main purpose of compiler is to change the code
written in one language without changing the meaning of the program and also used to show
errors to the programmer.
When you execute a program which is written in HLL programming language then it
executes into two parts. In the first part, the source program compiled and translated into the
object program (low level language).
In the second part, object program translated into the target program through the Interpreter.
Features of Compilers
• Correctness
Page 1 of 26
• Speed of compilation
• Preserve the correct the meaning of the code
• The speed of the target code
• Recognize legal and illegal program constructs
• Good error reporting/handling
• Code debugging help
Types of Compiler
Following are the different types of Compiler:
• Single Pass Compilers
• Two Pass Compilers
• Multipass Compilers
The Two pass compiler method also simplifies the retargeting process. It also allows multiple
front ends.
Page 2 of 26
Multipass Compilers
The multipass compiler processes the source code it divided a large program into multiple
small programs and process them. It develops multiple intermediate codes. All of these
multipass take the output of the previous phase as an input. So it requires less memory.
Tasks of Compiler
Main tasks performed by the Compiler are:
• Breaks up the up the source program into pieces and impose grammatical structure on
them
• Allows to construct the desired target program from the intermediate representation
and also create the symbol table.
• Compiles source code and detects errors in it
• Manage storage of all variables and codes.
• Support for separate compilation
• Read, analyze the entire program, and translate to semantically equivalent
• Translating the source code into object code depending upon the type of machine
History of Compiler
Important Landmark of Compiler's history are as follows:
• The "compiler" word was first used in the early 1950s by Grace Murray Hopper
• The first compiler was build by John Backum and his group between 1954 and 1957
at IBM
• COBOL was the first programming Languages which was compiled on multiple
platforms in 1960
• The study of the scanning and parsing issues were pursued in the 1960s and 1970s to
provide a complete solution.
Page 3 of 26
Steps for Language processing systems
Before knowing about the concept of compilers, we first need to understand a few other tools
which work with compilers.
• Preprocessor: The preprocessor a tool which produces input for Compiler. It deals
with macro processing, language extension, etc.
• Interpreter: An interpreter is like Compiler which translates object program into target
program. The main difference between both is that interpreter reads and transforms
code line by line. Compiler reads the entire code at once and creates the machine
code.
• Assembler: It translates assembly language code into machine understandable
language. The output result of assembler is a combination of machine instructions as
well as the data required to store these instructions in memory.
• Linker: The linker helps to link and merge various object files to create an executable
file. All these files might have been compiled separately. The main task of a linker is
to search for called modules in a program and to find out the memory location where
all modules are stored.
Page 4 of 26
• Loader: The loader is a part of the OS, which performs the tasks of loading executable
files into memory and run them. It also calculates the size of a program which creates
additional memory space.
Compiler Construction Tools
Compiler construction tools were introduced as computer-related technologies spread all over
the world. They are also known as a compiler- compilers, compiler- generators or translator.
These tools use specific language or algorithm for specifying and implementing the
component of the compiler. Following are the example of compiler construction tools.
• Scanner generators: This tool takes regular expressions as input. For example LEX for
Unix Operating System.
• Syntax-directed translation engines: These software tools offer an intermediate code
by using the parse tree. It has a goal of associating one or more translations with each
node of the parse tree.
• Parser generators: A parser generator takes a grammar as input and automatically
generates source code which can parse streams of characters with the help of a
grammar.
• Automatic code generators: Takes intermediate code and converts them into Machine
Language
• Data-flow engines: This tool is helpful for code optimization. Here, information is
supplied by user and intermediate code is compared to analyze any relation. It is also
known as data-flow analysis. It helps you to find out how values are transmitted from
one part of the program to another part.
Uses of Compiler
• Compiler verifies entire program, so there are no syntax or semantic errors
• The executable file is optimized by the compiler, so it is executes faster
• There is no need to execute the program on the same machine it was built
• Translate entire program in other language
• Link the files into an executable format
• Check for syntax errors and data types
• Helps to handle language performance issues
• The techniques used for constructing a compiler can be useful for other purposes
as well
Page 5 of 26
Applications of Compiler
• Compiler design helps full implementation of High-Level Programming Languages
• Support optimization for Computer Architecture Parallelism
• Design of New Memory Hierarchies of Machines
• Widely used for Translating Programs
• Used with other Software Productivity Tools
Page 6 of 26
Figure: The structure of a compiler.
Lexical Analysis
• Lexical analysis is the first phase of compiler which is also termed as scanning.
• Source program is scanned to read the stream of characters and those characters are
grouped to form a sequence called lexemes which produces token as output.
• Token: Token is a sequence of characters that represent lexical unit, which matches
with the pattern, such as keywords, operators, identifiers etc.
• Lexeme: Lexeme is instance of a token i.e., group of characters forming a token. ,
• Pattern: Pattern describes the rule that the lexemes of a token takes. It is the structure
that must be matched by strings.
Example: c=a+b*5;
Lexemes Tokens
C identifier
= assignment symbol
A identifier
+ + (addition symbol)
Page 7 of 26
B identifier
* * (multiplication symbol)
5 5 (number)
; Semicolon
Syntax Analysis
• Syntax analysis is the second phase of compiler which is also called as parsing.
• Parser converts the tokens produced by lexical analyzer into a tree like representation
called parse tree.
• Syntax tree is a compressed representation of the parse tree in which the operators appear
as interior nodes and the operands of the operator are the children of the node for that
operator.
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.
Page 8 of 26
Intermediate Code Generation
t1 = inttofloat (5)
t2 = id3* tl
t3 = id2 + t2
id1 = t3
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.
• This phase reduces the redundant code and attempts to improve the intermediate code so
that faster-running machine code will result.
Page 9 of 26
• During the code optimization, the result of the program is not affected.
• To improve the code generation, the optimization involves
➢ Deduction and removal of dead code (unreachable code).
➢ Calculation of constants in expressions and terms.
➢ Collapsing of repeated expression into temporary string.
➢ Loop unrolling.
➢ Moving code outside the loop.
➢ 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
➢ Generation of correct references.
➢ Generation of correct data types.
➢ Generation of missing code.
LDF R2, id3
MULF R2, # 5.0
LDF R1, id2
ADDF R1, R2
STF id1, R1
Page 10 of 26
Example
char a,b,c; const:5
Symbol name Type Address
A char 1000
b char 1002
C char 1004
5 const 1008
Error Handling
• Each phase can encounter errors. After detecting an error, a phase must handle the error
so that compilation can proceed.
• In lexical analysis, errors occur in separation of tokens.
• In syntax analysis, errors occur during construction of syntax tree.
• In semantic analysis, errors may occur at the following cases:
➢ When the compiler detects constructs that have right syntactic structure but no
meaning
➢ During type conversion.
• In code optimization, errors occur when the result is affected by the optimization. In code
generation, it shows error when code is missing etc.
Page 11 of 26
1.3 THE SCIENCE OF BUILDING A COMPILER
Page 12 of 26
A compiler must accept all source programs that conform to the specification of the language;
the set of source programs is infinite and any program can be very large, consisting of
possibly millions of lines of code. Any transformation performed by the compiler while
translating a source program must preserve the meaning of the program being compiled.
Compiler writers thus have influence over not just the compilers they create, but all the
programs that their compilers compile. This leverage makes writing compilers particularly
rewarding; however, it also makes compiler development challenging.
Finally, a compiler is a complex system; we must keep the system simple to assure that the
engineering and maintenance costs of the compiler are manageable. There is an infinite
number of program optimizations that we could implement, and it takes a nontrivial amount
of effort to create a correct and effective optimization. We must prioritize the optimizations,
implementing only those that lead to the greatest benefits on source programs encountered in
practice.
Page 13 of 26
Figure: Programming Language Basics
Static and Dynamic Distinction
• The scope of a declaration of x is the region of the program in which uses of x refer to
this declaration.
• A language uses static scope or lexical scope if it is possible to determine the scope of
a declaration by looking only at the program.
• Otherwise, the language uses dynamic scope. With dynamic scope, as the program
runs, the same use of x could refer to any of several different declarations of x.
Environment and States
• The scope rules for C are based on program structure. The scope of a declaration is
determined implicitly by where the declaration appears in the program.
Page 14 of 26
• Programming languages such as C++, Java, and C#, also provide explicit control over
scopes through the use of keywords like public, private, and protected.
• A block is a grouping of declarations and statements. C uses braces { and } to delimit
a block, the alternative use of begin and end in some languages.
• If p is an object of a class with a field (member) x, then the use of x in p.x refers to
field x in the class definition.
• Through the use of keywords like public, private, and protected, object oriented
languages such as C++ or Java provide explicit control over access to member names
in a super class. These keywords support encapsulation by restricting access.
o Public - Public names are accessible from outside the class
o Private - Private names include method declarations and definitions associated
with that class and any "friend" classes.
o Protected - Protected names are accessible to subclasses.
Dynamic Scope
• The term dynamic scope, however, usually refers to the following policy: a use of a
name x refers to the declaration of x in the most recently called procedure with such a
declaration.
• Dynamic scoping of this type appears only in special situations. The two dynamic
policies are:
o Macro expansion in the C
o Method resolution in OOPs
• Every language has some method for passing parameters to functions and procedures.
• Formal Parameters: The identifier used in a method to stand for the value that is
passed into the method by a caller.
• Actual Parameters: The actual value that is passed into the method by a caller.
o Call by Value - The actual parameter is evaluated (if it is an expression) or
copied (if it is a variable) in a formal parameter.
Page 15 of 26
o Call by Reference - The address of the actual parameter is passed as value of
the corresponding formal parameter.
o Call by Name - The Called object execute as if the actual parameter were
substituted literally for the formal parameter.
Aliasing
When lexical analyzer identifies the first token it will send it to the parser, the parser receives
the token and calls the lexical analyzer to send next token by issuing the getNextToken()
Page 16 of 26
command. This Process continues until the lexical analyzer identifies all the tokens. During
this process the lexical analyzer will neglect or discard the white spaces and comment lines.
A pattern is a description of the form that the lexemes of a token may take [ or match]. In the
case of a keyword as a token, the pattern is just the sequence of characters that form the
keyword. For identifiers and some other tokens, the pattern is a more complex structure that
is matched by many strings.
A lexeme is a sequence of characters in the source program that matches the pattern for a
token and is identified by the lexical analyzer as an instance of that token. Example: In the
following C language statement ,
printf ("Total = %d\n‖, score) ;
both printf and score are lexemes matching the pattern for token id, and "Total = %d\n‖ is a
lexeme matching literal [or string].
Page 17 of 26
1.5 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.
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.
Page 18 of 26
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.
Page 19 of 26
Initially both the bp and fp are pointing to the first character of first buffer. Then the fp
moves towards right in search of end of lexeme. as soon as blank character is recognized, the
string between bp and fp is identified as corresponding token. to identify, the boundary of
first buffer end of buffer character should be placed at the end first buffer.
Similarly end of second buffer is also recognized by the end of buffer mark present at the end
of second buffer. when fp encounters first eof, then one can recognize end of first buffer and
hence filling up second buffer is started. in the same way when second eof is obtained then it
indicates of second buffer.
Alternatively both the buffers can be filled up until end of the input program and stream of
tokens is identified. This eof character introduced at the end is calling Sentinel which is used
to identify the end of buffer.
Page 20 of 26
• Firstly lexical analyzer creates a program lex.1 in the Lex language. Then Lex
compiler runs the lex.1 program and produces a C program [Link].c.
• Finally C compiler runs the [Link].c program and produces an object program [Link].
• [Link] is lexical analyzer that transforms an input stream into a sequence of tokens.
Page 21 of 26
string is successfully processed and the automata reaches its final state, it is accepted, i.e., the
string just fed was said to be a valid token of the language in hand.
Page 22 of 26
1.8 REGULAR EXPRESSIONS
Regular expression
• Regular expression is a sequence of pattern that defines a string. It is used to denote
regular languages.
• It is also used to match character combinations in strings. String searching algorithm
used this pattern to find the operations on string.
• In regular expression, x* means zero or more occurrence of x. It can generate {e, x,
xx, xxx, xxxx,.....}
• In regular expression, x+ means one or more occurrence of x. It can generate {x, xx,
xxx, xxxx,.....}
Operations on Regular Language
The various operations on regular language are:
1. Union: If L and M are two regular languages then their union L U M is also a union.
L U M = {s | s is in L or s is in M}
2. Intersection: If L and M are two regular languages then their intersection is also an
intersection.
L ⋂ M = {st | s is in L and t is in M}
3. Kleene closure: If L is a regular language then its kleene closure L1* will also be a
regular language.
L* = Zero or more occurrence of language L.
Example
Write the regular expression for the language:
L = {abn w:n ≥ 3, w ∈ (a,b)+}
Page 23 of 26
Solution:
The string of language L starts with "a" followed by atleast three b's. Itcontains atleast one
"a" or one "b" that is string are like abbba, abbbbbba, abbbbbbbb, abbbb.....a
So regular expression is:
r= ab3b* (a+b)+
Here + is a positive closure i.e. (a+b)+ = (a+b)* - ∈
Solution:
Step 1: In the given DFA, q2 and q4 are the unreachable states so remove them.
Step 2: Draw the transition table for rest of the states.
Page 24 of 26
Step 3:
Now divide rows of transition table into two sets as:
1. One set contains those rows, which start from non-final sates:
2. Other set contains those rows, which starts from final states.
Page 25 of 26
Fig: Minimized DFA
Page 26 of 26
UNIT - II
2.1 INTRODUCTION
Syntax Analysis is a second phase of the compiler design process in which the given input
string is checked for the confirmation of rules and structure of the formal grammar. It
analyses the syntactical structure and checks if the given input is in the correct syntax of the
programming language or not.
Syntax Analysis in Compiler Design process comes after the Lexical analysis phase. It is also
known as the Parse Tree or Syntax Tree. The Parse Tree is developed with the help of pre-
defined grammar of the language. The syntax analyser also checks whether a given program
fulfills the rules implied by a context-free grammar. If it satisfies, the parser then creates the
parse tree of that source program. Otherwise, it will display error messages.
Page 1 of 37
grammar can be applied regardless of context—it does not depend on any other symbols that
may or may not be around a given symbol that is having a rule applied to it.
Example:
L= {wcwR | w € (a, b)*}
Production rules:
1. S → aSa
2. S → bSb
3. S → c
Now check that abbcbba string can be derived from the given CFG.
S ⇒ aSa
S ⇒ abSba
Page 2 of 37
S ⇒ abbSbba
S ⇒ abbcbba
By applying the production S → aSa, S → bSb recursively and finally applying the
production
S → c, we get the string abbcbba.
Derivation
A derivation is basically a sequence of production rules, in order to get the input string.
During parsing, we take two decisions for some sentential form of input:
• Deciding the non-terminal which is to be replaced.
• Deciding the production rule, by which, the non-terminal will be replaced.
To decide which non-terminal to be replaced with production rule, we can have two options.
Left-most Derivation
If the sentential form of an input is scanned and replaced from left to right, it is called left-
most derivation. The sentential form derived by the left-most derivation is called the left-
sentential form.
Right-most Derivation
If we scan and replace the input with production rules, from right to left, it is known as right-
most derivation. The sentential form derived from the right-most derivation is called the
right-sentential form.
Example
Production rules:
E→E+E
E→E-E
E → id
Input string: id + id * id
Page 3 of 37
E → id + E - E
E → id + id - E
E → id + id - id
Notice that the left-most side non-terminal is always processed first.
Parse Tree
A parse tree is a graphical depiction of a derivation. It is convenient to see how strings are
derived from the start symbol. The start symbol of the derivation becomes the root of the
parse tree. For the string id + id – id, to construct two parse trees: Left-Most -Derivation and
Right- Most -Derivation
Page 4 of 37
Fig: Types of Top-Down Parsing Techniques
Backtracking
Backtracking is a technique in which for expansion for non-terminal symbol we choose one
alternative and if some mismatch occurs then we try another alternative if any.
Example
Consider the grammar G : S → cAd
A→ab|a and the input string w=cad.
The parse tree can be constructed using the following top-down approach :
Step1:
Initially create a tree with single node labeled S. An input pointer points to ‘c’, the first
symbol of w. Expand the tree with the production of S.
Step2:
Page 5 of 37
The leftmost leaf ‘c’ matches the first symbol of w, so advance the input pointer to the second
symbol of w ‘a’ and consider the next leaf ‘A’. Expand A using the first alternative.
Step3:
The second symbol ‘a’ of w also matches with second leaf of tree. So advance the input
pointer to third symbol of w ‘d’.But the third leaf of tree is b which does not match with the
input symbol [Link] discard the chosen production and reset the pointer to
second backtracking.
A backtracking parser will try different production rules to find the match for the input string
by backtracking each time. The backtracking is powerful than predictive parsing. But this
technique is slower and its requires exponential time in general. Hence backtracking is not
preferred for practical compilers.
Predictive parser
As the same name suggests the predictive parser tries to predict the next construction using
one or more lookahead symbols from input string. There are two types of predictive parsers.
Page 6 of 37
1. Recursive Descent parser
2. LL(1) parser
1. Ambiguity
2. Back Tracking
3. Left Recursion
4. Left Factoring
Ambiguity
A grammar is said to be ambiguous if there exists more than one leftmost derivation or more
than one rightmost derivative or more than one parse tree for the given input string. If the
grammar is not ambiguous then it is called unambiguous.
Example:
S = aSb | SS
S=∈
For the string aabb, the above grammar generates two parse trees:
Page 7 of 37
If the grammar has ambiguity then it is not good for a compiler construction. No method can
automatically detect and remove the ambiguity but you can remove ambiguity by re-writing
the whole grammar without ambiguity.
Backtracking
Backtracking is a technique in which for expansion for non-terminal symbol we choose one
alternative and if some mismatch occurs then we try another alternative if any.
Example
Step1:
Initially create a tree with single node labeled S. An input pointer points to ‘c’, the first
symbol of w. Expand the tree with the production of S.
Step2:
The leftmost leaf ‘c’ matches the first symbol of w, so advance the input pointer to the second
symbol of w ‘a’ and consider the next leaf ‘A’. Expand A using the first alternative.
Page 8 of 37
Step3:
The second symbol ‘a’ of w also matches with second leaf of tree. So advance the input
pointer to third symbol of w ‘d’.But the third leaf of tree is b which does not match with the
input symbol [Link] discard the chosen production and reset the pointer to
second backtracking.
Left Recursion
Page 9 of 37
A --> A |
introduce a new nonterminal A' and rewrite the rule as
A --> A'
A' --> | A'
Thus the production:
E --> E + T | T
is left-recursive with "E" playing the role of "A","+ T" playing the role of , and "T" playing
the role of A'. Introducing the new nonterminal E', the production can be replaced by:
E --> T E'
E' --> | + T E'
Of course, there may be more than one left-recursive part on the right-hand side. The general
rule is to replace:
Step one describes a rule to eliminate direct left recursion from a production. To eliminate
left-recursion from an entire grammar may be more difficult because of indirect left-
recursion. For example,
A --> B x y | x
B --> C D
C --> A | c
D --> d
is indirectly recursive because
A ==> B x y ==> C D x y ==> A D x y.
That is, A ==> ... ==> A where is D x y.
Page 10 of 37
To eliminates left-recursion entirely. It contains a "call" to a procedure which eliminates
direct left-recursion (as described in step one).
Left Factoring
Left Factoring is a grammar transformation technique. It consists in "factoring out" prefixes
which are common to two or more productions.
Left factoring is removing the common left factor that appears in two productions of the same
non-terminal. It is done to avoid back-tracing by the parser. Suppose the parser has a look-
ahead ,consider this example-
A -> qB | qC
where A,B,C are non-terminals and q is a sentence. In this case, the parser will be confused
as to which of the two productions to choose and it might have to back-trace. After left
factoring, the grammar is converted to-
A -> qD
D -> B | C
In this case, a parser with a look-ahead will always choose the right production.
Left recursion is a case when the left-most non-terminal in a production of a non-terminal is
the non-terminal itself( direct left recursion ) or through some other non-terminal definitions,
rewrites to the non-terminal again(indirect left recursion). Consider these examples -
Page 11 of 37
If the given grammar is
E -> TE′
E′-> +TE′ | €
T -> FT′
T′-> *FT′ | €
F -> (E) | id
Reccursive procedures for the recursive descent parser for the given grammar are given
below
procedure E( )
{
if(Lookahead==' $ ')
declare as Success ;
else error;
T( );
E′( );
}
procedure T ( )
{
F( );
T′( );
}
Procedure E′( )
{
if (Lookahead= = '+')
{
match('+');
T ( );
E′( );
}
else if (Lookahead==NULL)
{
Page 12 of 37
match('NULL') ;
}
else error;
}
procedure T′( )
{
if (Lookahead == '*')
{
match('+');
F ( );
T′( );
}
else if(Lookahead == 'NULL')
{
match(NULL);
}
else error;
}
procedure F( )
{
if (Lookahead== '(' )
{
match( '(' );
E( );
}
if else(Lookahead==' )' )
{
match(' )' );
}
if else(Lookahead==' id ')
{
Page 13 of 37
match('id');
}
else error;
}
Procedure Error ( )
{
printf(" Error!");
}
Procedure NULL( )
{
Printf(" Empty!");
}
Page 14 of 37
Fig. Predictive LL(1) parser
A table-driven predictive parser has an input buffer, a stack, a parsing table, and an output
stream. The input buffer contains the string to be parsed, followed by $, a symbol used as a
right end marker to indicate the end of the input string. The stack contains a sequence of
grammar symbols with $ on the bottom, indicating the bottom of the stack. Initially, the stack
contains the start symbol of the grammar on top of $. The parsing table is a two dimensional
array M[A,a] where A is a nonterminal, and a is a terminal or the symbol $. The parser is
controlled by a program that behaves as follows. The program considers X, the symbol on the
top of the stack, and a, the current input symbol. These two symbols determine the action of
the parser. There are three possibilities.
1. If X= a=$, the parser halts and announces successful completion of parsing.
2. If X=a!=$, the parser pops X off the stack and advances the input pointer to the next
input symbol.
3. If X is a nonterminal, the program consults entry M[X,a] of the parsing table M. This
entry will be either an X-production of the grammar or an error entry. If, for example,
M[X,a]={X- >UVW}, the parser replaces X on top of the stack by WVU( with U on
top). As output, we shall assume that the parser just prints the production used; any
other code could be executed here. If M[X,a]=error, the parser calls an error recovery
routine.
Algorithm for predictive LL(1) Parsing.
Input: A string w and a parsing table M for grammar G.
Output: If w is in L(G), a leftmost derivation of w; otherwise, an error indication.
Page 15 of 37
Method: Initially, the parser is in a configuration in which it has $S on the stack with S,
the start symbol of G on top, and w$ in the input buffer. The program that utilizes the
predictive parsing table M to produce a parse for the input is shown in Fig.
else error()
Page 16 of 37
Rules for FOLLOW( ):
1. If S is a start symbol, then FOLLOW(S) contains $.
2. If there is a production A → αBβ, then everything in FIRST(β) except ε is placed
in follow(B).
3. If there is a production A → αB, or a production A → αBβ where FIRST(β)
contains ε, then everything in FOLLOW(A) is in FOLLOW(B).
Algorithm for construction of predictive LL(1) parsing table:
Input : Grammar G
Output : Parsing table M
Method :
1. For each production A → α of the grammar, do steps 2 and 3.
2. For each terminal a in FIRST(α), add A → α to M[A, a].
3. If ε is in FIRST(α), add A → α to M[A, b] for each terminal b in FOLLOW(A). If ε is
in FIRST(α) and $ is in FOLLOW(A) , add A → α to M[A, $].
4. Make each undefined entry of M be error.
Example:
Consider the following grammar:
E→E+T|T
T→T*F|F
F→(E)|id
Solution
Page 17 of 37
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) = {+, * , $ , ) }
Predictive parsing Table
Stack Implementation
Page 18 of 37
1.4 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.
Page 19 of 37
Fig: Types of Bottom up Parser
Shift-reduce parsing uses two unique steps for bottom-up parsing. These steps are known as
shift-step and reduce-step.
• Shift step: The shift step refers to the advancement of the input pointer to the next
input symbol, which is called the shifted symbol. This symbol is pushed onto the
stack. The shifted symbol is treated as a single node of the parse tree.
• Reduce step : When the parser finds a complete grammar rule (RHS) and replaces it
to (LHS), it is known as reduce-step. This occurs when the top of the stack contains a
handle. To reduce, a POP function is performed on the stack which pops off the
handle and replaces it with LHS non-terminal symbol.
Shift Reducing Parser to construct parse free from leaves to root. It works on the same
principle of bottom up parser. A shift reduce parser requires following steps:
Shift: Moving of Symbols from input buffer out to the stack, this action is called shift.
Page 20 of 37
Reduce: If the handle appears on the top of the stack there reduction of if by
appropriate rule is done that means RHS of rule is popped of and LHS is pushed in.
Accept: If the stack contain start symbol only and input buffer is empty at the same time then
that action is called accept.
Operator precedence grammar is kinds of shift reduce parsing method. It is applied to a small
class of operator grammars.
Page 21 of 37
o No two non-terminals are adjacent.
Operator precedence can only established between the terminals of the grammar. It ignores
the non-terminal.
a ⋗ b means that terminal "a" has the higher precedence than terminal "b".
a ⋖ b means that terminal "a" has the lower precedence than terminal "b".
a ≐ b means that the terminal "a" and "b" both have same precedence.
Precedence table:
2.9 LR PARSERS
LR parsing is one type of bottom up parsing. It is used to parse the large class of grammars.
Page 22 of 37
Figure: Structure of LR Parser
Input
Contains the string to be parsed and pointer
Parsing Table
Contains two parts ACTION and GOTO which is used by the parser program
1. ACTION Part
The ACTION part of the table is a two dimensional array indexed by state and the input
symbol, i.e. ACTION[state][input], An action table entry can have one of following four
kinds of values in it. They are:
1. Shift X, where X is a State number.
2. Reduce X, where X is a Production number.
3. Accept, signifying the completion of a successful parse.
2. GO TO Part
The GO TO part of the table is a two dimensional array indexed by state and a Non terminal,
i.e. GOTO[state][NonTerminal]. A GO TO entry has a state number in the table.
Augment Grammar
Page 23 of 37
The Augment Grammar G`, is G with a new starting symbol S` an additional production
S` ->S. this helps the parser to identify when to stop the parsing and announce the acceptance
of the input. The input string is accepted if and only if the parser is about to reduce by S`-> S.
NOTE: Augment Grammar is simply adding one extra production by preserving the actual
meaning of the given Grammar G.
Stack
Contains string of the form s0X1s1X2…..Xmsm where sm is on the top of the stack. Each Xi
denotes a grammar symbol and si a state. Each state symbol summarizes the information
contained in the stack below it
Parser Program
This determines the state on the sm on the top of the stack and the current input symbol a to
determine the next step
Types LR Parsers
SLR parsing, CLR parsing and LALR parsing.
In the SLR (1) parsing, we place the reduce move only in the follow of left hand side.
Various steps involved in the SLR (1) Parsing:
o For the given input string write a context free grammar
o Check the ambiguity of the grammar
Page 24 of 37
o Add Augment production in the given grammar
o Create Canonical collection of LR (0) items
o Draw a data flow diagram (DFA)
o Construct a SLR (1) parsing table
I1: GOTO(I0,E)
E’ → E.
E → E.+ T
I2: GOTO(I0,T)
E → T.
T → T .* F
I3: GOTO(I0,F)
Page 25 of 37
T → F.
I4: GOTO(I0,( )
F → (.E)
E→.E+T
E → .T
T → .T * F
T → .F
F → .( E )
F → .id
I5: GOTO(I0,id)
F → id.
I6: GOTO(I1,E)
E → E + .T
T → .T * F
T → .F
F → .( E )
F → .id
I7: GOTO(I2,*)
T → T * .F
F → .( E)
F → .id
I8: GOTO(I4,E)
F → ( E .)
E → E. + T
I9: GOTO(I6,T)
E → E + T.
T → T. * F
Page 26 of 37
I10: GOTO(I7,F)
T → T * F.
I11: GOTO(I8,) )
F → ( E ).
ACTION GOTO
STATES id + * ( ) $ E T F
0 s5 1 2 3
1 s6 s4 ACC
2 r2 s7 r2 r2
3 r4 r4 r4 r4
4 s5 S4 8
5 r6 r6 r6 r6
6 s5 s4 9
7 s5 s4 10
8 s6 s11
9 r1 s7 r1 r1
10 r3 r3 r3 r3
11 r5 r5 r5 r5
CLR refers to canonical lookahead. CLR parsing use the canonical collection of LR (1) items
to build the CLR (1) parsing table. CLR (1) parsing table produces the more number of states
as compare to the SLR (1) parsing.
In the CLR (1), we place the reduce node only in the lookahead symbols. Various steps
involved in the CLR (1) Parsing:
Page 27 of 37
o Create Canonical collection of LR (0) items
o Draw a data flow diagram (DFA)
o Construct a CLR (1) parsing table
LR (1) item
The look ahead is used to determine that where we place the final item.
The look ahead always add $ symbol for the argument production.
S → AA
A → aA
A→b
Add Augment Production, insert '•' symbol at the first position for every production in G and
also add the lookahead.
S` → •S, $
S → •AA, $
A → •aA, a/b
A → •b, a/b
I0 State:
Add all productions starting with S in to I0 State because "." is followed by the non-terminal.
So, the I0 State becomes
Page 28 of 37
I0 = S` → •S, $
S → •AA, $
Add all productions starting with A in modified I0 State because "." is followed by the non-
terminal. So, the I0 State becomes.
I0= S` → •S, $
S → •AA, $
A → •aA, a/b
A → •b, a/b
Add all productions starting with A in I2 State because "." is followed by the non-terminal.
So, the I2 State becomes
I2= S → A•A, $
A → •aA, $
A → •b, $
Add all productions starting with A in I3 State because "." is followed by the non-terminal.
So, the I3 State becomes
Page 29 of 37
I4= Go to (I0, b) = closure ( A → b•, a/b) = A → b•, a/b
I5= Go to (I2, A) = Closure (S → AA•, $) =S → AA•, $
I6= Go to (I2, a) = Closure (A → a•A, $)
Add all productions starting with A in I6 State because "." is followed by the non-terminal.
So, the I6 State becomes
I6 = A → a•A, $
A → •aA, $
A → •b, $
Drawing DFA:
LALR refers to the lookahead LR. To construct the LALR (1) parsing table, we use the
canonical collection of LR (1) items.
Page 30 of 37
In the LALR (1) parsing, the LR (1) items which have same productions but different look
ahead are combined to form a single set of items
LALR (1) parsing is same as the CLR (1) parsing, only difference in the parsing table.
I0 State:
Add all productions starting with S in to I0 State because "•" is followed by the non-terminal.
So, the I0 State becomes
I0 = S` → •S, $
S → •AA, $
Add all productions starting with A in modified I0 State because "•" is followed by the non-
terminal. So, the I0 State becomes.
I0= S` → •S, $
S → •AA, $
A → •aA, a/b
A → •b, a/b
Page 31 of 37
Add all productions starting with A in I2 State because "•" is followed by the non-terminal.
So, the I2 State becomes
I2= S → A•A, $
A → •aA, $
A → •b, $
Add all productions starting with A in I3 State because "•" is followed by the non-terminal.
So, the I3 State becomes
Add all productions starting with A in I6 State because "•" is followed by the non-terminal.
So, the I6 State becomes
I6 = A → a•A, $
A → •aA, $
A → •b, $
Page 32 of 37
I7= Go to (I2, b) = Closure (A → b•, $) = A → b•, $
I8= Go to (I3, A) = Closure (A → aA•, a/b) = A → aA•, a/b
I9= Go to (I6, A) = Closure (A → aA•, $) A → aA•, $
If we analyze then LR (0) items of I3 and I6 are same but they differ only in their lookahead.
I3 = { A → a•A, a/b
A → •aA, a/b
A → •b, a/b
}
I6= { A → a•A, $
A → •aA, $
A → •b, $
}
Clearly I3 and I6 are same in their LR (0) items but differ in their lookahead, so we can
combine them and called as I36.
The I4 and I7 are same but they differ only in their look ahead, so we can combine them and
called as I47.
The I8 and I9 are same but they differ only in their look ahead, so we can combine them and
called as I89.
Drawing DFA:
Page 33 of 37
LALR Parsing table:
YACC Specification
The YACC specification file consists of three parts.
Declaration section: In this section, ordinary C declarations are inserted and grammar tokens
are declared. The tokens should be declared between %{ and %}
Translation rule section
It includes the production rules of context free grammar with corresponding actions
Rule-1 action-1
Rule-2 action-2
:
:
Rule n action n
If there is more than one alternative to a single rule then those alternatives are separated by ‘|’
(pipe) character. The actions are typical C statements. If CFG is
LHS: alternative 1 | alternative 2 | …… alternative n
Then
LHS: alternative 1 {action 1}
| alternative 2 {action 1}
:
:
alternative n {action n}
Page 34 of 37
C functions Section: this consists of one main function in which the routine yyparse() is
called. And it also contains required C functions
Example:
YACC Specification of a simple desk calculator:
%{
#include <ctype.h>
%}
%token DIGIT
%%
line: expr ‘\n’ { printf(“%d\n”, $1); }
;
expr : expr ‘+’ term { $$ = $1 + $3; }
| term
;
term : term ‘*’ factor { $$ = $1 * $3; }
| factor
;
factor : ‘(‘ expr ‘)’ { $$ = $2; }
| DIGIT
;
%%
yylex() {
int c;
c = getchar();
if(isdigit(c)
{
yylval = c-‘0’;
return DIGIT;
}
return c;
}
The declaration keywords %left, %right and %nonassoc inform YACC that the following
tokens are to be treated as left-associative (as binary + - * & / commonly are), right-
associative (as exp often is), or non-associative (as binay < & > often are)
The order of declarations informs YACC that the tokens should be accorded increasing
precedence
Page 37 of 37
UNIT - III
• In syntax directed translation, every non-terminal can get one or more than one attribute
or sometimes 0 attribute depending on the type of the attribute. The value of these
attributes is evaluated by the semantic rules associated with the production rule.
• In the semantic rule, attribute is VAL and an attribute may hold anything like a string, a
number, a memory location and a complex record
• In Syntax directed translation, whenever a construct encounters in the programming
language then it is translated according to the semantic rules define in that particular
programming language.
Example:
Page 1 of 20
T→T*F [Link] := [Link] + [Link]
1. Synthesized Attributes – These are those attributes which derive their values from their
children nodes i.e. value of synthesized attribute at node is computed from the values of
attributes at children nodes in parse tree.
Example:
E --> E1 + T { [Link] = [Link] + [Link]}
In this, [Link] derive its values from [Link] and [Link]
• Write the SDD using appropriate semantic rules for each production in given grammar.
• The annotated parse tree is generated and attribute values are computed in bottom up
manner.
• The value obtained at root node is the final output.
Example: Consider the following grammar
S --> E
E --> E1 + T
E --> T
T --> T1 * F
T --> F
F --> digit
The SDD for the above grammar can be written as follow
Page 2 of 20
Let us assume an input string 4 * 5 + 6 for computing synthesized attributes. The annotated parse
tree for the input string is
For computation of attributes we start from leftmost bottom node. The rule F –> digit is used to
reduce digit to F and the value of digit is obtained from lexical analyzer which becomes value of
Page 3 of 20
F i.e. from semantic action [Link] = [Link]. Hence, [Link] = 4 and since T is parent node of F
so, we get [Link] = 4 from semantic action [Link] = [Link]. Then, for T –> T1 * F production, the
corresponding semantic action is [Link] = [Link] * [Link] . Hence, [Link] = 4 * 5 = 20
Similarly, combination of [Link] + [Link] becomes [Link] i.e. [Link] = [Link] + [Link] = 26. Then, the
production S –> E is applied to reduce [Link] = 26 and semantic action associated with it prints
the result [Link] . Hence, the output will be 26.
2. Inherited Attributes – These are the attributes which derive their values from their parent or
sibling nodes i.e. value of inherited attributes are computed by value of parent or sibling nodes.
Example:
A --> BCD { [Link] = [Link], [Link] = [Link] }
Page 4 of 20
Let us assume an input string int a, c for computing inherited attributes. The annotated parse tree
for the input string is
The value of L nodes is obtained from [Link] (sibling) which is basically lexical value obtained
as int, float or double. Then L node gives type of identifiers a and c. The computation of type is
done in top down manner or preorder traversal. Using function Enter_type the type of identifiers
a and c is inserted in symbol table at corresponding [Link].
Page 5 of 20
3.2 SYNTAX DIRECTED TRANSLATION SCHEME
• The Syntax directed translation scheme is a context -free grammar.
• The syntax directed translation scheme is used to evaluate the order of semantic rules.
• In translation scheme, the semantic rules are embedded within the right side of the
productions.
• The position at which an action is to be executed is shown by enclosed between braces. It
is written within the right side of the production.
Example
S→E$ { [Link] }
Page 6 of 20
S→E$ { [Link] }
Page 7 of 20
Figure: Intermediate Code Generator
• If the compiler directly translates source code into the machine code without generating
intermediate code then a full native compiler is required for each new machine.
• The intermediate code keeps the analysis portion same for all the compilers that's why it
doesn't need a full compiler for every unique machine.
• Intermediate code generator receives input from its predecessor phase and semantic
analyzer phase. It takes input in the form of an annotated syntax tree.
• Using the intermediate code, the second phase of the compiler synthesis phase is changed
according to the target machine.
Syntax tree
Page 8 of 20
When you create a parse tree then it contains more details than actually needed. So, it is very
difficult to compiler to parse the parse tree. Take the following parse tree as an example:
• In the parse tree, most of the leaf nodes are single child to their parent nodes.
• In the syntax tree, we can eliminate this extra information.
• Syntax tree is a variant of parse tree. In the syntax tree, interior nodes are operators and
leaves are operands.
• Syntax tree is usually used when represent a program in a tree structure.
A sentence id + id * id would have the following syntax tree:
Page 9 of 20
Abstract syntax trees are important data structures in a compiler. It contains the least unnecessary
information.
Abstract syntax trees are more compact than a parse tree and can be easily used by a compiler.
Postfix Notation
• Postfix notation is the useful form of intermediate code if the given language is
expressions.
• Postfix notation is also called as 'suffix notation' and 'reverse polish'.
• Postfix notation is a linear representation of a syntax tree.
• In the postfix notation, any expression can be written unambiguously without
parentheses.
• The ordinary (infix) way of writing the sum of x and y is with operator in the middle: x *
y. But in the postfix notation, we place the operator at the right end as xy *.
• In postfix notation, the operator follows the operand.
Example
Production
1. E → E1 op E2
2. E → (E1)
3. E → id
Page 10 of 20
[Link] = [Link]
[Link] = id print id
Quadruples
The quadruples have four fields to implement the three address code. The field of quadruples
contains the name of the operator, the first source operand, the second source operand and the
result respectively.
Example
1. a := -b * c + d
Three-address code is as follows:
t1 := -b
Page 11 of 20
t2 := c + d
t3 := t1 * t2
a := t3
(0) uminus B - t1
(1) + c d t2
(2) * t1 t2 t3
(3) := t3 - a
Triples
The triples have three fields to implement the three address code. The field of triples contains the
name of the operator, the first source operand and the second source operand.
Example:
a := -b * c + d
Three address code is as follows:
t1 := -b
t2 := c + d
t3 := t1 * t2
a := t3
These statements are represented by triples as follows:
Page 12 of 20
(0) uminus b -
(1) + c d
(3) := (2) -
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, say offset, can keep
track of the next available relative address.
In the translation scheme shown below:
• Non-terminal P generates a sequence of declarations of the form id : T.
• Before the first declaration is considered, offset is set to 0. As each new name is seen ,
that name is entered in the symbol table with offset equal to the current value of offset,
and offset is incremented by the width of the data object denoted by that name.
• The procedure enter( name, type, offset ) creates a symbol-table entry for name, gives its
type type and relative address offset in its data area.
• Attribute type represents a type expression constructed from the basic types integer and
real by applying the type constructors pointer and array. If type expressions are
represented by graphs, then attribute type might be a pointer to the node representing a
type expression.
Page 13 of 20
• 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.
Page 14 of 20
➢ [Link] is the label to which control flows if E is true, and [Link] 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 [Link] to the three-address instruction immediately following
[Link].
➢ [Link] is a label that is attached to the first three-address instruction to be executed after
the code for S.
Code for if-then , if-then-else, and while-do statements
Page 15 of 20
Control-Flow Translation of Boolean Expressions:
Syntax-directed definition to produce three-address code for Booleans
Page 16 of 20
3.7 CASE STATEMENTS
The “switch” or “case” statement is available in a variety of languages. The switch-statement
syntax is as shown below :
Page 17 of 20
Translation of a case statement
Page 18 of 20
Calling Sequences: The translation for a call includes a calling sequence, a sequence of actions
taken on entry to and exit from each procedure. The falling are the actions that take place in a
calling sequence :
1. When a procedure call occurs, space must be allocated for the activation record of the
called procedure.
2. The arguments of the called procedure must be evaluated and made available to the called
procedure in a known place.
3. Environment pointers must be established to enable the called procedure to access data in
enclosing blocks.
4. The state of the calling procedure must be saved so it can resume execution after the call.
5. Also saved in a known place is the return address, the location to which the called,
routine must transfer after it is finished.
6. Finally a jump to the beginning of the code for the called procedure must be generated.
Page 19 of 20
➢ Here, the code for S is the code for Elist, which evaluates the arguments, followed by a
param p statement for each argument, followed by a call statement.
➢ queue is emptied and then gets a single pointer to the symbol table location for the name
that denotes the value of E.
Page 20 of 20
UNIT – IV
Run-Time Environments: Stack Allocation of Space, Access to Nonlocal Data on the Stack,
Heap Management, Introduction to Garbage Collection, Introduction to Trace-Based
Collection.
Code Generation: Issues in the Design of a Code Generator, The Target Language, Addresses
in the Target Code, Basic Blocks and Flow Graphs, Optimization of Basic Blocks, A Simple
Code Generator, Peephole Optimization, Register Allocation and Assignment, Dynamic
Programming Code-Generation.
Almost all compilers for languages that use procedures, functions, or methods as units of user-
defined actions manage at least part of their run-time memory as a stack. Each time a
procedure1 is called, space for its local variables is pushed onto a stack, and when the procedure
terminates, that space is popped off the stack. As we shall see, this arrangement not only allows
space to be shared by procedure calls whose durations do not overlap in time, but it allows us
to compile code for a procedure in such a way that the relative addresses of its nonlocal
variables are always the same, regardless of the sequence of procedure calls.
Stack allocation is a procedure in which stack is used to organize the storage. The stack used
in stack allocation is known as control stack. In this type of allocation, creation of data objects
is performed dynamically. In stack allocation, activation records are created for the allocation
of memory. These activation records are pushed onto the stack using Last In First Out (LIFO)
method. Locals are stored in the activation records at run time and memory addressing is done
by using pointers and registers.
Page 1 of 23
4.2 ACCESS TO NONLOCAL DATA ON THE STACK
The scope of a declaration in a block-structured language is given by the most closely nested
rule: – The scope of a declaration in a block B includes B. – If a name X is not declared in a
block B, then an occurrence of X in B is in the scope of a declaration of X in an enclosing block
B´ such that
● B´ has a declaration of X, and
● B´ is more closely nested around B than any other block with a declaration of X
• Allocation. When a program requests memory for a variable or object,3 the memory
manager produces a chunk of contiguous heap memory of the requested size. If
possible, it satisfies an allocation request using free space in the heap; if no chunk of
the needed size is available, it seeks to increase the heap storage space by getting
consecutive bytes of virtual memory from the operating system. If space is exhausted,
the memory manager passes that information back to the application program.
• De-allocation. The memory manager returns de-allocated space to the pool of free
space, so it can reuse the space to satisfy other allocation requests.
Memory managers typically do not return memory to the operating system, even if the
program's heap usage drops. Memory management would be simpler if (a) all allocation
requests were for chunks of the same size, and (b) storage were released predictably, say, first-
allocated first-de-allocated. There are some languages, such as Lisp, for which condition (a)
Page 2 of 23
holds; pure Lisp uses only one data element — a two pointer cell — from which all data
structures are built. Condition (b) also holds in some situations, the most common being data
that can be allocated on the run-time stack. However, in most languages, neither (a) nor (b)
holds in general. Rather, data elements of different sizes are allocated, and there is no good
way to predict the lifetimes of all allocated objects.
Space Efficiency. A memory manager should minimize the total heap space needed by
a program. Doing so allows larger programs to run in a fixed virtual address space.
Space efficiency is achieved by minimizing "fragmentation," discussed in Section
7.4.4.
Program Efficiency. A memory manager should make good use of the memory
subsystem to allow programs to run faster. As the time taken to execute an instruction
can vary widely depending on where objects are placed in memory. Fortunately,
programs tend to exhibit "locality," which refers to the nonrandom clustered way in
which typical programs access memory. By attention to the placement of objects in
memory, the memory manager can make better use of space and, hopefully, make the
program run faster.
Low Overhead. Because memory allocations and de-allocations are frequent operations
in many programs, it is important that these operations be as efficient as possible. That
is, we wish to minimize the overhead — the fraction of execution time spent performing
allocation and de-allocation. Notice that the cost of allocations is dominated by small
requests; the overhead of managing large objects is less important, because it usually
can be amortized over a larger amount of computation.
Page 3 of 23
references, it is considered unnecessary and can be deleted to free up the space in memory.
Advanced reference counting detects objects that only reference each other, which indicates
the objects are unused by the parent process.
Garbage collection may also be done at compile-time, when a program's source code is
compiled into an executable program. In this method, the compiler determines which resources
in memory will never be accessed after a certain time. It can then add instructions to
automatically deallocate those resources from memory. While this is an effective way to
eliminate unused objects, it must be done conservatively to avoid deleting references required
by the program.
Garbage collection is an important part of software development since it keeps programs from
using up too much RAM. Besides helping programs run more efficiently, it can also prevent
serious bugs, such as memory leaks, that can cause a program to crash.
Page 4 of 23
Following Algorithm, which we consider after introducing a general framework for trace-based
algorithms, is an optimization of following Algorithm. By using an additional list to hold all
the allocated objects, it visits the reachable objects only once.
Algorithm: Mark-and-sweep garbage collection.
INPUT: A root set of objects, a heap, and a free list, called Free, with all the unallocated chunks
of the heap. All chunks of space are marked with boundary tags to indicate their free/used status
and size.
OUTPUT: A modified free list after all the garbage has been removed.
METHOD: The algorithm, shown in Fig. 7.21, uses several simple data structures. List Free
holds objects known to be free. A list called Unscanned, holds objects that we have determined
are reached, but whose successors we have not yet considered. That is, we have not scanned
these objects to see what other objects can be reached through them. The Unscanned list is
empty initially.
Additionally, each object includes a bit to indicate whether it has been reached (the reached-
bit). Before the algorithm begins, all allocated objects have the reached-bit set to 0.
In line (1) of Fig. 7.21, we initialize the Unscanned list by placing there all the objects
referenced by the root set. The reached-bit for these objects is also set to 1. Lines (2) through
Page 5 of 23
(7) are a loop, in which we, in turn, examine each object o that is ever placed on the Unscanned
list. The for-loop of lines (4) through (7) implements the scanning of object o.
We examine each object d for which we find a reference within o. If d has already been reached
(its reached-bit is 1), then there is no need to do anything about d; it either has been scanned
previously, or it is on the Unscanned list to be scanned later. However, if d was not reached
already, then we need to set its reached-bit to 1 in line (6) and add d to the Unscanned list in
line (7). Figure 7.22 illustrates this process. It shows an Unscanned list with four objects. The
first object on this list, corresponding to object o in the discussion above, is in the process of
being scanned. The dashed lines correspond to the three kinds of objects that might be reached
from o:
1. A previously scanned object that need not be scanned again.
2. An object currently on the Unscanned list.
3. An item that is reachable, but was previously thought to be unreached.
Lines (8) through (11), the sweeping phase, reclaim the space of all the objects that remain
unreached at the end of the marking phase. Note that these will include any objects that were
on the Free list originally. Because the set of unreached objects cannot be enumerated directly,
the algorithm sweeps through the entire heap. Line (10) puts free and unreached objects on the
Free list, one at a time. Line (11) handles the reachable objects. We set their reached-bit to 0, in
order to maintain the proper preconditions for the next execution of the garbage-collection
algorithm.
Page 6 of 23
The following issue arises during the code generation phase:
1. Input to code generator
The input to code generator is the intermediate code generated by the front end, along with
information in the symbol table that determines the run-time addresses of the data-objects
denoted by the names in the intermediate representation. Intermediate codes may be
represented mostly in quadruples, triples, indirect triples, Postfix notation, syntax trees,
DAG’s, etc. The code generation phase just proceeds on an assumption that the input are free
from all of syntactic and state semantic errors, the necessary type checking has taken place and
the type-conversion operators have been inserted wherever necessary.
2. Target program
The target program is the output of the code generator. The output may be absolute machine
language, relocatable machine language, assembly language.
Absolute machine language as output has advantages that it can be placed in a fixed memory
location and can be immediately executed.
Assembly language as output makes the code generation easier. We can generate symbolic
instructions and use macro-facilities of assembler in generating code. And we need an
additional assembly step after code generation.
3. Memory Management
Mapping the names in the source program to the addresses of data objects is done by the front
end and the code generator. A name in the three address statements refers to the symbol table
entry for name. Then from the symbol table entry, a relative address can be determined for the
name.
4. Instruction selection
Selecting the best instructions will improve the efficiency of the program. It includes the
instructions that should be complete and uniform. Instruction speeds and machine idioms also
plays a major role when efficiency is considered. But if we do not care about the efficiency of
the target program then instruction selection is straight-forward.
Page 7 of 23
For example, the respective three-address statements would be translated into the latter code
sequence as shown below:
P:=Q+R
S:=P+T
MOV Q, R0
ADD R, R0
MOV R0, P
MOV P, R0
ADD T, R0
MOV R0, S
Here the fourth statement is redundant as the value of the P is loaded again in that statement
that just has been stored in the previous statement. It leads to an inefficient code sequence. A
given intermediate representation can be translated into many code sequences, with significant
cost differences between the different implementations. A prior knowledge of instruction cost
is needed in order to design good sequences, but accurate cost information is difficult to predict.
5. Register allocation issues
Use of registers make the computations faster in comparison to that of memory, so efficient
utilization of registers is important. The use of registers are subdivided into two subproblems:
1. During Register allocation – we select only those set of variables that will reside in the
registers at each point in the program.
2. During a subsequent Register assignment phase, the specific register is picked to access the
variable.
As the number of variables increases, the optimal assignment of registers to variables becomes
difficult. Mathematically, this problem becomes NP-complete. Certain machine requires
register pairs consist of an even and next odd-numbered register. For example
M a, b
These types of multiplicative instruction involve register pairs where the multiplicand is an
even register and b, the multiplier is the odd register of the even/odd register pair.
Evaluation order –
Page 8 of 23
The code generator decides the order in which the instruction will be executed. The order of
computations affects the efficiency of the target code. Among many computational orders,
some will require only fewer registers to hold the intermediate results. However, picking the
best order in the general case is a difficult NP-complete problem.
Approaches to code generation issues: Code generator must always generate the correct code.
It is essential because of the number of special cases that a code generator might face. Some of
the design goals of code generator are:
Correct
Easily maintainable
Testable
Efficient
Page 9 of 23
Here,
All the statements execute in a sequence one after the other.
Flow Graphs-
A flow graph is a directed graph with flow control information added to the basic blocks.
The basic blocks serve as nodes of the flow graph.
Example:
Compute the basic blocks for the given three address statements-
(1) PROD = 0
(2) I = 1
(3) T2 = addr(A) – 4
(4) T4 = addr(B) – 4
(5) T1 = 4 x I
(6) T3 = T2[T1]
(7) T5 = T4[T1]
(8) T6 = T3 x T5
(9) PROD = PROD + T6
(10) I = I + 1
(11) IF I <=20 GOTO (5)
Solution-
We have-
Page 10 of 23
Now, the given code can be partitioned into two basic blocks as-
Page 11 of 23
The required flow graph is-
Page 12 of 23
Optimization process can be applied on a basic block. While optimization, we don't need to
change the set of expressions computed by the block.
There are two type of basic block optimization. These are as follows:
1. Structure-Preserving Transformations
2. Algebraic Transformations
1. Structure preserving transformations:
The primary Structure-Preserving Transformation on basic blocks is as follows:
2. b : = a - d
3. c : = b + c
4. d : = a - d
In the above expression, the second and forth expression computed the same expression. So the
block can be transformed as follows:
1. a : = b + c
2. b : = a - d
3. c : = b + c
4. d : = b
Page 13 of 23
o This can be caused when once declared and defined once and forget to remove them in this case
o Suppose the statement x:= y + z appears in a block and x is dead symbol that means it will never
subsequently used. Then without changing the value of the basic block you can safely remove this
statement.
(c) Renaming temporary variables
A statement t:= b + c can be changed to u:= b + c where t is a temporary variable and u is a
new temporary variable. All the instance of t can be replaced with the u without changing the
basic block value.
(d) Interchange of statement
Suppose a block has the following two adjacent statements:
1. t1 : = b + c
2. t2 : = x + y
These two statements can be interchanged without affecting the value of block when value of
t1 does not affect the value of t2.
2. Algebraic transformations:
o In the algebraic transformation, we can change the set of expression into an
algebraically equivalent set. Thus the expression x:= x + 0 or x:= x *1 can be eliminated
from a basic block without changing the set of expression.
1. a:= b + c
2. e:= c +d +b
The following intermediate code may be generated:
1. a:= b + c
Page 14 of 23
2. t:= c +d
3. e:= t + b
• A code generator generates target code for a sequence of three- address statements and
effectively uses registers to store operands of the statements.
• For example: consider the three-address statement a := b+c It can have the following
sequence of codes:
ADD Rj, Ri Cost = 1
(or)
ADD c, Ri Cost = 2
(or)
MOV c, Rj Cost = 3
ADD Rj, Ri
Register and Address Descriptors:
• 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.
• An address descriptor stores the location where the current value of the name can be found at
run time.
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 function getreg to 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 instruction MOV y’ , L to place a copy of y in L.
Page 15 of 23
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
Generating Code for Assignment Statements:
• The assignment d : = (a-b) + (a-c) + (a-c) might be translated into the following three-address
code sequence: Code sequence for the example is:
Page 16 of 23
if x < 0 goto z ADD z, R0
MOV R0,x
CJ< z
Page 17 of 23
Optimized code:
y = x + 5;
i = y;
w = y * 3;
2. Constant folding:
The code that can be simplified by user itself, is simplified.
Initial code:
x = 2 * 3;
Optimized code:
x = 6;
3. Strength Reduction:
The operators that consume higher execution time are replaced by the operators consuming
less execution time.
Initial code:
y = x * 2;
4. Optimized code:
y = x + x; or y = x << 1;
Initial code:
y = x / 2;
Optimized code:
y = x >> 1;
5. Null sequences:
Useless operations are deleted.
6. Combine operations:
Several operations are replaced by a single equivalent operation.
Page 18 of 23
Advantage
Heavily used values reside in registers
Disadvantage
Does not consider non-uniform distribution of uses
Need of global register allocation
Local allocation does not take into account that some instructions (e.g. those in loops) execute
more frequently. It forces us to store/load at basic block endpoints since each block has no
knowledge of the context of others.
To find out the live range(s) of each variable and the area(s) where the variable is used/defined
global allocation is needed. Cost of spilling will depend on frequencies and locations of uses.
Register allocation depends on:
Size of live range
Number of uses/definitions
Frequency of execution
Number of loads/stores needed.
Cost of loads/stores needed.
Register allocation by graph coloring
Global register allocation can be seen as a graph coloring problem.
Basic idea:
1. Identify the live range of each variable
2. Build an interference graph that represents conflicts between live ranges (two nodes are
connected if the variables they represent are live at the same moment)
3. Try to assign as many colors to the nodes of the graph as there are registers so that two
neighbors have different colors
Page 19 of 23
Page 20 of 23
4.12 DYNAMIC PROGRAMMING CODE-GENERATION
2. Traverse T, using the cost vectors to determine which subtrees of T must be computed
into memory.
3. 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.
In order to compute the costs C[i] at node n, we view the instructions as tree-rewriting rules,
as in Section 8.9. Consider each template E that matches the input tree at node n. By examining
the cost vectors at the corresponding descendants of n, determine the costs of evaluating the
operands at the leaves of E. For those operands of E that are registers, consider all possible
orders in which the corresponding subtrees of T can be evaluated into registers. In each
ordering, the first subtree corresponding to a register operand can be evaluated using i available
registers, the second using i -1 registers, and so on. To account for node n, add in the cost of
the instruction associated with the template E. The value C[i] is then the minimum cost over
all possible orders.
Page 21 of 23
The cost vectors for the entire tree T can be computed bottom up in time linearly proportional
to the number of nodes in T. It is convenient to store at each node the instruction used to achieve
the best cost for C[i] for each value of i. The smallest cost in the vector for the root of T gives
the minimum cost of evaluating T.
Example: Consider a machine having two registers RO and Rl, and the following
instructions, each of unit cost:
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
Page 22 of 23
children of the root show that the minimum cost of computing the root with one register
available is 5 2 1 = 8.
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.
Dynamic programming techniques have been used in a number of compilers, including the
second version of the portable C compiler, PCC2. The technique facilitates retargeting because
of the applicability of the dynamic programming technique to a broad class of machines.
Page 23 of 23
UNIT – V
• Many transformations can be performed at both the local and global levels. Local
transformations are usually performed first.
Function-Preserving Transformations
• There are a number of ways in which a compiler can improve a program without
changing the function it computes.
➢ Copy propagation
➢ Constant folding
Page 1 of 11
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 already 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;
The optimization using copy propagation can be done as follows:
A=Pi*r*r;
Here the variable x is eliminated
Dead-Code Eliminations:
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 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.
For example:
i=0;
if(i=1)
{
a=b+5;
}
Here, ‘if’ statement is dead code because this condition will never get satisfied.
Page 2 of 11
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 thereby 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, which moves code outside a loop;
➢ Reduction in strength, which replaces and expensive operation by a cheaper one, such as
a multiplication by an addition.
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:
while (i <= limit-2) /* statement does not change limit*/
Code motion will result in the equivalent of
t= limit-2;
while (i<=t) /* statement does not change limit or t */
Page 3 of 11
Induction Variables :
Loops are usually processed inside out. For example consider the loop around B3. Note that
the values of j and t4 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..
Page 4 of 11
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:
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.
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 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.
Page 5 of 11
• There are subtleties that go along with such statements as procedure calls,
assignments through pointer variables, and even assignments to array variables.
Page 6 of 11
Example:
❖ In the flow graph below,
❖ Initial node,node1 dominates every node. *node 2 dominates itself
❖ node 3 dominates all but 1 and 2. *node 4 dominates all but 1,2 and 3.
❖ node 5 and 6 dominates only themselves,since flow of control can skip around either by
goin through the other.
❖ node 7 dominates 7,8 ,9 and 10. *node 8 dominates 8,9 and 10.
❖ node 9 and 10 dominates only themselves.
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}
Page 7 of 11
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}
D(10)={1,3,4,7,8,10}
Natural Loops:
One application of dominator information is in determining the loops of a flow graph suitable
for improvement. There are two essential properties of loops:
❖ A loop must have a single entrypoint, 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
headerOne 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
Example:
In the above graph,
7→4 4 DOM 7
10 →7 7 DOM 10
4→3
8→3
9 →1
The above edges will form loop in flow graph. Given a back edge n → d, we define the natural
loop of the edge to be d plus the set of nodes that can reach n without going through d. Node d
is the header of the loop.
Algorithm: Constructing the natural loop of a back edge.
Input: A flow graph G and a back edge n→d.
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
Page 8 of 11
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.
Procedure insert(m):
if m is not in loop then begin loop := loop U {m};
push m onto stack end;
stack : = empty;
loop : = {d}; insert(n);
while stack is not empty do begin
pop m, the first element of stack, off stack;
for each predecessor p of m do insert(p)
end
Inner loops:
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:
Several transformations require us to move statements “before the header”. Therefore begin
treatment of a loop L by creating a new block, called the preheader. 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. Edges from inside loop L to the header are not changed. Initially
the pre-header is empty, but transformations on L may place statements in it.
Page 9 of 11
Fig. 5.4 Two loops with the same header
Definition:
A flow graph G is reducible if and only if we can partition the edges into two disjoint groups,
forward edges and back edges, with the following properties.
Page 10 of 11
1. The forward edges from an acyclic graph in which every node can be reached from initial
node of G.
2. The back edges consist only of edges where heads dominate theirs tails.
Example:
The above flow graph is reducible. If we know the relation DOM for a flow graph, we can find
and remove all the back edges. The remaining edges are forward 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.
Page 11 of 11