Design and Implementation of Modern Compilers
Design and Implementation of Modern Compilers
)
SEMESTER - II (CBCS)
PAPER II
DESIGN AND IMPLEMENTATION
OF MODERN COMPILERS
SUBJECT CODE: PSCS202
© UNIVERSITY OF MUMBAI
Programme Co-ordinator : Shri Mandar Bhanushe
Head, Faculty of Science and Technology IDOL,
Univeristy of Mumbai – 400098
Course Co-ordinator : Mr Sumedh Shejole
Assistant Professor,
IDOL, University of Mumbai- 400098
1. Introduction to Compilers........................................................................................1
LR parsers, The canonical collection of LR(0) items, Constructing SLR parsing tables,
Constructing canonical LR parsing tables, Constructing LALR parsing tables, Using
ambiguous grammars, An automatic parser generator, Implementation of LR parsing
tables, Constructing LALR sets of items.
19
Unit III: Advanced syntax analysis and basic semantic analysis
20
1
INTRODUCTION TO COMPILERS
Unit Structure
1.0 Objectives
1.1 Introduction
1.2 The structure to compiler
1.2.1 Lexical Analysis
1.2.2 Syntax Analysis
1.2.3 Semantic Analysis
1.2.4 Intermediate Code Generation
1.2.5 Code Optimization
1.2.6 Code Generation
1.2.7 Target Code Generator
1.2.8 Symbol-Table Management
1.3 Lexical Analyzers
1.4 Regular Expressions
1.5 Finite Automata
1.5.1 From Regular to Finite Automata
1.5.2 Minimizing the States of DFA
1.6 Context Free Grammars
1.7 Derivation and Parse Tree
1.8 Parsers
1.8.1 Shift-Reduce Parsing
1.8.2 Operator-Precedence Parsing
1.8.3 Top-Down Parsing
1.8.4 Predictive Parsers
1.9 Summary
1.10 Unit End Exercises
1.0 OBJECTIVES
1
Design and implementation
of Modern Compilers
1.1 INTRODUCTION
Compiler is a software that translate one language into another language.
The compiler converts the code from high-level language (source code) to
low level language (machine code/object code) as shown in figure 1. From
the compiler expected that it will give optimized result in terms of time and
space.
Figure. 1
Figure 2.
1.2.1 LEXICAL ANALYSIS
1. It is the first level of compilation process.
2. At first it takes code as input from source code. Then it starts to
convert the data.
3. It reads the source code one character at a time and then
convert this source code into meaningful lexemes.
4. These lexemes are represented as a token in lexical analyzer.
5. It also removes the white space and comments.
6. It also uses to check and remove the lexical errors.
7. It reads the character or values from left to right.
1.2.2 SYNTAX ANALYSIS
1. It is a second phase of compiler.
2. It takes the input from lexical analysis as tokens then convert these
tokens into parse tree.
3. When tokens convert into parse tree it will follow the rules of source
code grammar.
4. These grammar codes as known as context free grammar.
5. This phase analyzes the parser and then check the input
expressions that are syntactically correct or not.
1.2.3 SEMANTIC ANALYSIS
1. This level checks source code for semantic consistency with
language definition for that it uses the syntax tree and for the
information symbol table. 3
Design and implementation 2. It collects all the information and checks the validity for variables,
of Modern Compilers
keyword, data and save it into syntax tree or in the symbol table.
3. In this analysis every operator checks weather it is having matching
operands.
4. Type checking and flow checking is an important part of Semantic
analysis.
5. Language specification may allow type conversion also it is known as
coercions.
6. It also checks weather the language follows the rules or not.
7. It also verifies the parser tree of syntax analyzer.
For instance, coercions appear in figure 3. Assume that Interest, principal,
rate have been declared to be floating point number and lexeme 70 is
itself forms of an integer. The (*) operator is concern to a floating-point
number rate and the integer value 70. In this case, integer value is
translated into floating point number.
Figure 3
44
1.2.4 INTERMEDIATE CODE GENERATION Introduction to Compilers
When compiler convert the source code into target code then compiler
create one or more intermediate code. Syntax tree are the intermediate
representation of syntax and semantic analysis. This code is similar for all
the compilers. This intermediate representation having two properties
1. To convert into the object code or target machine
2. It produces the result in easy manner.
This is an example of intermediate code. This code consists three operators.
It is also known as three-address code. Each instruction consists assignment
operator. l1, l2, l3 are the temporary name that hold the values. In l1
statement integer value is converted into floating point value. In l2
statement multiplication operator are used. In l3 statement addition operator
are used.
E.g.
l1= inttofloat(70)
l2= id3*l1
l3= id2+l2
Id1=l3
1.2.5 CODE OPTIMIZATION
1. In the code optimization step is to improve the intermediate code
performance for better target code result. In the code optimizer firstly
decide the code should be small, it will be giving the result very
quickly and it will consume less power.
2. One special point is that the code should be user friendly.
3. The code optimizer also can reduce the compilation and execute
time of compiler when it compiles the code.
4. This example shows that the conversion of integer value into floating
point (60.0) at once after that it will use previous result.
l1= id3*60.0
Id1= id2+l1
1.2.6 CODE GENERATION
1. Code generation phase is an important part that takes the intermediate
code value as a input and writes the code for target language.
2. Intermediate instructions are converted into sequence of target
instruction code that perform a particular task.
3. During the code generation firstly decide about the variable names,
keywords, operation which gives the result as per the requirement.
4. Example for code generation. F letter is use for the floating-point
value and R1, R2 are the intermediate code and the first value of
each statement specify the destination means where statements result
will store.
5. # Symbol specifies the 70.0 is treated as immediate constants. E.g.
LDF R2, id3
5
Design and implementation MULF R2, R2, #70.0 LDF R1, id2
of Modern Compilers
ADDF R1, R1, R2 STF id1, R1
1.2.7 TARGET CODE GENERATER
1. After the completion of code generation phase execute that code and
user will get the desired result.
2. If the result according to the requirements, then solve another
problem if the result will not come according to the user
requirements, then do some changes till the desired result will come.
3. This is final phase of compiler.
4. All the phases of compiler are divided into two parts:
1. Front end
2. Back end
1. Front end
In this phase all the phases come viz. lexical analysis, syntax
analysis, semantic analysis and Intermediate code generation.
2. Back end
Code optimization and code generation phases comes under
back-end section.
1.2.8 SYMBOL-TABLE MANAGMENT
1. Symbol table is a data structure that consist all the variable name,
keyword, fields name.
2. With the help of symbol table user can easily store and get the data for
each record with name quickly.
3. It collects the information for attribute of a name.
4. It also provides the detail or information for the storage, type, and its
scope.
5. Different kind of data structure techniques are used to create a
symbol table. Some of techniques are:
1. Linked list
2. Hash table
3. Tree
E.g. int sum (int x, int y) {
add=0; add=x+y; return add;
}
Operations on Symbol table
1. Allocates the operations on symbol table
2. Insert the operations on symbol table
66
3. Set_attributes Introduction to Compilers
4. Get_attributes
5. Free operation
6. Look up operation
Table 2.
10
10
5. Finite automata having two states, Accept or Reject state. When the Introduction to Compilers
automata reach its final state, it means the string processed
successfully.
6. Finite automata consist of:
• A finite set T of M states.
• Start state.
• Accepting or final state.
• Moving from one state to another state use transition function.
Definition of FA
11
Design and implementation
of Modern Compilers
Figure 7. DFA
In the following diagram 7 it is showing that q0 is the initial state. q1 is the
final state, when a input apply on state then the next state will become q1
that is final state. When b input apply on q0 state then the next state will
be q2. State q1 having a self-loop.
Definition of DFA
DFA having 5 tuples.
1. Q: Represent the states.
2. ∑: To represent input symbol.
3. q0: Initial state.
4. F: Final state
5. δ: transition function
Transition function represented as:
δ: Q x ∑ ->Q
13
Design and implementation E.g.
of Modern Compilers
1. Q = {q0,q1,q2}
2. ∑= {a,b}
3. q0 = {q0}
4. F= {q1}
Figure 8.
Transition function: Table 4 represent the transition function
Table 4.
1.5.1 CONVERSION FROM REGULAR EXPRESSION TO
FINITE AUTOMATA
To convert the regular expression to finite automata, use subset method
some steps are:
Step 1 − Construct a Transition diagram for a given RE by using non-
deterministic finite automata (NFA) with ε moves.
Step 2 − Convert NFA with ε to NFA without ε.
Step 3 − Convert the NFA to the equivalent Deterministic Finite Automata
(DFA).
14
14
Example to convert R.E to Finite automata. Introduction to Compilers
15
Design and implementation 1.5.2 MINIMIZING THE STATES OF DFA
of Modern Compilers
Minimization means reducing the number of states of FA. There are some
steps to minimize DFA.
Step 1: Remove all the states that are unreachable from the initial state via
any set of the transition of DFA.
Step 2: Draw the transition table for all pair of states.
Step 3: Now split the transition table into two tables T1 and T2. T1 contains
all final states, and T2 contains non-final states.
Step 4: Find similar rows from T1 such that:
1. δ (q, a) = p
2. δ (r, a) = p
That means, find the two states which have the same value of a and b and
remove one of them.
Step 5: Repeat step 3 until we find no similar rows available in the transition
table T1.
Step 6: Repeat step 3 and step 4 for table T2 also.
Step 7: Now combine the reduced T1 and T2 tables. The combined
transition table is the transition table of minimized DFA.
Example:
Solution:
Step 1: In the given DFA, q3 and q5 are the unreachable states so remove
them.
16
16
Step 2: For the other states draw transition table. Introduction to Compilers
Step 4: Set 1 doesn’t have any similar rows so it will be the same.
Step 5: In set 2, row 1 and row 2 having similar states q4 and q6 on 0
and 1.so skip q6 and replace with q4.
17
Design and implementation
of Modern Compilers
Minimize DFA shown as:
1. S ⇒ bSb
2. S ⇒ baSab
3. S ⇒ baaSaab
4. S ⇒ baacaab
Applying the production S → bSb, S → aSa recursively and at last we get
the final production S →c, now we get the final string baacaab.
Classification of Context Free Grammars
Context free grammar are divided into two properties:
1. Number of strings it generates.
Figure 9.
1. Leftmost Derivation-
22
22
1.8 PARSERS Introduction to Compilers
Figure 12.
Top-Down Parsing
1. Top-down parsing is called as recursive or predictive parsing.
2. To construct a parse tree use, bottom-up parsing.
3. In top down parsing the process start from the start symbol and
convert it into input symbol.
4. Top-down parser are categories into 2 parts: Recursive descent
parser, and non-recursive descent parser.
(i) Recursive descent parser:
It is also called as Brute force parser or backtracking parser.
(ii) Non-recursive descent parser:
To generates the parse tree, use parsing tree rather backtracking.
23
Design and implementation Bottom-up parsing
of Modern Compilers
1. Bottom-up parsing is called as shift-reduce parsing.
2. To construct a parse tree use, bottom-up parsing.
3. In bottom up parsing the process start from the start symbol
and design a parse tree from the start symbol by touching out
the string from rightmost derivation in reverse.
Example: Production
1. T → P
2. P→ P * E
3. P → id
4. E → P
5. E → id
Parse Tree representation of input string "id * id" is as follows:
Figure 13.
Bottom-up parsing having various parsing techniques.
1. Shift-Reduce parser
2. Operator Precedence parser
24
24
3. Table Driven LR parser Introduction to Compilers
• LR( 1 )
• SLR( 1 )
• CLR ( 1 )
• LALR( 1 )
Further Bottom-up parser is classified into 2 types: LR parser, and
Operator precedence parser. LR parser is of 4 types:
(a). LR(0) (b). SLR(1) (c). LALR(1) (d). CLR(1)
i) LR parser:
It generates the parse tree for a particular grammar by using
unambiguous grammar. For the derivation it follows right most
derivation. LR parser having 4 types:
(a). LR(0) (b). SLR(1) (c). LALR(1) (d). CLR(1)
ii) Operator precedence parser:
It generates the parse tree for a particular grammar or string with a
condition i.e., two consecutives non terminal and epsilon doesn’t
come at the right-hand side of any production.
1.8.1 SHIFT-REDUCE PARSING
1. In Shift reduce parsing reduce a string of a grammar from the start
symbol as figure 14.
2. a string of a grammar from the start symbol.
3. It uses a stack to hold the grammar and to hold the string it uses input
tape.
Figure 14.
4. It performs two actions: shift and reduce so it is known as shift
reduce parsing,
5. When shifting process start then the current symbol of string move
to the stack.
6. Shift reduce parsing having 2 categories:
Example
Operator Precedence Parsing
LR-Parser
A → A+A A → A-A A → (A) A → a
Input string: x1-(x2+x3)
25
Design and implementation Parsing table: Describe in Table 5.
of Modern Compilers
Table 5.
1.8.2 OPERATOR-PRECEDENCE PARSING
1. It is related to small type of class operator grammar.
2. If the grammar is the type of operator precedence, then it should have
two properties:
3. The production should not have any a∈ operator at right side.
4. Non-terminal symbols are not adjacent.
5. It can only perform the operation between the terminal symbols of
grammar. It doesn’t take any notice to non-terminals.
6. Operator precedence are categories into three relations ⋗ ⋖ ≐.
x ⋗ y means that terminal “x “has greater precedence than the
terminal “y”.
x ⋖ y means that terminal “y “has higher precedence than the terminal
“x”.
x ≐ y means that precedence of terminal “x and y “are equal.
7. Operator precedence parser comes under bottom-up parser that
interprets with operator grammar.
8. In this parser ambiguous grammar is not allowed.
9. There are 2 ways that determine which precedence relation should
hold the pair of terminals.
26
26
1. Use precedence of order and conventional associativity. Introduction to Compilers
Table 6.
Parsing Action
1. At the end of string use $ symbol.
2. After that scan input string from left to right until ⋗ is encountered.
3. Now scan the string towards left above all the equal precedence
Until first left ⋖ is encountered.
4. Now handle all the string value that lie between ⋖ and ⋗.
5. If at last we get $ it means the parsing is successfully accepted.
Example: Grammar:
S → S+E/E E → E* F/F F → id
Given string:
1. w = id + id * id
Let us consider a parse tree for it as follow:
27
Design and implementation Figure 15. Parse tree
of Modern Compilers
According to parse tree, we can design operator precedence table describe
in table 7:
Figure 16.
Disadvantage of operator precedence parser
If we have n number of operators then the size for table will be n*n. Then
complexity will be 0(n 2). To decrease the size of table, use operator
function table. Operator precedence parsers use precedence function
that plot terminal symbols to integer, and the relations between the symbol
are affected by numerical comparison. Parsing table enclosed by two
precedence function f and g that plot terminal symbols to integers.
1. f(a) < g(b) takes the precedence to b
2. f(a) = g(b) a and b have the same precedence
3. f(a) > g(b) takes the precedence over b
1.8.3 TOP-DOWN PARSING
1. Top-down parsing is called as recursive or predictive parsing.
28
28
2. To construct a parse tree use, bottom-up parsing. Introduction to Compilers
3. In top down parsing the process start from the start symbol and
convert it into input symbol.
4. It always uses left most derivation.
5. It is a parser that generate the parse for a particular string
through the help of grammar production.
6. Top-down parser categories into two parts as shown in figure 17.
a) Back tracking
b) Non-backtracking
• Predictive Parser
• LL Parser
Figure. 17
Recursive Descent Parsing
Recursive descent parser is a top-down parser. It starts to construct the parse
tree from the top and it reads the input from left to right. It is use to process
each terminal and non-terminal entities. This technique is use to make the
parse tree for that parser recursively pass the input. Context free grammar is
recursive in nature so this grammar uses in recursive descent parsing.
A technique that doesn’t require any backtracking that are known as
predictive parser.
29
Design and implementation Back-tracking
of Modern Compilers
Top-down parser match the input string against the production rule from
the start node. Example for CFG:
X → pYd | pZd
Y → ka | wa
Z → ae
It will start the production rule from the root node i.e., X and start to find
the match of a letter from left most input i.e., ‘p’. The production of X (X →
pYd) match with it. Then top-down parser proceeds to the next input string
i.e., ‘w’. Now parser find the match for non-terminal ‘Y’ and check the
production for (Y → ka). This string doesn’t match with input string. So
top-down parser backtracks to get the result of Y. (Y → wa). The parser
matches complete string in ordered manner. Hence string is accepted as
shown in figure 18.
Figure 18.
Example for Top-down parser:
Input string: “adbc”
Representation of Parse Tree for input string "adbc" is as shown in
figure 19
30
30
Introduction to Compilers
31
Design and implementation
of Modern Compilers
Figure 20.
Predictive parsing uses a parsing table to parse the input and for storing the
data in the parse table it uses stack and then prepare the parse tree. Stack an
input string contains $ symbol at the end. $ symbol represent the stack is
empty and the inputs are used. Parser use the parsing table for the
combination of input and stack elements. Describe in figure 20.
Describe the processing of parsers
32
32 Figure 21.
Recursive descent parser has more than one input production then it chooses Introduction to Compilers
one production from the production, whereas predictive parser use table to
generate the parse tree.
Predictive Parser Algorithm:
1. Construct a transition diagram (DFA/NFA) for each production of
grammar.
2. By reducing the number of states, optimize DFA to produce the
final transition diagram.
3. Simulate the string on the transition diagram to parse a string.
4. If the transition diagram reaches an accept state after the input is
consumed, it is parsed.
LL Parser:
Figure 22.
1.9 SUMMARY
33
Design and implementation 3. Compilers are classified into three categories
of Modern Compilers
a. Single pass compiler
b. Two pass compilers
c. Multi pass compiler
4. Compiler is a software that convert high level language into machine
level language.
5. When compiler convert the one language into another language then
it doesn’t change the meaning of code it only finds the syntax errors.
6. In lexical analyzer helps to identify the tokens from symbol table. 7.
Lexical analysis implemented with DFA.
8. Lexical analyzer removes the white space and comments. 9. Lexical
analyzer breaks the syntax into series of tokens.
10. Syntactic analysis collects all the information and checks the validity
for variables, keyword, data and save it into syntax tree or in the
symbol table.
11. Top-down parsing is called as recursive or predictive parsing. 12.
Operator precedence are categories into three relations ⋗ ⋖ ≐.
13. Parser is a compiler that divide the data into smaller elements that
get from lexical analysis phase.
14. DFA is a collection of 5 tuples (Q, 𝛴, 𝛿, q0, F)
a. Q : To represent the Finite state
b. ∑: To represent the input symbol
c. q0: To represent the initial state.
d. F: to represent the final state.
e. δ: perform the transition function on string
15. Derivation having two parts
1. Left most derivation
2. Right most derivation
16. CFG (context free grammar) is a formal grammar. It is used to
produce string in a formal language.
17. Predictive parsing uses a parsing table to parse the input and for
storing the data in the parse table it uses stack and then prepare the
parse tree.
18. Operator precedence parsing can only perform the operation between
the terminal symbols of grammar. It doesn’t take any notice to non-
terminals.
19. A recursive grammar classified into 3 types
a. General recursive grammar
b. Left recursive grammar
34
34
c. Right recursive grammar Introduction to Compilers
1.10 EXCERSICE
1) Define Complier?
2) What is the difference between compiler and interpreter? 3) What is
symbol table?
4) What are the phases/structure of compiler?
5) Define applications of compiler?
6) The regular expression (1*0)*1* denotes the same set as
(A) 0*(10*)*
(B) 0 + (0 + 10)*
(C) (0 + 1)* 10(0 + 1)*
(D) none of these
7) Which one of the following languages over the alphabet {1,0} is
described by the regular expression? (1+0)*1(1+0)*1(1+0)*
8) Which of the following languages is generated by given grammar? X
-> bS | aS | ∊
9) DFA with ∑ = {0, 1} accepts all ending with 1.
10) Assume FA accepts any three digit binary value ending in digit 0 FA
= {Q(q0, qf), Σ(0,1), q0, qf, δ}
11) Consider the grammar
S → aB | bA
A → a | aS | bAA
B → b| bS | aBB
For the string w = aabbabab, find-
1. Leftmost derivation
2. Rightmost derivation
3. Parse Tree
12) Consider the grammar-
S → X1Y
X → 0X | ∈
Y → 0Y | 1Y | ∈
For the string w = 11010, find-
35
Design and implementation 1. Leftmost derivation
of Modern Compilers
2. Rightmost derivation
3. Parse Tree
13) Construct Regular expression for the language L= {w ε{1,0}/w.
14) Define the parts of string?
15) Define DFA and NFA?
16) Differentiate between Recursive descent and Predictive parser?
17) Describe the language denoted by the R.E. (0/1)*0(0/1)(0/1).
18) Define the steps of lexical analyzer?
19) Explain Parsers and its types?
20) Write the R.E. for the set of statements over {x, y, z} that contain an
even no of x’s.
21) What is parse tree?
22) Write down the operations on languages?
23) What is regular expression? Write down the rules for R.E? 24) Define
the types of top-down parser?
25) Explain Top-down parser and bottom-up parser?
36
36
2
AUTOMATIC CONSTRUCTION OF
EFFICIENT PARSERS
Unit Structure
2.1 Objectives
2.2 Introduction
2.3 Overview
2.4 Basic concepts related to Parsers
2.4.1 The Role of the Parser
2.4.2 Syntax Error Handling: -
2.5 Summary
2.6 Reference for Further Reading
2.1 OBJECTIVES
2.2 INTRODUCTION
Every programming language has rules that prescribe the syntactic structure
of well formed programs. The syntax of programming language constructs
can be described by context- free grammars or BNF (Back us – Naur Form)
notation. For certain classes of grammars, we can automatically construct
an efficient parser that determines if a source program is syntactically well
formed. In addition to this, the parser construction process can reveal
syntactic ambiguities and other difficult-to-parse constructs that does not
remain undetected in the initial design phase of a language and its compiler.
2.3 OVERVIEW
At the end of this chapter you will know and understand the following
concepts in detail :-
1) Parsing methods used in compilers.
2) Basic concepts.
3) Techniques used in Efficient Parsers.
4) Algorithms – to recover from commonly occurring errors.
37
Design and implementation
of Modern Compilers
2.4. BASIC CONCEPTS RELATED TO PARSERS
There are three general types of parsers for grammar’s. Universal parsing
methods such as the Cocke-Younger-Kasami algorithm and Earley’s
algorithm can parse any grammar. But these methods are inefficient to use
in production compilers. The Efficient methods commonly used in
compilers are as follows:-
38
38
2.4.1.1 Syntax Error Handling:- Design and Implementation
of Modern Compiler
Planning the error handling right from the start can both simplify the
structure of a compiler and improve its response to errors.
Programs can contain errors at many different levels as follows:-
a) Lexical, such as misspelling an identifier, keyword or an operator.
b) Syntactic, such as an arithmetic expression with unbalanced
parenthesis.
c) Semantic, such as an operator applied to an incompatible operand.
d) Logical, such as an infinitely recursive call.
For recovery from syntax errors, the error handler in a parser has
simple-to-state goals:-
a) It should report the presence of errors clearly and accurately.
b) It should recover from each error quickly enough to be able to detect
subsequent errors.
c) It should not significantly slow down the processing of correct
programs.
2.4.1.2 Error – Recovery Strategies:-
There are many different general strategies that a parser can employ to
recover from syntactic error. Here are few methods listed down which have
broad applicability. :-
a) Panic mode – Simplest and adequate method and panic mode
recovery does not work in an infinite loop.
b) Phrase level – local correction, one must be careful to choose
replacements that do not lead to infinite loops. Difficulty in coping
with situations in which the actual error has occurred before the point
of detection.
c) Error productions – One can generate appropriate error diagnostics
to indicate the erroneous construct that has been recognized in the
input.
d) Global corrections – Too costly to implement in terms of time and
space.
2.4.2 CONTEXT FREE GRAMMARS: -
A context-free grammar (grammar for short) consists of terminals, non-
terminals, a start symbol, and productions.
39
Design and implementation 1. Terminals are the basic symbols from which strings are formed. The
of Modern Compilers
word "token" is a synonym for "terminal" when we are talking about
grammars for programming languages.
2. Non terminals are syntactic variables that denote sets of strings. They
also impose a hierarchical structure on the language that is useful for
both syntax analysis and translation.
3. In a grammar, one non terminal is distinguished as the start symbol,
and the set of strings it denotes is the language defined by the
grammar.
4. The productions of a grammar specify the way the terminals and non-
terminals can be combined to form strings. Each production consists
of a non terminal, followed by an arrow, followed by a string of non-
terminals and terminals.
Inherently recursive structures of a programming language are
defined by a context-free Grammar. In a context-free grammar, we
have four triples G( V,T,P,S). Here , V is finite set of terminals (in our
case, this will be the set of tokens) T is a finite set of non-terminals
(syntactic- variables).P is a finite set of productions rules in the
following form A → α where A is a non- terminal and α is a string of
terminals and non-terminals (including the empty string).S is a start
symbol (one of the non-terminal symbol).
L(G) is the language of G (the language generated by G) which is a
set of sentences. A sentence of L(G) is a string of terminal symbols of
G. If S is the start symbol of G then ω is a sentence of L(G) if S ω
where ω is a string of terminals of G. If G is a context-free grammar
(G) is a context-free language. Two grammars G1 and G2 are
equivalent, if they produce same grammar.
Consider the production of the form S α, if α contains non-terminals,
it is called as a sentential form of G. If α does not contain non-
terminals, it is called as a sentence of G.
Example: Consider the grammar for simple arithmetic expressions:
expr → expr op expr
expr → ( expr )
expr → - expr
expr → id
op → +
op → -
op → *
op → /
op → ^
Terminals : id + - * / ^ ( )
Non-terminals : expr , op
40
40 Start symbol : expr
2.4.2.1 Notational Conventions: Design and Implementation
of Modern Compiler
1. These symbols are terminals:
i. Lower-case letters early in the alphabet such as a, b, c.
ii. Operator symbols such as +, -, etc.
iii. Punctuation symbols such as parentheses, comma etc.
iv. Digits 0,1,…,9.
v. Boldface strings such as id or if (keywords)
2. These symbols are non-terminals:
i. Upper-case letters early in the alphabet such as A, B, C..
ii. The letter S, when it appears is usually the start symbol.
iii. Lower-case italic names such as expr or stmt.
3. Upper-case letters late in the alphabet, such as X,Y,Z, represent
grammar symbols, that is either terminals or non-terminals.
4. Greek letters α , β , γ represent strings of grammar symbols.
e.g., a generic production could be written as A → α.
5. If A → α1 , A → α2 , . . . . , A → αn are all productions with A , then
we can write A
→ α1 | α2 |. . . . | αn , (alternatives for A).
6. Unless otherwise stated, the left side of the first production is the start
symbol.
Using the shorthand, the grammar can be written as:
E → E A E | ( E ) | - E | id
A→+|-|*|/|^
2.4.2.2 Derivations:
A derivation of a string for a grammar is a sequence of grammar rule
applications that transform the start symbol into the string. A derivation
proves that the string belongs to the grammar's language.
41
Design and implementation 2.4.3.2.1. To create a string from a context-free grammar:
of Modern Compilers
• Begin the string with a start symbol.
• Apply one of the production rules to the start symbol on the left-hand
side by replacing the start symbol with the right-hand side of the
production.
Eliminating Ambiguity:
An ambiguous grammar can be rewritten to eliminate the ambiguity. e.g.
Eliminate the ambiguity from “dangling-else” grammar,
44
44
stmt → if expr then stmt Design and Implementation
of Modern Compiler
| if expr then stmt else stmt
| other
Match each else with the closest previous unmatched then. This
disambiguity rule can be incorporated into the grammar.
stmt → matched_stmt | unmatched_stmt
matched_stmt →if expr then matched_stmt else matched_stmt
| other
unmatched_stmt → if expr then stmt
| if expr then matched_stmt else unmatched_stmt
This grammar generates the same set of strings, but allows only one parsing
for string.
Removing Ambiguity by Precedence & Associativity Rules:
An ambiguous grammar may be converted into an unambiguous grammar
by implementing:
– Precedence Constraints
– Associativity Constraints
These constraints are implemented using the following rules:
Rule-1:
• The level at which the production is present defines the priority of the
operator
contained in it.
– The higher the level of the production, the lower the priority of operator.
– The lower the level of the production, the higher the priority of operator.
Rule-2:
• Rules:
46
46
Design and Implementation
of Modern Compiler
47
Design and implementation 2.4.2.3.1 Shift-Reduce Parsing:
of Modern Compilers
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 string to be recognized is abbcde. We want to reduce the string to S.
Steps of reduction:
Abbcde (b,d can be reduced)
aAbcde (leftmost b is reduced)
aAde (now Abc,b,d qualified for reduction)
aABe (d can be reduced)
S
Each replacement of the right side of a production by the left side in the
above example is
called reduction, which is equivalent to rightmost derivation in reverse.
Handle:
A substring which is the right side of a production such that replacement of
that substring by
the production left side leads eventually to a reduction to the start symbol,
by the reverse of a
rightmost derivation is called a handle.
48
48
2.4.2.4 Stack Implementation of Shift-Reduce Parsing: Design and Implementation
of Modern Compiler
There are two problems that must be solved if we are to parse by handle
pruning. The first is to locate the substring to be reduced in a right-sentential
form, and the second is to determine what production to choose in case there
is more than one production with that substring on the right side.
A convenient way to implement a shift-reduce parser is to use a stack to
hold grammar symbols and an input buffer to hold the string w to be parsed.
We use $ to mark the bottom of the stack and also the right end of the input.
Initially, the stack is empty, and the string w is on the input, as follows:
STACK INPUT
$ w$
The parser operates by shifting zero or more input symbols onto the stack
until a handle is on top of the stack. The parser repeats this cycle until it has
detected an error or until the stack contains the start symbol and the input is
empty:
STACK INPUT
$S $
Example: The actions a shift-reduce parser in parsing the input string
id1+id2*id3, according to the ambiguous grammar for arithmetic
expression.
49
Design and implementation In above fig. Reductions made by Shift Reduce Parser.
of Modern Compilers
While the primary operations of the parser are shift and reduce, there are
actually four possible actions a shift-reduce parser can make:
(1) shift, (2) reduce,(3) accept, and (4) error.
• In a shift action, the next input symbol is shifted unto the top of the
stack.
• In a reduce action, the parser knows the right end of the handle is at
the top of the stack. It must then locate the left end of the handle within
the stack and decide with what non-terminal to replace the handle.
• In an error action, the parser discovers that a syntax error has occurred
and calls an error recovery routine.
Figure below represents the stack implementation of shift reduce parser
using unambiguous grammar.
50
50
This is not an operator grammar, because the right side EAE has two Design and Implementation
of Modern Compiler
consecutive nonterminals. However, if we substitute for A each of its
alternate, we obtain the following
operator grammar:
E→E + E |E – E |E * E | E / E | ( E ) | E ^ E | - E | id
In operator-precedence parsing, we define three disjoint precedence
relations between pair of terminals. This parser relies on the following three
precedence relations.
51
Design and implementation
of Modern Compilers
52
52
Above fig. – Operator Precedence Relation Table Design and Implementation
of Modern Compiler
• Terminals that can be the first terminal in a string derived from that
non-terminal.
53
Design and implementation • LEADING(A)={ a| A=>+
of Modern Compilers
γaδ },where γ is ε or any non-terminal, =>+ indicates derivation in one or
more steps, A is a non-terminal.
Algorithm for LEADING(A):
{
1. ‘a’ is in LEADING(A) is A→ γaδ where γ is ε or any non-terminal.
2.If ‘a’ is in LEADING(B) and A→B, then ‘a’ is in LEADING(A).
}
Computation of TRAILING:
• Terminals that can be the last terminal in a string derived from that
non-terminal.
• TRAILING(A)={ a| A=>+
γaδ },where δ is ε or any non-terminal, =>+ indicates derivation in one or
more steps, A is a non-terminal.
Algorithm for TRAILING(A):
{
1. ‘a’ is in TRAILING(A) is A→ γaδ where δ is ε or any non-terminal.
2.If ‘a’ is in TRAILING(B) and A→B, then ‘a’ is in TRAILING(A).
}
Example 1: Consider the unambiguous grammar,
E→E + T
E→T
T→T * F
T→F
F→(E)
F→id
Step 1: Compute LEADING and TRAILING:
LEADING(E)= { +,LEADING(T)} ={+ , * , ( , id}
LEADING(T)= { *,LEADING(F)} ={* , ( , id}
54
54
LEADING(F)= { ( , id} Design and Implementation
of Modern Compiler
TRAILING(E)= { +, TRAILING(T)} ={+ , * , ) , id}
TRAILING(T)= { *, TRAILING(F)} ={* , ) , id}
TRAILING(F)= { ) , id}
Step 2: After computing LEADING and TRAILING, the table is
constructed between all the terminals in the grammar including the ‘$’
symbol.
56
56
Above fig. - Parse the input string(id+id)*id$ Design and Implementation
of Modern Compiler
2.4.2.6 Precedence Functions:
Compilers using operator-precedence parsers need not store the table of
precedence relations. In most cases, the table can be encoded by two
precedence functions f and g that map terminal symbols to integers. We
attempt to select f and g so that, for symbols a and b.
1. f (a) < g(b) whenever a<·b.
2. f (a) = g(b) whenever a = b. and
3. f(a) > g(b) whenever a ·> b.
Algorithm for Constructing Precedence Functions:
1. Create functions fa for each grammar terminal a and for the end of
string symbol.
2. Partition the symbols in groups so that fa and gb are in the same group
if a = b (there can be symbols in the same group even if they are not
connected by this relation).
3. Create a directed graph whose nodes are in the groups, next for each
symbols a and b do: place an edge from the group of gb to the group
of fa if a <· b, otherwise if a ·> b place an edge from the group of fa
to that of gb.
4. If the constructed graph has a cycle then no precedence functions
exist. When there are no cycles collect the length of the longest paths
from the groups of fa and gb respectively.
Precedence Graph.
There are no cycles,so precedence function exist. As f$ and g$ have no out
edges,f($)=g($)=0.The longest path from g+ has length 1,so g(+)=1.There
is a path from gid to f* to g* to f+ to g+ to f$ ,so g(id)=5.The resulting
precedence functions are:
57
Design and implementation
of Modern Compilers
Example 2:
Consider the following grammar, and construct the operator precedence
parsing table and check whether the input string (i) *id=id (ii)id*id=id are
successfully parsed or not?
S→L=R
S→R
L→*R
L→id
R→L
Solution:
1. Computation of LEADING:
LEADING(S) = {=, * , id}
LEADING(L) = {* , id}
LEADING(R) = {* , id}
2. Computation of TRAILING:
TRAILING(S) = {= , * , id}
TRAILING(L)= {* , id}
TRAILING(R)= {* , id}
3. Precedence Table:
58
58
4. Parsing the given input string: Design and Implementation
of Modern Compiler
1. *id = id
2. id*id=id
59
Design and implementation Above fig. – Steps in Top-Down Parse.
of Modern Compilers
The leftmost leaf, labeled c, matches the first symbol of w, hence advance
the input pointer to a, the second symbol of w. Fig 2.21(b) and (c) shows
the backtracking required to match the input string.
Predictive Parser:
A grammar after eliminating left recursion and left factoring can be parsed
by a recursive descent parser that needs no backtracking is a called a
predictive parser. Let us understand how to eliminate left recursion and left
factoring.
Eliminating Left Recursion:
A grammar is said to be left recursive if it has a non-terminal A such that
there is a derivation A=>Aα for some string α. Top-down parsing methods
cannot handle left-recursive grammars. Hence, left recursion can be
eliminated as follows:
If there is a production A → Aα | β it can be replaced with a sequence of
two productions
A → βA'
A' → αA' | ε
Without changing the set of strings derivable from A.
Example : Consider the following grammar for arithmetic expressions:
E → E+T | T
T → T*F | F
F → (E) | id
First eliminate the left recursion for E as
E → TE'
E' → +TE' | ε
Then eliminate for T as
T → FT '
T'→ *FT ' | ε
Thus the obtained grammar after eliminating left recursion is
E → TE'
25
E' → +TE' | ε
T → FT '
T'→ *FT ' | ε
F → (E) | id
60
60
Algorithm to eliminate left recursion: Design and Implementation
of Modern Compiler
1. Arrange the non-terminals in some order A1, A2 . . . An.
2. for i := 1 to n do begin
for j := 1 to i-1 do begin
replace each production of the form Ai → Aj γ
by the productions Ai → δ1 γ | δ2γ | . . . | δk γ.
where Aj → δ1 | δ2 | . . . | δk are all the current Aj-productions;
end
eliminate the immediate left recursion among the Ai- productions
end
Left factoring:
Left factoring is a grammar transformation that is useful for producing a
grammar suitable for predictive parsing. When it is not clear which of two
alternative productions to use to expand a non-terminal A, we can rewrite
the A-productions to defer the decision until we have seen enough of the
input to make the right choice.
If there is any production A → αβ1 | αβ2 , it can be rewritten as
A → αA'
A’ → αβ1 | αβ2
Consider the grammar,
S → iEtS | iEtSeS | a
E→b
Here,i,t,e stand for if ,the,and else and E and S for “expression” and
“statement”.
After Left factored, the grammar becomes
S → iEtSS' | a
S' → eS | ε
E→b
Non-recursive Predictive Parsing:
It is possible to build a non-recursive predictive parser by maintaining a
stack explicitly, rather than implicitly via recursive calls. The key problem
during predictive parsing is that of 26 determining the production to be
applied for a non-terminal. 61
Design and implementation
of Modern Compilers
62
62
Rules for FIRST(): Design and Implementation
of Modern Compiler
1. If X is terminal, then FIRST(X) is {X}.
2. If X → ε is a production, then add ε to FIRST(X).
3. If X is non-terminal and X → aα is a production then add a to FIRST(X).
4. If X is non-terminal and X → Y 1 Y2…Yk is a production, then place a
in FIRST(X) if for some i, a is in FIRST(Yi), and ε is in all of
FIRST(Y1),…,FIRST(Yi-1); that is, Y1,….Yi-1 => ε. If ε is in FIRST(Yj)
for all j=1,2,..,k, then add ε to FIRST(X).
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 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.
Since there are more than one production for an entry in the table, the
grammar is not LL(1) grammar.
2.4.4 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 of lookahead that are
used in making parsing decisions.. When (k) is omitted, it is assumed to be
1.
67
Design and implementation Method :
of Modern Compilers
1. Construct C ={I0, I1, …. In}, the collection of sets of LR(0) items for
G’.
2. State i is constructed from Ii. The parsing functions for state i are
determined as follows:
3. The goto transitions for state i are constructed for all non-terminals A
using the rule: If
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].
Input: An input string w and an LR parsing table with functions action and
goto for grammar G.
Method: Initially, the parser has s0 on its stack, where s0 is the initial state,
and w$ in the input buffer. The parser then executes the following program:
let s be the state on top of the stack and a the symbol pointed to by ip;
end
68
68
else if action[s, a]=reduce A→β then begin Design and Implementation
of Modern Compiler
pop 2* |β |symbols off the stack;
let s‟ be the state now on top of the stack;
push A then goto[s‟, A] on top of the stack;
output the production A→ β
end
else if action[s, a]=accept then
return
else error( )
end
Example: Implement SLR Parser for the given grammar:
1.E→E + T
2.E→T
3.T→T * F
4.T→F
5.F→(E)
6.F→id
Step 1 : Convert given grammar into augmented grammar.
Augmented grammar:
E'→E
35
E→E + T
E→T
T→T * F
T→F
F→(E)
F→id
Step 2 : Find LR (0) items.
69
Design and implementation
of Modern Compilers
70
70
Design and Implementation
of Modern Compiler
71
Design and implementation 2.4.6 Constructing Canonical or LR(1) parsing tables
of Modern Compilers
LR or canonical LR parsing incorporates the required extra information into
the state by redefining configurations to include a terminal symbol as an
added component. LR(1) configurations have the general form:
A –> X1...Xi
• Xi+1...Xj , a
This means we have states corresponding to X1...Xi on the stack and we are
looking to put states corresponding to Xi+1...Xj on the stack and then
reduce, but only if the token following Xj is the terminal a. a is called the
lookahead of the configuration. The lookahead only comes into play with
LR(1) configurations with a dot at the right end:
A –> X1…Xj •, a
This means we have states corresponding to X1...Xj on the stack but we
may only reduce when the next symbol is a. The symbol a is either a
terminal or $ (end of input marker).
With SLR(1) parsing, we would reduce if the next token was any of those
in Follow(A).
With LR(1) parsing, we reduce only if the next token is exactly a. We may
have more than one symbol in the lookahead for the configuration, as a
convenience, we list those symbols separated by a forward slash. Thus, the
configuration A –> u•, a/b/c says that it is valid to reduce u to A only if the
next token is equal to a, b, or c. The configuration lookahead will always be
a subset of Follow(A).
Recall the definition of a viable prefix from the previous handout. Viable
prefixes are those prefixes of right sentential forms that can appear on the
stack of a shift-reduce parser. Formally we say that a configuration [A –>
u•v, a] is valid for a viable prefix α if there is a rightmost derivation S =>*
βAw =>* βuvw where α = βu and either a is the first symbol of w or w is ∂
and a is $.
For example:
S –> ZZ
Z –> xZ | y
There is a rightmost derivation S =>* xxZxy => xxxZxy. We see that
configuration
[Z –> x•Z, x] is valid for viable prefix α = xxx by letting β = xx, A = Z, w
= xy, u = x and
v = Z. Another example is from the rightmost derivation S =>* ZxZ =>
ZxxZ, making
[Z –> x•Z, $] valid for viable prefix Zxx.
72
72
Often we have a number of LR(1) configurations that differ only in their Design and Implementation
of Modern Compiler
lookahead components. The addition of a lookahead component to LR(1)
configurations allows us to make parsing decisions beyond the capability of
SLR(1) parsers. There is, however, a big price to be paid. There will be
more distinct configurations and thus many more possible configurating
sets. This increases the size of the goto and action tables considerably. In
the past when memory was smaller, it was difficult to find storageefficient
ways of representing these tables, but now this is not as much of an issue.
Still, it’s a big job building LR tables for any substantial grammar by hand.
The method for constructing the configurating sets of LR(1) configurations
is essentially the same as for SLR, but there are some changes in the closure
and successor operations because we must respect the configuration
lookahead. To compute the closure of an LR(1) configurating set I:
Repeat the following until no more configurations can be added to state I:
— For each configuration [A –> u•Bv, a] in I, for each production B –> w
in G', and for each terminal b in First(va) such that [B –> •w, b] is not in I:
add [B –> •w, b] to I.
What does this mean? We have a configuration with the dot before the
non-terminal B.
In LR(0), we computed the closure by adding all B productions with no
indication of what was expected to follow them. In LR(1), we are a little
more precise— we add each B production but insist that each have a
lookahead of va. The lookahead will be First(va) since this is what follows
B in this production. Remember that we can compute first sets not just for
a single non-terminal, but also a sequence of terminal and non-terminals.
First(va) includes the first set of the first symbol of v and then if that symbol
is nullable, we include the first set of the following symbol, and so on. If
the entire sequence v is nullable, we add the lookahead a already required
by this configuration.
The successor function for the configurating set I and symbol X is computed
as this:
Let J be the configurating set [A –> uX•v, a] such that [A –> u•Xv, a] is in
I.
successor(I,X) is the closure of configurating set J.
We take each production in a configurating set, move the dot over a symbol
and close on the resulting production. This is basically the same successor
function as defined for LR(0), but we have to propagate the lookahead
when computing the transitions.
We construct the complete family of all configurating sets F just as we did
before. F is initialized to the set with the closure of [S' –> S, $]. For each
configurating set I and each grammar symbol X such that successor(I,X) is
not empty and not in F, add successor (I,X) to F until no other configurating
set can be added to F. 73
Design and implementation LR(1) grammars
of Modern Compilers
Every SLR(1) grammar is a canonical LR(1) grammar, but the canonical
LR(1) parser may have more states than the SLR(1) parser. An LR(1)
grammar is not necessarily SLR(1), the grammar given earlier is an
example. Because an LR(1) parser splits states based on differing
lookaheads, it may avoid conflicts that would otherwise result if using the
full follow set.
A grammar is LR(1) if the following two conditions are satisfied for each
configurating set:
1. For any item in the set [A –> u•xv, a] with x a terminal, there is no
item in the set of the form [B –> v•, x]. In the action table, this
translates no shift-reduce conflict for any state. The successor
function for x either shifts to a new state or reduces, but not both.
2. The lookaheads for all complete items within the set must be disjoint,
e.g. set cannot have both [A –> u•, a] and [B –> v•, a] This translates
to no reduce-reduce conflict on any state. If more than one
non-terminal could be reduced from this set, it must be possible to
uniquely determine which is appropriate from the next input token.
As long as there is a unique shift or reduce action on each input
symbol from each state, we can parse using an LR(1) algorithm. The
above state conditions are similar to what is required for SLR(1), but
rather than the looser constraint about disjoint follow sets and so on,
canonical LR(1) computes a more precise notion of the appropriate
lookahead within a particular context and thus is able to resolve
conflicts that SLR(1) would encounter.
2.4.7 LALR Table Construction
A LALR(1) parsing table is built from the configurating sets in the same
way as canonical LR(1); the lookaheads determine where to place reduce
actions. In fact, if there are no mergable states in the configuring sets, the
LALR(1) table will be identical to the corresponding LR(1) table and we
gain nothing.
In the common case, however, there will be states that can be merged and
the LALR table will have fewer rows than LR. The LR table for a typical
programming language may have several thousand rows, which can be
merged into just a few hundred for LALR. Due to merging, the LALR(1)
table seems more similar to the SLR(1) and LR(0) tables, all three have the
same number of states (rows), but the LALR may have fewer reduce
actions—some reductions are not valid if we are more precise about the
lookahead. Thus, some conflicts are avoided because an action cell with
conflicting actions in SLR(1) or LR(0) table may have a unique entry in an
LALR(1) once some erroneous reduce actions have been eliminated.
74
74
Brute Force? Design and Implementation
of Modern Compiler
There are two ways to construct LALR(1) parsing tables. The first (and
certainly more obvious way) is to construct the LR(1) table and merge the
sets manually. This is sometimes referred as the "brute force" way. If you
don’t mind first finding all the multitude of states required by the canonical
parser, compressing the LR table into the LALR version is straightforward.
1. Construct all canonical LR(1) states.
2. Merge those states that are identical if the lookaheads are ignored, i.e.,
two states being merged must have the same number of items and the
items have the same core (i.e., the same productions, differing only in
lookahead). The lookahead on merged items is the union of the
lookahead from the states being merged.
3. The successor function for the new LALR(1) state is the union of the
successors of the merged states. If the two configurations have the
same core, then the original successors must have the same core as
well, and thus the new state has the same successors.
4. The action and goto entries are constructed from the LALR(1) states
as for the canonical LR(1) parser. Consider the LR(1) table for the
grammar given on page 1 of this handout. There are nine states.
Looking at the configurating sets, we saw that states 3 and 6 can be merged,
so can 4 and 7, and 8 and 9. Now we build this LALR(1) table with the six
remaining states:
75
Design and implementation Having to compute the LR(1) configurating sets first means we won’t save
of Modern Compilers
any time or effort in building an LALR parser. However, the work wasn’t
all for naught, because when the parser is executing, it can work with the
compressed table, thereby saving memory. The difference can be an order
of magnitude in the number of states.
However there is a more efficient strategy for building the LALR(1) states
called step-by-step merging. The idea is that you merge the configurating
sets as you go, rather than waiting until the end to find the identical ones.
Sets of states are constructed as in the LR(1) method, but at each point
where a new set is spawned, you first check to see 6 whether it may be
merged with an existing set. This means examining the other states to see
if one with the same core already exists. If so, you merge the new set with
the existing one, otherwise you add it normally.
Here is an example of this method in action:
S' –> S
S –> V = E
E –> F | E + F
F –> V | int | (E)
V –> id
Start building the LR(1) collection of configurating sets as you would
normally:
I0: S' –> •S, $
S –> •V = E, $
V –> •id, =
I1: S' –> S•, $
I2: S' –> V• = E, $
I3: V –> id•, =
I4: S –> V =•E, $
E –> •F, $/+
E –> •E + F, $/+
F –>•V, $/+
F –>•int, $/+
F –>•(E), $/+
V –>•id, $/+
I5: S –> V = E•, $
E –> E• + F, $/+
I6: E –> F•, $/+
I7: F–> V•, $/+
I8: F–> int•, $/+
76
76
I9: F–> (•E), $/+ Design and Implementation
of Modern Compiler
E –> •F, )/+
E –> •E + F, )/+
F –> •V, )/+
F –> •int, )/+
F –> •(E), )/+
V –> •id )/+
I10: F–> (E•), $/+
E –> E• + F, )/+
When we construct state I11, we get something we’ve seen before:
I11: E –>F•,)/+
It has the same core as I6, so rather than add a new state, we go ahead and
merge with that one to get:
I611: E –>F•, $/+/)
We have a similar situation on state I12 which can be merged with state I7.
The algorithm continues like this, merging into existing states where
possible and only adding new states when necessary. When we finish
creating the sets, we construct the table just as in LR(1).
2.4.8 An automatic parser generator
A parser generator takes a grammar as input and automatically generates
source code that can parse streams of characters using the grammar.
The generated code is a parser, which takes a sequence of characters and
tries to match the sequence against the grammar. The parser typically
produces a parse tree, which shows how grammar productions are expanded
into a sentence that matches the character sequence. The root of the parse
tree is the starting nonterminal of the grammar. Each node of the parse tree
expands into one production of the grammar.
The final step of parsing is to do something useful with this parse tree.
We’re going to translate it into a value of a recursive data type. Recursive
abstract data types are often used to represent an expression in a language,
like HTML, or Markdown, or Java, or algebraic expressions. A recursive
abstract data type that represents a language expression is called an abstract
syntax tree (AST).
Antlr is a mature and widely-used parser generator for Java, and other
languages as well.
Example tool for parser generator is YACC:
77
Design and implementation
of Modern Compilers
2.5 SUMMARY
78
78
3
ADVANCED SYNTAX ANALYSIS AND
BASIC SEMANTIC ANALYSIS
Unit Structure
3.0 Objectives
3.1 Introduction
3.2 Syntax-directed Translation
3.3 Syntax-directed Translation Schemes
3.4 Implementation of Syntax-directed Translators
3.4 Semantic Analysis
3.4.1 Introduction to Tiger Compiler
3.4.2 Symbol Tables
3.4.3 Bindings for Tiger Compiler
3.4.4 Type-checking Expressions
3.4.5 Type-checking Declarations
3.5 Activation Records
3.5.1 Stack Frames
3.5.2 Frames in the Tiger Compiler
3.6 Translation to Intermediate Code
3.6.1 Intermediate Representation Trees
3.6.2 Translation into Trees
3.6.3 Declarations
3.7 Basic Blocks and Traces
3.7.1 Taming Conditional Branches
3.8 Liveness Analysis
3.8.1 Solution of Dataflow Equations
3.8.2 Interference graph construction
3.8.3 Liveness in the Tiger Compiler
3.9 Summary
3.10 Exercise
3.11 Reference for further reading
79
Design and implementation
of Modern Compilers
3.0 OBJECTIVES
The aim of this chapter is to explain the role of the syntax analysis and to
introduce the important techniques used in the syntax analysis and semantic
analysis. After going through this unit, you will be able to understand:
3.1 INTRODUCTION
• For example:
82
82
3.3 IMPLEMENTATION OF SYNTAX-DIRECTED Advanced Syntax
Analysis and Basic
Semantic Analysis
TRANSLATORS
Any SDT is implemented by first creating a parse tree and then performing
the actions in preorder traversal.
Syntax Directed Translation (SDT) Schemes
1. Postfix Translation Schemes
2. Parser-Stack Implementation of Postfix SDT's
3. SDT's With Actions Inside Productions
4. SDT for L-Attributed Definitions
3.3.1 Postfix Translation Scheme:
In this scheme we parse the grammar bottom up and each action is placed
at the end of production i.e. all the actions are at right ends of the
productions. This SDT is called Postfix SDT.
Fig 3.2 implements Desk calculator SDD of Fig 3.1 as a postfix SDT. “Print
a value” action is performed for first production and rest of the actions are
equivalent to the semantic rules.
𝑡𝑜𝑝 = 𝑡𝑜𝑝 − 2}
𝑋→𝑇
𝑡𝑜𝑝 = 𝑡𝑜𝑝 − 2}
𝑇→𝐹
𝐹 → 𝑐𝑜𝑛𝑠𝑡
In several languages such as C, Pascal, Tiger etc., local variables are created
at the time of entry to the function and destroyed when function returns.
Several function calls may exist simultaneously and each call has its own
instances of variables. Consider below tiger function:
function f (a:int):int =
let var b := a+a
in if y<50
then f(b)
else b-1
end
89
Design and implementation Each time new instance of a is created when f is called and for each a
of Modern Compilers
instance of b is also created when entered into the body. Because this is
recursive call, many a’s exist simultaneously. A function returns in LIFO
manner i.e it returns when all its called functions have returned. So we can
use a Stack (LIFO) to hold local variables.
3.5.1 Stack Frames
Two operations are performed on stack data structures. On entry to the
function, local variables are pushed into the stack and popped on exit in
large batches. All variables are not initialized at time of push and we keep
accessing all variables deep in stack. This way we need a different suitable
model.
In this model, stack is used as array with special register (called stack
pointer), which locate variable in this big array. Size of stack increases with
entries and shrinks with exit from the function. These locations on the stack
allocated to the local variables, formal parameters, return and other
temporary identifiers are called that function’s Activation Records or
Stack Frames. For example consider following stack frame:
local
variables
return address current frame
temporaries
saved registers
argument m
outgoing arguments argument 2
stack pointer → argument 1
static link
next frame
↓ lower
addresses
Fig 3.8
90
90
Features of this stack frame: Advanced Syntax
Analysis and Basic
Semantic Analysis
i. Stack starts from higher addresses and grow towards lower addresses.
ii. In previous frame incoming arguments are passed by the caller and
stored at known offset from frame pointer.
iii. Return address is created by call statement, it tells where control
should return after completion of currently called function. Some
local variables are stored in current frame others are stored in machine
register. Machine register variables sometimes shifted to the frame to
create space in register.
iv. When this function calls another function (nested function) then
outgoing argument space is used to pass parameters.
v. Stack pointer is pointing to the first argument passed by calling
function. On entry of function new frame is allocated and size of that
frame is subtracted from SP to generate new SP. At this time old SP
is called Frame Pointer FP. i.e. FP=SP+ frame size.
vi. When function exits FP is copied back to SP and current FP attains
old FP value.
vii. Return address is the address of instruction which is just next to call
statement in the calling function.
3.5.2 Frames in the Tiger Compiler
Including Tiger there are many languages (Pascal, ML), which support
block structure feature. That feature allow, in nested functions, the inner
function can also use variables declared in outer function.
Following arrangements can achieve this.
i. When a function f is called, it is passed a pointer to the frame of that
function which enclosed f statistically. This pointer is called static
link.
ii. A global array is maintained which contains static nesting depth
pointers, this array is called Display.
iii. When a function f is called, all the variables of calling function are
also passed as extra arguments (and same way passed to nested
functions of f). This method is called lambda lifting.
If we uses C functions in Tiger then, Tiger compiler uses standard stack
frame layout, and abstract semantic analysis module which hides the
internal representation of the symbol tables. This abstract implementation
makes module machine independent.
Tiger compiler have two layers of abstraction between semantic analysis
and frame layout:
91
Design and implementation semant.c
of Modern Compilers
translate.h
translate.c
frame.h temp.h
𝜇 frame.c temp.c
Fig 3.9
Here,
92
92
Fig 3.10 without IR Advanced Syntax
Analysis and Basic
Semantic Analysis
93
Design and implementation
of Modern Compilers MEM
BINOP
+ TEMP fp CONST k
TRACES
Now the order of basic blocks doesn’t affect execution result. Control jumps
to new appropriate place at the end of each block. So we arrange these
blocks in such a way that every CJUMP is followed by its false label. Also
many target labels are immediate next to their unconditional jumps. So that
deletion of these unconditional JUMPs makes compilation of program
faster.
96
96
Advanced Syntax
Analysis and Basic
Semantic Analysis
𝒐𝒖𝒕[𝒏] = ⋃ 𝒊𝒏[𝒔]
𝒔∈𝒔𝒖𝒄𝒄[𝒏]
Where,
Pred[n]: set of all predecessors of node n
Succ[n]: set of all successors of node n
From above equation we can say:
A variable is live-in at node n, if it is in use set of that node i.e in use[n].
97
Design and implementation If a variable is live-out at n but it is not defined at n i.e not in def[n], then
of Modern Compilers
this variable must be line-in at n.
Live-out variable at n is live-in at all nodes s in succ[n].
3.8.2 INTERFERENCE GRAPH CONSTRUCTION
Liveness information is used in compiler optimization. Some optimization
algorithms need to know at each node in the flow graph, which set of
variables are live. Registers are allocated to the temporaries accordingly. If
we have a set of temporary variables v1,v2,v3,……, which are to be
allocated to registers r1,r2,r3,……, then the condition due to which we can’t
allocate same register to v1 & v2 is called an interference. This may occur
due to overlapping live period i.e v1 & v2 both are live at same time in the
program. In this case we can’t assign same register to these variables.
Interference can also occur when any variable v1 is generated by such
instruction which doesn’t address register r1, in this case v1 and r1 interfere.
Interference information is represented as matrix of variables by marking x
on the inference. This matrix can be expressed as undirected graph. Each
node of graph is representing variables and edge between two nodes
(variables) represent interference. Interference matrix and corresponding
graph of fig 3.9 is:-
A B C
A x
B x
C x x
Fig 3.13 interference representation
3.8.3 LIVENESS IN THE TIGER COMPILER
In tiger compiler, first control-flow graph is generated and then liveness of
a variable is analyzed. This analysis is expressed as interference graph. To
represent these two types of graph, an abstract data type Graph is created.
G_Graph() := function to create empty directed graph.
G_Node(g,x) := adds new node in graph g with additional information
x.
G_addEdge(n,m) := creates directed edge from n to m, now m is available
in G_succ(n) list and n is present in G_pred(m) list. If instruction n is
followed by instruction m (even by a jump), then there will be an edge
between n and m in the control-flow graph. In flow graph each node
contains information about the following:
a) FG_def(n): a set of all temporaries defined at n
b) FG_use(n): a set of temporary variables used at n
c) FG_isMove(n): represents any Move instruction at n
98
98
LIVENESS ANALYSIS Advanced Syntax
Analysis and Basic
Semantic Analysis
The liveness module takes flow-graph as input and produces:
• Interference graph
3.9 SUMMARY
This chapter gives the translation of languages guided by context-free
grammars. The translation techniques in this chapter are applied for type
checking and intermediate-code generation in compiler design. The
techniques are also useful for implementing little languages for specialized
tasks. To illustrate the issues in compiling real programming languages,
code snippets of Tiger (a simple but nontrivial language of the Algol family,
with nested scope and heap-allocated records) are discussed. These code
snippets can be implemented in C-language or java. For complete code refer
book by A.Andrew et.al., Modern Compiler Implementation in java (2004).
3.10 EXCERCISE
Q1. What are inherited and synthesized attributes?
Q2. What is the difference between syntax directed definition and syntax
directed translation?
99
Design and implementation
of Modern Compilers
3.11 REFERENCE FOR FURTHER READING
100
100
4
DATAFLOW ANALYSIS AND LOOP
OPTIMIZATION
Unit Structure
4.0 Objectives
4.1 Introduction
4.2 Overview
4.3 The Principle Sources of Optimization
4.3.1 Loop Optimization:
4.3.2 The Dag Representation of Basic Blocks
4.3.3 Dominators
4.3.4 Reducible Flow Graphs
4.3.5 Depth-First Search
4.3.6 Loop-Invariant Computations
4.3.7 Induction Variable Elimination
4.3.8 Some Other Loop Optimizations.
4.3.8.1 Frequency Reduction (Code Motion):
4.3.8.2 Loop Unrolling:
4.3.8.3 Loop Jamming:
4.4 Dataflow Analysis
4.4.1 Intermediate Representation for Flow Analysis
4.4.2 Various Dataflow Analyses
4.4.3 Transformations Using Dataflow Analysis
4.4.4 Speeding Up Dataflow Analysis
4.4.4.1 Bit Vectors
4.4.4.2 Basic Blocks
4.4.4.3 Ordering the Nodes
4.4.4.4 Work-List Algorithms
4.4.4.5 Use-Def and Def-Use Chains
4.4.4.6 Wordwise Approach
4.5 Alias Analysis
4.6 Summary
4.7 Bibliography
4.8 Unit End Exercises
101
Design and implementation
of Modern Compilers
4.0 OBJECTIVES
After going through this chapter you will be able to understand the
following concepts in detail:-
DAG Representation, Dominators, Reducible flow graphs, Depth-first
search, Loop invariant computations, Induction variable elimination,
various loop optimizations.
Also Intermediate representation, dataflow analyses, transformations,
speeding up and alias analysis.
4.1 INTRODUCTION
4.2 OVERVIEW
102
102
A DAG for basic block is a directed acyclic graph with the following labels Dataflow Analysis and
Loop Optimization
on nodes:
• The leaves of graph are labelled by unique identifier and that identifier
can be variable names or constants.
• Interior nodes of the graph are labelled by an operator symbol.
• Nodes are also given a sequence of identifiers for labels to store the
computed value.
• DAGs are a type of data structure. It is used to implement
transformations on basic blocks.
• DAG provides a good way to determine the common sub-expression.
• It gives a picture representation of how the value computed by the
statement is used in subsequent statements.
103
Design and implementation Create node(OP) for case(1), with node(z) as its right child and node(OP)
of Modern Compilers
as its left child (y).
For the case (2), see if there is a node operator (OP) with one child node (y).
Node n will be node(y) in case (3).
Step 3 –
Remove x from the list of node identifiers. Step 2: Add x to the list of
attached identifiers for node n.
Example :
T0 = a + b —Expression 1
T1 = T0 + c —-Expression 2
d = T0 + T1 —–Expression 3
Expression 1 : T0 = a + b
Expression 2: T1 = T0 + c
Expression 3 : d = T0 + T1
104
104
Dataflow Analysis and
Loop Optimization
Example :
T1 = a + b
T2 = T1 + c
T3 = T1 x T2
Example :
T1:= 4*I0
T2:= a[T1]
T3:= 4*I0
T4:= b[T3]
T5:= T2 * T4
T6:= prod + T
105
Design and implementation prod:= T6
of Modern Compilers
T7:= I0 + 1
I0:= T7
if I0 <= 20 goto 1
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.
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.
A depth first ordering can be used to detect loops in any flow graph; it also
helps speed up iterative data flow algorithms.
One possible DFS representation of the data flow on left given on the right
side figure.
108
108
Dataflow Analysis and
Loop Optimization
• Definition
• A loop invariant is a condition [among program variables] that is
necessarily true immediately before and immediately after each
iteration of a loop.
• A loop invariant is some predicate (condition) that holds for every
iteration of the loop.
• For example, let’s look at a simple for loop that looks like this:
• Int j = 9;
• for (int i=0; i<10; i++)
• J--;
• In this example it is true (for every iteration) that i + j == 9.
• A weaker invariant that is also true is that i >= 0 && i <= 10.
• One may get confused between the loop invariant, and the loop
conditional ( the condition which controls termination of the loop ).
• The loop invariant must be true:
• before the loop starts
• before each iteration of the loop
• after the loop terminates ( although it can temporarily be false
during the body of the loop ).
• On the other hand the loop conditional must be false after the
loop terminates, otherwise, the loop would never terminate.
• Usage:
Loop invariants capture key facts that explain why code works. This means
that if you write code in which the loop invariant is not obvious, you should
add a comment that gives the loop invariant. This helps other programmers
understand the code, and helps keep them from accidentally breaking the
invariant with future changes.
109
Design and implementation A loop Invariant can help in the design of iterative algorithms when
of Modern Compilers
considered an assertion that expresses important relationships among the
variables that must be true at the start of every iteration and when the loop
terminates. If this holds, the computation is on the road to effectiveness. If
false, then the algorithm has failed.
Loop invariants are used to reason about the correctness of computer
programs. Intuition or trial and error can be used to write easy algorithms
however when the complexity of the problem increases, it is better to use
formal methods such as loop invariants.
Loop invariants can be used to prove the correctness of an algorithm, debug
an existing algorithm without even tracing the code or develop an algorithm
directly from specification.
A good loop invariant should satisfy three properties:
Data flow analysis is a process for collecting information about the use,
definition, and dependencies of data in programs. The data flow analysis
algorithm operates on a control flow graph generated from an AST. You
can use a control flow graph to determine the parts of a program to which a
particular value assigned to a variable might propagate.
An execution path (or path) from point p1 to point pn is a sequence of points
p1, p2, ..., pn such that:
for each i = 1, 2, ..., n − 1, either 1 pi is the point immediately preceding a
statement and pi+1 is the point immediately following that same statement,
or
pi is the end of some block and pi+1 is the beginning of a successor block.
111
Design and implementation In general, there is an infinite number of paths through a program and there
of Modern Compilers
is no bound on the length of a path. Program analyses summarize all
possible program states that can occur at a point in the program with a finite
set of facts.
No analysis is necessarily a perfect representation of the state.
Process of dataflow analysis:
1) Build a flow graph(nodes = basic blocks, edges = control flow)
2) set up a set of equations between in[b] and out[b] for all basic blocks
b.
Effect of code in basic block:
112
112
The portion of the compiler that does scanning, parsing and static semantic Dataflow Analysis and
Loop Optimization
analysis is called the front-end.
The translation and code generation portion of it is called the back-end.
The front-end depends mainly on the source language and the back-end
depends on the target architecture.
More than one intermediate representation may be used for different levels
of code improvement. A high level intermediate form preserves source
language structure. Code improvements on loop can be done on it.
A low level intermediate form is closer to target architecture.
Parse tree is a representation of complete derivation of the input. It has
intermediate nodes labeled with non-terminals of derivation.This is used
(often implicitly) for parsing and attribute synthesis.
A syntax tree is very similar to a parse tree where extraneous nodes are
removed.
It is a good representation that is close to the source-language as it preserves
the structure of source constructs.
It may be used in applications like source-to-source translation, or syntax-
directed editor etc.
Linear Intermediate Representation:-
Both the high-level source code and the target assembly codes are linear in
their text.
The intermediate representation may also be linear sequence of codes. with
conditional branches and jumps to control the flow of computation.
A linear intermediate code may have one operand address a , two-address
b, or three-address like RISC architectures.
GCC Intermediate Codes:-
The GCC compiler uses three intermediate representations:
1. GENERIC - it is a language independent tree representation of the
entire function.
2. GIMPLE - is a three-address representation generated from
GENERIC.
3. RTL - a low-level representation known as register transfer language.
Consider the following C function.
double CtoF(double cel) { return cel * 9 / 5.0 + 32 ;}
C program with if:-
#include <stdio.h>
int main()
{
113
Design and implementation int l, m ;
of Modern Compilers
scanf("%d", &l);
if(l < 10) m = 5*l;
else m = l + 10;
printf("l: %d, m: %d\n", l, m);
return 0;
}
C program with for:-
#include <stdio.h>
int main()
{
int n, i, sum=0 ;
scanf("%d", &n);
for(i=1; i<=n; ++i) sum = sum+i;
printf("sum: %d\n", sum);
return 0;
}
Representation of Three-Address Code:-
Any three address code has two essential components: operator and
operand.
There can be at most three operands and one operator.
The operands are of three types, a name from the source program, a
temporary name generated by the compiler or a constant a.
a There are different types of constants used in a programming language.
There is another category of name, a label in the sequence of three-address
codes.
A three-address code sequence may be represented as a list or array of
structures.
Quadruple:-
A quadruple is the most obvious first choice a.
It has an operator, one or two operands, and the target field.
Triple:-
A triple is a more compact representation of a three-address code.
It does not have an explicit target field in the record.
114
114
When a triple u uses the value produced by another triple d, then u refers to Dataflow Analysis and
Loop Optimization
the value number (index) of d.
Example:-
t1 = a * a
t2 = a * b
t3 = t1 + t2
t4 = t3 + t2
t5 = t1 + t4
Indirect Triple:-
It may be necessary to reorder instructions for the improvement of
execution.
Reordering is easy with a quad representation, but is problematic with triple
representation as it uses absolute index of a triple.
As a solution indirect triples are used, where the ordering is maintained by
a list of pointers (index) to the array of triples.
The triples are in their natural translation order and can be accessed by their
indexes.
But the execution order is maintained by an array of pointers (index)
pointing to the array of triples.
Static Single-Assignment (SSA) Form:-
This representation is similar to three-address code with two main
differences.
Every definition a has a distinct name (virtual register).
Each use of a value refers to a particular definition.
e.g. t7= a + t3.
If the same user variable is defined on more than one control paths a, they
are renamed as distinct variables with appropriate subscripts.
When more than one control-flow paths join, a φ-function is used to
combine the variables.
The φ-function selects the value of its arguments depending on the control-
flow path (data-flow under control-flow).
Each name is defined at one place a. Use of a name contains information
about the location of its definition (data-flow).
SSA-form tries to encode data-flow under flow-control.
Consider the following C code:
for(f=i=1; i<=n; ++i) f = f*i;
115
Design and implementation The corresponding three-address codes and SSA codes are as follows.
of Modern Compilers
i = 1 i0 = 1
f = 1 f0 = 1
L2: if i>n goto - if i0 > n goto L1
L2: i1 =
φ(i0, i2)
f1 =
φ(f0, f2)
f = f*i f2 = f1*i1
i = i + 1 i2 = i1 + 1
goto L2 if i2 <= n goto L2
L1: i3 =
φ(i0, i2)
f3 =
φ(f0, f2)
4.4.2 Various dataflow analyses
A data-flow value for a program point represents an abstraction of the set
of all possible program states that can be observed for that point The set of
all possible data-flow values is the domain for the application under
consideration Example: for the reaching definitions problem, the domain of
data-flow values is the set of all subsets of definitions in the program A
particular data-flow value is a set of definitions IN[s] and OUT[s]: data-
flow values before and after each statement s The data-flow problem is to
find a solution to a set of constraints on IN[s] and OUT[s], for all statements.
Two kinds of constraints :
Those based on the semantics of statements (transfer functions)
Those based on flow of control
116
116
We always compute safe estimates of data-flow values Dataflow Analysis and
Loop Optimization
A decision or estimate is safe or conservative, if it never leads to a change
in what the program computes (after the change)
These safe values may be either subsets or supersets of actual values, based
on the application
Basic Terminologies –
Definition Point: a point in a program containing some definition.
Reference Point: a point in a program containing a reference to a data item.
Evaluation Point: a point in a program containing evaluation of expression.
Now let us take a global view and consider all the points in all the blocks.
A path from p1 to pn is a sequence of points p1, p2,….,pn such that for each
i between 1 and n-1, either
1. Pi is the point immediately preceding a statement and pi+1 is the point
immediately following that statement in the same block, or
2. Pi is the end of some block and pi+1 is the beginning of a successor
block.
Reaching definitions
A definition of variable x is a statement that assigns, or may assign, a value
to x. The most common forms of definition are assignments to x and
statements that read a value from an i/o device and store it in x. These
118
118
statements certainly define a value for x, and they are referred to as Dataflow Analysis and
Loop Optimization
unambiguous definitions of x. There are certain kinds of statements that
may define a value for x; they are called ambiguous definitions.
The most usual forms of ambiguous definitions of x are:
1. A call of a procedure with x as a parameter or a procedure that can
access x because x is in the scope of the procedure.
2. 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.
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 by an unambiguous definition and an
ambiguous definition of the appearing later along one path.
120
120
There is a subtle miscalculation in the rules for gen and kill. We have made Dataflow Analysis and
Loop Optimization
the assumption that the conditional expression E in the if and do statements
are “uninterpreted”; that is, there exists inputs to the program that make
their branches go either way.
We assume that any graph-theoretic path in the flow graph is also an
execution path, i.e., a path that is executed when the program is run with
least one possible input. When we compare the computed gen with the
“true” gen we discover that the true gen is always a subset of the computed
gen. on the other hand, the true kill is always a superset of the computed
kill.
These containments hold even after we consider the other rules. It is natural
to wonder whether these differences between the true and computed gen
and kill sets present a serious obstacle to data-flow analysis. The answer lies
in the use intended for these data.
Overestimating the set of definitions reaching a point does not seem serious;
it merely stops us from doing an optimization that we could legitimately do.
On the other hand, underestimating the set of definitions is a fatal error; it
could lead us into making a change in the program that changes what the
program computes. For the case of reaching definitions, then, we call a set
of definitions safe or conservative if the estimate is a superset of the true set
of reaching definitions. We call the estimate unsafe, if it is not necessarily
a superset of the truth.
Returning now to the implications of safety on the estimation of gen and
kill for reaching definitions, note that our discrepancies, supersets for gen
and subsets for kill are both in the safe direction. Intuitively, increasing gen
adds to the set of definitions that can reach a point, and cannot prevent a
definition from reaching a place that it truly reached. Decreasing kill can
only increase the set of definitions reaching any given point.
Computation of in and out:
Many data-flow problems can be solved by synthesized translation to
compute gen and kill. It can be used, for example, to determine
computations. 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. Assuming we
know in[S] we compute out by equation, that is
Out[S] = gen[S] U (in[S] - kill[S])
121
Design and implementation Considering cascade of two statements S1; S2, as in the second case. We
of Modern Compilers
start by observing in[S1]=in[S]. Then, we recursively compute out[S1],
which gives us in[S2], since a definition reaches the beginning of S2 if and
only if it reaches the end of S1. Now we can compute out[S2], and this set
is equal to out[S].
Consider the 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. That is,
in[S1] = in[S2] = in[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 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 implement complement of B and then using logical and to
compute A
Local reaching definitions:
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:
It is often convenient to store the reaching definition information as” use-
definition chains” or “ud-chains”, which are lists, for each use of a variable,
of all the definitions that reaches that use. If a use of variable a in block B
122
122
is preceded by no unambiguous definition of a, then ud-chain for that use of Dataflow Analysis and
Loop Optimization
a is the set of definitions in in[B] that are definitions of a.in addition, if there
are ambiguous definitions of a ,then all of these for which no unambiguous
definition of a lies between it and the use of a are on the ud-chain for this
use of a.
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.
General control flow:
Data-flow analysis must take all control paths into account. If the control
paths are evident from the syntax, then data-flow equations can be set up
and solved in a syntax directed manner. When programs can contain goto
statements or even the more disciplined break and continue statements, the
approach we have taken must be modified to take the actual control paths
into account.
Several approaches may be taken. The iterative method works arbitrary
flow graphs. Since the flow graphs obtained in the presence of break and
continue statements are reducible, such constraints can be handled
systematically using the interval-based methods. However, the syntax-
directed approach need not be abandoned when break and continue
statements are allowed.
4.4.4 Speeding up dataflow analysis
There are several ways to speed up the evaluation of dataflow equations.
4.4.4.1 Bit vectors
• Suppose we have a node n in the flow graph that has only one
predecessor, p, and p has only one successor, n.
• we can combine the gen and kill effects of p and n and replace nodes
n and p with a single node.
• Such a single node is called a basic block.
• A basic block is a sequence of statements that is always entered at the
beginning and exited at the end, that is:
The first statement is a label.
The last statement is a jump or cjump.
There are no other labels, jumps, or cjumps.
• The algorithm for dividing a long sequence of statements into basic
blocks is quite simple. The sequence is scanned from beginning to
end;
1. whenever a label is found, a new block is started (and the
previous block is ended);
2. whenever a jump or cjump is found, a block is ended (and the
next block is started).
3. If this leaves any block not ending with a jump or cjump, then
a jump to the next block’s label is appended to the block.
4. If any block has been left without a label at the beginning, a new
label is invented and stuck there.
We introduce a new label done which mean the beginning of the
epilogue, and put a jump(name done) at the end of the last block.
1. start with the in set computed for the entire block and,
2. apply the gen and kill sets of the statements that precede n in
the block.
125
Design and implementation
of Modern Compilers
If two or more expressions denote the same memory address we can say
that the expressions are aliases of each other.
How do aliases arise?
• Pointers
• Call by reference (parameters can alias each other or non-locals)
• Array indexing
• C union, Pascal variant records, Fortran EQUIVALENCE and
COMMON blocks
Alias analysis techniques are usually classified by flow-sensitivity and
context-sensitivity. They may determine may-alias or must-alias
information. The term alias analysis is often used interchangeably with
points-to analysis, a specific case.
127
Design and implementation Alias analysers intend to make and compute useful information for
of Modern Compilers
understanding aliasing in programs.
Example code:
p.foo = 1;
q.foo = 2;
i = p.foo + 3;
There are three possible alias cases here:
The variables p and q cannot alias (i.e., they never point to the same memory
location).
The variables p and q must alias (i.e., they always point to the same memory
location).
It cannot be conclusively determined at compile time if p and q alias or not.
If p and q cannot alias, then i = p.foo + 3; can be changed to i = 4. If p and
q must alias, then i = p.foo + 3; can be changed to i = 5 because p.foo + 3 =
q.foo + 3. In both cases, we are able to perform optimizations from the alias
knowledge (assuming that no other thread updating the same locations can
interleave with the current thread, or that the language memory model
permits those updates to be not immediately visible to the current thread in
absence of explicit synchronization constructs).
On the other hand, if it is not known if p and q alias or not, then no
optimizations can be performed and the whole of the code must be executed
to get the result. Two memory references are said to have a may-alias
relation if their aliasing is unknown.
In alias analysis, we divide the program's memory into alias classes. Alias
classes are disjoint sets of locations that cannot alias to one another. For the
discussion here, it is assumed that the optimizations done here occur on a
low-level intermediate representation of the program. This is to say that the
program has been compiled into binary operations, jumps, moves between
registers, moves from registers to memory, moves from memory to
registers, branches, and function calls/returns.
There are two ways for Alias Analysis:
Type-based alias analysis
Flow-based alias analysis
4.6 SUMMARY
128
128
Compiler optimization is generally implemented using a sequence of Dataflow Analysis and
Loop Optimization
optimizing transformations, algorithms which take a program and transform
it to produce a semantically equivalent output program that uses fewer
resources or executes faster.
Data-flow optimizations, based on data-flow analysis, primarily depend on
how certain properties of data are propagated by control edges in the
control-flow graph.
We have learnt about various techniques of loop optimization and dataflow
analysis as applicable for compiler design.
4.7 BIBLIOGRAPHY
1. For the above graph find the live expressions at the end of each block.
2. For the above graph find the available expressions
3. Are there any expressions which may be hoisted in above example, if
so hoist them
4. Is there any constant folding possible in above graph. If so, do it.
5. Eliminate any common sub-expressions in the above figure
6. In the above figure, what is the limit flow graph? Is the flow graph
reducible.
7. Give an algorithm in time O(n) on an n-node flow graph to find the
extended basic block ending at each node.
129